Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
- name: Check each provider feature standalone
run: |
set -euo pipefail
for feature in cli keyring gcsm awssm vault openbao infisical bws akv; do
for feature in cli keyring gcsm awssm vault openbao infisical bws akv sops; do
echo "::group::$feature"
cargo check --package secretspec --no-default-features --features "$feature"
echo "::endgroup::"
Expand All @@ -75,6 +75,20 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- name: Download SOPS 3.13.1
uses: robinraju/release-downloader@28fc21f50d76778e7023361aa1f863e717d3d56f
with:
repository: getsops/sops
tag: v3.13.1
fileName: sops-v3.13.1.amd64.exe
out-file-path: sops-bin

- name: Add SOPS to PATH
shell: pwsh
run: |
Rename-Item -Path "sops-bin/sops-v3.13.1.amd64.exe" -NewName "sops.exe"
(Resolve-Path "sops-bin").Path |
Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install Rust
run: rustup toolchain install stable --profile minimal
- name: cargo test
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- SOPS provider (`sops://`, `sops` build feature) for reading and writing
YAML, JSON, dotenv, and INI files through the SOPS CLI, including templated
per-project/profile paths and provider-credential injection for encryption
keys and cloud authentication.
- `secretspec config init --provider <PROVIDER> --profile <PROFILE>` can save
global defaults without interactive prompts, including `--profile none` to
clear the default profile.
Expand Down
6 changes: 5 additions & 1 deletion devenv.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
# rust-toolchain.toml (read automatically via toolchainFile).
toolchainFile = ./rust-toolchain.toml;
};

languages.javascript = {
directory = "./docs";
enable = true;
# Node 22 (the plain nixpkgs default) bundles npm 10.x, which mishandles
# npm Trusted Publishing's OIDC handshake and can even misreport a brand
Expand Down Expand Up @@ -70,6 +72,8 @@
# ext-php-rs's bindgen step can parse php.h.
pkgs.php.unwrapped.dev
pkgs.rustPlatform.bindgenHook
# For development of the SOPS provider
pkgs.sops
];

# Fully-static musl build of the Go SDK (-tags static + -extldflags -static).
Expand Down Expand Up @@ -110,7 +114,7 @@
# Build the CLI for integration tests
cargo build --release
export PATH="$PWD/target/release:$PATH"

# Run CLI integration tests
bash tests/cli-integration.sh
'';
Expand Down
7 changes: 6 additions & 1 deletion docs/astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ $ secretspec import dotenv://.env.production

## Providers

Secrets can be stored in: keyring (default), KeePass KDBX (0.17+), dotenv files, environment variables, systemd service credentials (0.17+), 1Password, Gopass (0.15+), LastPass, Pass, Proton Pass, Google Cloud Secret Manager, AWS Secrets Manager, HashiCorp Vault, OpenBao (0.17+), Bitwarden Secrets Manager, Azure Key Vault, Infisical (0.16+), or age (0.17+).`,
Secrets can be stored in: keyring (default), KeePass KDBX (0.17+), dotenv files, environment variables, systemd service credentials (0.17+), 1Password, Gopass (0.15+), LastPass, Pass, Proton Pass, Google Cloud Secret Manager, AWS Secrets Manager, HashiCorp Vault, OpenBao (0.17+), Bitwarden Secrets Manager, Azure Key Vault, Infisical (0.16+), age (0.17+), or SOPS (0.17+).`,
}),
],
title: "SecretSpec",
Expand Down Expand Up @@ -252,6 +252,11 @@ Secrets can be stored in: keyring (default), KeePass KDBX (0.17+), dotenv files,
slug: "providers/age",
badge: { text: "0.17+", variant: "note" },
},
{
label: "SOPS",
slug: "providers/sops",
badge: { text: "0.17+", variant: "note" },
},
],
},
{
Expand Down
5 changes: 5 additions & 0 deletions docs/src/assets/sops-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion docs/src/content/docs/concepts/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ DATABASE_URL = { description = "Production database", providers = ["prod_vault"]
| [akv](/providers/akv/) | Azure Key Vault (requires the `akv` build feature) | ✓ | ✓ | ✓ | — |
| [infisical](/providers/infisical/) (0.16+) | Infisical (requires the `infisical` build feature) | ✓ | ✓ | ✓ | — |
| [age](/providers/age/) (0.17+) | An age-encrypted file (requires the `age` build feature) | ✓ | ✓ | ✓ | — |
| [sops](/providers/sops/) (0.17+) | SOPS-encrypted files (requires the `sops` build feature and SOPS CLI) | ✓ | ✓ | ✓ | Depends on the configured SOPS key service |

“TPM-backed keys” means the local key used by the provider can be protected by
a [TPM 2.0](https://trustedcomputinggroup.org/resource/tpm-library-specification/)
Expand Down Expand Up @@ -295,7 +296,10 @@ Provider credentials follow these rules:
- **Names are provider-specific.** Bitwarden accepts `access_token`; Vault
accepts `token`, `role_id`, and `secret_id`; 1Password accepts
`service_account_token`; Azure Key Vault accepts `tenant_id`, `client_id`, and
`client_secret`. Unsupported names are rejected before any source is read.
`client_secret`; SOPS (0.17+) accepts `age_key`, `aws_secret_access_key`,
`azure_client_secret`, `google_oauth_access_token`, `hc_vault_token`,
`huawei_sdk_ak`, and `huawei_sdk_sk`. Unsupported names are rejected before
any source is read.

## Next steps

Expand Down
207 changes: 207 additions & 0 deletions docs/src/content/docs/providers/sops.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
---
title: "SOPS: Secrets OPerationS"
description: SOPS integration
tableOfContents:
maxHeadingLevel: 4
---

The SOPS provider integrates with <a href="https://getsops.io" target="_blank" rel="noopener noreferrer">SOPS</a> to enable reading and writing of secrets encrypted at rest in local files.

:::caution[Version compatibility]
The `sops` provider is added in SecretSpec 0.17.
:::

## Prerequisites

- The SOPS CLI available within the environment:
- <a href="https://github.com/getsops/sops/releases" target="_blank" rel="noopener noreferrer">Manually download a release binary</a>
- <a href="https://search.nixos.org/packages?channel=unstable&query=sops#show=sops" target="_blank" rel="noopener noreferrer">Use the SOPS Nix package</a>
- Install with a package manager:

```bash
# Homebrew
$ brew install sops

# Arch
$ sudo pacman -S sops
```

## Configuration

### URI Format

```
sops://path/to/secret[?key=value[&key=value]...]
```

- `path/to/secret` — required path to the encrypted file (absolute or relative)

- :::tip[Secrets in multiple files?]
In order to represent more than one file the path must be templated. i.e. separate SOPS-encrypted files per profile, or a hierarchical directory structure.

A templated path must include both `{project}` and `{profile}` placeholders. Examples:

- `sops://secrets-dir/{project}.{profile}.enc.json`
- `sops://secrets-dir/{project}/{profile}.enc.yaml`
:::

- `?key=value` -- optional query parameter, refer to the [Query Parameters section](#query-parameters) for available parameters.
- `&key=value` — additional parameters

### Query Parameters

Excepting the SecretSpec-specific `format` parameter, refer to <a href="https://getsops.io/docs/#usage" target="_blank" rel="noopener noreferrer">the SOPS documentation</a> for the purpose and usage of each parameter.

#### SecretSpec

| Provider URL Query Parameter Name | Purpose |
| --------------------------------- | ----------------------------------------------------------------------------------------------------- |
| format | Overrides the extension-based file format detection. Valid values: `dotenv` `env` `ini` `json` `yaml` |

#### SOPS

| Provider URL Query Parameter Name | Corresponding Environment Variable |
| --------------------------------- | ---------------------------------- |
| sops_config | SOPS_CONFIG |
| sops_decryption_order | SOPS_DECRYPTION_ORDER |
| sops_editor | SOPS_EDITOR |
| sops_enable_local_keyservice | SOPS_ENABLE_LOCAL_KEYSERVICE |
| sops_keyservice | SOPS_KEYSERVICE |

#### Age

| Provider URL Query Parameter Name | Corresponding Environment Variable |
| --------------------------------- | ---------------------------------- |
| age_key_cmd | SOPS_AGE_KEY_CMD |
| age_key_file | SOPS_AGE_KEY_FILE |
| age_recipients | SOPS_AGE_RECIPIENTS |
| age_ssh_private_key_cmd | SOPS_AGE_SSH_PRIVATE_KEY_CMD |
| age_ssh_private_key_file | SOPS_AGE_SSH_PRIVATE_KEY_FILE |

#### AWS

| Provider URL Query Parameter Name | Corresponding Environment Variable |
| --------------------------------- | ---------------------------------- |
| aws_access_key_id | AWS_ACCESS_KEY_ID |
| aws_profile | AWS_PROFILE |
| aws_region | AWS_REGION |
| kms_arn | SOPS_KMS_ARN |

#### GCP

| Provider URL Query Parameter Name | Corresponding Environment Variable |
| --------------------------------- | ---------------------------------- |
| gcp_kms_client_type | SOPS_GCP_KMS_CLIENT_TYPE |
| gcp_kms_endpoint | SOPS_GCP_KMS_ENDPOINT |
| gcp_kms_ids | SOPS_GCP_KMS_IDS |
| gcp_kms_universe_domain | SOPS_GCP_KMS_UNIVERSE_DOMAIN |

#### Azure

| Provider URL Query Parameter Name | Corresponding Environment Variable |
| --------------------------------- | ---------------------------------- |
| azure_client_id | AZURE_CLIENT_ID |
| azure_keyvault_urls | SOPS_AZURE_KEYVAULT_URLS |
| azure_tenant_id | AZURE_TENANT_ID |

#### PGP

| Provider URL Query Parameter Name | Corresponding Environment Variable |
| --------------------------------- | ---------------------------------- |
| pgp_fp | SOPS_PGP_FP |

#### GPG

| Provider URL Query Parameter Name | Corresponding Environment Variable |
| --------------------------------- | ---------------------------------- |
| gpg_exec | SOPS_GPG_EXEC |

#### Hashicorp Vault/OpenBao

| Provider URL Query Parameter Name | Corresponding Environment Variable |
| --------------------------------- | ---------------------------------- |
| hc_vault_addr | VAULT_ADDR |
| hc_vault_allowlist | SOPS_HC_VAULT_ALLOWLIST |

#### Huawei Cloud

| Provider URL Query Parameter Name | Corresponding Environment Variable |
| --------------------------------- | ---------------------------------- |
| huawei_kms_ids | SOPS_HUAWEICLOUD_KMS_IDS |
| huawei_sdk_project_id | HUAWEICLOUD_SDK_PROJECT_ID |

### Provider credentials

Secret values used to authenticate SOPS belong in a provider alias's
`credentials` map, not in the SOPS URI. The SOPS provider accepts these
semantic credential names:

| Credential | SOPS environment fallback |
| ---------- | ------------------------- |
| `age_key` | `SOPS_AGE_KEY` |
| `aws_secret_access_key` | `AWS_SECRET_ACCESS_KEY` |
| `azure_client_secret` | `AZURE_CLIENT_SECRET` |
| `google_oauth_access_token` | `GOOGLE_OAUTH_ACCESS_TOKEN` |
| `hc_vault_token` | `VAULT_TOKEN` |
| `huawei_sdk_ak` | `HUAWEICLOUD_SDK_AK` |
| `huawei_sdk_sk` | `HUAWEICLOUD_SDK_SK` |

For example, this alias loads an age identity from the system keyring and
passes it only to the SOPS child process:

```toml title="secretspec.toml"
[providers]
sops_age = {
uri = "sops://secrets.enc.yaml?age_recipients=age1example",
credentials = { age_key = "keyring" }
}

[profiles.production.defaults]
providers = ["sops_age"]
```

When a credential is not declared on the alias, SOPS can still use its normal
environment variable. See [Provider credentials](/concepts/providers/#provider-credentials)
for convention and explicit credential-source addresses.

### Format handling

SecretSpec asks SOPS to emit JSON when decrypting, then uses one lookup path
for YAML, JSON, dotenv, and INI files. When `?format=` is present, SecretSpec
also passes the corresponding SOPS input type so filenames such as
`.env.production.enc?format=dotenv` work correctly.

SOPS does not support `--input-type ini`, so `?format=ini` is accepted only
when the filename itself ends in `.ini`. Without `?format=`, the filename must
end in `.yaml`, `.yml`, `.json`, `.env`, `.dotenv`, or `.ini`; an unrecognized
extension is reported as a configuration error.

YAML and JSON single-file layouts can nest values by project and profile. INI
uses profile sections, while dotenv is always flat; in a single dotenv file,
the same key therefore cannot hold different values for different profiles.
Use a templated path such as `.env.{profile}.enc?format=dotenv` when profiles
need separate dotenv values.

## Usage

### Set a secret with age

```bash
$ secretspec set DATABASE_URL --provider sops://secrets.enc.json?age_key_file=key.txt&age_recipients=age1jpa8rf5qmrg6pw444fcgpkaxg8x4neueszrexzagdjpunjlgeyzq304w34

```

Or, if `keys.txt` exists in a place discoverable by SOPS, and `.sops.yaml` exists in the project and has been configured e.g.

```yaml sops.yaml
creation_rules:
- path_regex: secrets.enc.json$
age: "age1jpa8rf5qmrg6pw444fcgpkaxg8x4neueszrexzagdjpunjlgeyzq304w34"
```

then:

```bash
$ secretspec set DATABASE_URL --provider sops://secrets.enc.json

```
1 change: 1 addition & 0 deletions docs/src/content/docs/quick-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ $ secretspec config init
akv: Azure Key Vault
infisical: Infisical secret management (0.16+)
age: age-encrypted file (0.17+)
sops: SOPS encrypted files (0.17+)
? Select your default profile:
> development
default
Expand Down
Loading