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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Flat layout (`?layout=flat`), a general provider setting that resolves and
writes convention secrets by key alone at the store root, dropping the
`secretspec/{project}/{profile}` scaffolding — the natural shape for a
single-project store, such as one migrated from another secret manager.
Supported by every hierarchical provider (Infisical, Vault, AWS Secrets
Manager, Google Cloud Secret Manager and Azure Key Vault); providers with no
hierarchy (`dotenv`, `env`, `bws`) are already flat and ignore it. Any
provider-native container (a Vault mount) or user-supplied prefix (an AWS
`?prefix=`, an Infisical `?path=`) still applies. The default nested layout is
unchanged. Dropping the profile gives up profile separation, so a provider
that pins its environment another way (an Infisical `?env=`) combined with
`?layout=flat` collapses every profile onto one key deliberately; and because
the key is used as the backend name directly, it must be a legal name there
(Azure Key Vault, which uses it verbatim, refuses a key it cannot store).
- Vault / OpenBao JWT/OIDC authentication (`?auth=jwt`) logs in through a
configured Vault role using `VAULT_JWT`, or requests a short-lived OIDC token
automatically in GitHub Actions and Forgejo Actions jobs with `id-token:
Expand Down
21 changes: 21 additions & 0 deletions docs/src/content/docs/providers/akv.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ akv://VAULT_NAME[?auth=env|cli|managed_identity|workload_identity][&suffix=DNS_S
- `managed_identity` — the VM / App Service / AKS system-assigned managed identity
- `workload_identity` — AKS workload identity federation (`AZURE_TENANT_ID`/`AZURE_CLIENT_ID`/`AZURE_FEDERATED_TOKEN_FILE`, injected automatically by AKS)
- `suffix`: an explicit Key Vault DNS suffix for a bare `VAULT_NAME`, e.g. `akv://myvault?suffix=vault.azure.cn` for a sovereign cloud, instead of relying on a dotted `VAULT_NAME`
- `?layout=flat` (0.17+): use the key as the Azure secret name verbatim, with no Base32-encoded `{project}/{profile}` scaffolding — see [Layout](#layout-017)

### URI examples

Expand Down Expand Up @@ -101,6 +102,26 @@ though Key Vault's identifiers do not preserve all of those distinctions. The
encoded components contain no hyphens, so the `--` component separators cannot
be confused with component data.

### Layout (0.17+)

Added in SecretSpec 0.17; `?layout=flat` is not available in SecretSpec 0.16 or earlier.

`?layout=` is a [general provider setting](/reference/providers/#layout-flat-017), spelled the same
way across every hierarchical backend. The default **nested** layout Base32-encodes the components
into `secretspec--{base32(project)}--{base32(profile)}--{base32(key)}`, as above.

The **flat** layout (`?layout=flat`) drops the `{project}/{profile}` scaffolding and uses the
`{key}` as the Azure secret name **verbatim** — the shape a store migrated from another manager
already has. Because there is no Base32 rewrite to fall back on, the key must itself be a legal
Azure Key Vault secret name (`^[0-9a-zA-Z-]+$`): an underscore, which the nested layout would have
encoded away, is refused rather than pointing at a name Azure cannot store. The project and profile
name no part of the secret name under flat, so they are not required.

```toml title="secretspec.toml"
[providers]
akv = "akv://myvault?layout=flat"
```

## Use existing secrets

A secret's
Expand Down
21 changes: 21 additions & 0 deletions docs/src/content/docs/providers/awssm.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ awssm://[AWS_PROFILE@]REGION[?prefix=PREFIX][&kms_key_id=KEY][&tag.NAME=VALUE...
- `PREFIX`: Optional root prefix prepended to all secret names. Useful when IAM policies scope access by prefix (e.g., only allow `myteam/*`).
- `kms_key_id`: Optional KMS key (id, ARN, or `alias/...`) used to encrypt secrets that secretspec creates.
- `tag.NAME=VALUE`: Optional tags applied to secrets that secretspec creates. Repeat for multiple tags.
- `?layout=flat` (0.17+): name secrets `[prefix/]{key}` with no `secretspec/{project}/{profile}` scaffolding — see [Layout](#layout-017)

`kms_key_id` and `tag.NAME=VALUE` are applied **only when secretspec creates a
secret** (`CreateSecret`); updating a value (`PutSecretValue`) accepts neither,
Expand Down Expand Up @@ -139,6 +140,26 @@ For example, `DATABASE_URL` in project `myapp` and profile `production` is
stored as `secretspec/myapp/production/DATABASE_URL`. With `?prefix=myteam`,
it becomes `myteam/secretspec/myapp/production/DATABASE_URL`.

### Layout (0.17+)

Added in SecretSpec 0.17; `?layout=flat` is not available in SecretSpec 0.16 or earlier.

`?layout=` is a [general provider setting](/reference/providers/#layout-flat-017), spelled the same
way across every hierarchical backend. The default **nested** layout names each secret
`[prefix/]secretspec/{project}/{profile}/{key}`, as above.

The **flat** layout (`?layout=flat`) drops the `secretspec/{project}/{profile}` scaffolding, so a
convention secret is named `[prefix/]{key}` — `DATABASE_URL`, or `myteam/DATABASE_URL` with
`?prefix=myteam`. Any `?prefix=` still applies, so IAM policies that scope access by prefix keep
working. This is the natural shape for a single-project store, e.g. one migrated from another
manager. Because the project and profile name no part of the secret name under flat, they are not
required.

```toml title="secretspec.toml"
[providers]
awssm = "awssm://us-east-1?layout=flat"
```

## Use existing secrets

A secret's [`ref`](/reference/configuration/#secret-references) field names an
Expand Down
23 changes: 22 additions & 1 deletion docs/src/content/docs/providers/gcsm.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ service account automatically.
### URI format

```
gcsm://PROJECT_ID
gcsm://PROJECT_ID[?layout=flat]
```

- `PROJECT_ID`: Your GCP project ID
- `?layout=flat` (0.17+): use the key alone as the secret id, with no `secretspec-{project}-{profile}-` prefix — see [Layout](#layout-017)

### URI examples

Expand All @@ -81,6 +82,26 @@ Secrets are stored as `secretspec-{project}-{profile}-{key}`. For example,
project `myapp`, profile `production`, and key `DATABASE_URL` map to
`secretspec-myapp-production-DATABASE_URL`.

### Layout (0.17+)

Added in SecretSpec 0.17; `?layout=flat` is not available in SecretSpec 0.16 or earlier.

`?layout=` is a [general provider setting](/reference/providers/#layout-flat-017), spelled the same
way across every hierarchical backend. The default **nested** layout builds the secret id
`secretspec-{project}-{profile}-{key}`, as above.

The **flat** layout (`?layout=flat`) drops that prefix, so a convention secret's id is the `{key}`
itself — `DATABASE_URL` maps straight to a `DATABASE_URL` secret. This is the natural shape for a
single-project store, e.g. one migrated from another manager. The project and profile name no part
of the id under flat, so they are not required; the key must still be a legal GCP secret id
(letters, digits, hyphens and underscores), since there is no `secretspec-` prefix rewriting the
rest of the name.

```toml title="secretspec.toml"
[providers]
gcsm = "gcsm://my-gcp-project?layout=flat"
```

## Use existing secrets

A secret's [`ref`](/reference/configuration/#secret-references) field names an
Expand Down
50 changes: 48 additions & 2 deletions docs/src/content/docs/providers/infisical.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,16 @@ environment variable when it is not declared. Use
### URI format

```
infisical://[host]/{project-id}[?env=slug&path=/prefix&tls=false]
infisical://[host]/{project-id}[?env=slug&path=/prefix&layout=flat&tls=false]
```

- `host`: the Infisical instance (falls back to `INFISICAL_DOMAIN`, then the legacy
`INFISICAL_API_URL`, then `app.infisical.com`)
- `{project-id}`: the project's **UUID**, from Project Settings → Project ID
- `?env=`: environment slug. Without it, the SecretSpec profile names the environment
- `?path=`: folder prefix holding SecretSpec's secrets (default: `/secretspec`)
- `?path=`: folder prefix holding SecretSpec's secrets. Defaults to `/secretspec` under the
nested layout and to the environment root (`/`) under the flat one
- `?layout=` (0.17+): `nested` (default) or `flat` — see [Layout](#layout-017)
- `?tls=false`: disable TLS, for self-hosted instances served over plain HTTP

Infisical's API addresses a project by UUID, not by the slug shown in its UI.
Expand Down Expand Up @@ -173,6 +175,50 @@ renamed.

Folders are created as needed when writing a secret.

### Layout (0.17+)

Added in SecretSpec 0.17; the flat layout is not available in SecretSpec 0.16.

The default **nested** layout stores secrets under `{path}/{project}/{profile}`, so many projects
and profiles can share one Infisical store without colliding.

The **flat** layout (`?layout=flat`) drops the `{project}/{profile}` folders, so a secret sits
directly at the folder prefix — the environment root by default, or `{path}` when one is given:

```
project "myapp", profile "prod", key "DATABASE_URL", layout flat
-> environment prod
folder /
key DATABASE_URL
```

```toml title="secretspec.toml"
[providers]
# Read secrets straight from each environment's root
infisical = "infisical://app.infisical.com/7e2f1a4c-...?layout=flat"

# ...or from a shared prefix, still with no project/profile folders
infisical_team = "infisical://app.infisical.com/7e2f1a4c-...?layout=flat&path=/team"
```

This is the natural shape for a **single-project store** — one migrated from another secret
manager, say — where SecretSpec's namespace folders would only be in the way. Because the flat
layout no longer puts the project or profile in a folder name, those names are unconstrained under
it.

The profile still names the environment, so distinct profiles stay apart:

```
dev profile, key DATABASE_URL -> environment dev, folder /, key DATABASE_URL
prod profile, key DATABASE_URL -> environment prod, folder /, key DATABASE_URL
```

:::caution
Pinning `?env=` **and** `?layout=flat` together collapses every profile onto one environment root,
so they share a key. That combination gives up profile separation deliberately, and is only safe
when a single profile is ever resolved against the store.
:::

## Use existing secrets

A secret can name one Infisical secret by its own coordinates, instead of SecretSpec's layout:
Expand Down
21 changes: 21 additions & 0 deletions docs/src/content/docs/providers/vault.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ openbao://[namespace@]host[:port][/mount][?key=value&...]
- `?role=` (0.17+): Vault role for JWT auth (or `VAULT_JWT_ROLE`)
- `?audience=` (0.17+): OIDC token audience for JWT auth (or `VAULT_JWT_AUDIENCE`)
- `?kv=1`: Use KV v1 engine (default: v2)
- `?layout=flat` (0.17+): address secrets by key alone at the mount root, with no `secretspec/{project}/{profile}` scaffolding — see [Layout](#layout-017)
- `?tls=false`: Disable TLS (for development servers)

### URI examples
Expand Down Expand Up @@ -131,6 +132,26 @@ configured mount, with its value in a field named `value`.
For KV v2, `DATABASE_URL` for project `myapp` and profile `production` is read
from `GET /v1/secret/data/secretspec/myapp/production/DATABASE_URL`.

### Layout (0.17+)

Added in SecretSpec 0.17; `?layout=flat` is not available in SecretSpec 0.16 or earlier.

`?layout=` is a [general provider setting](/reference/providers/#layout-flat-017), spelled the same
way across every hierarchical backend. The default **nested** layout stores each secret at
`secretspec/{project}/{profile}/{key}` under the mount, as above.

The **flat** layout (`?layout=flat`) drops that scaffolding, so a convention secret is the `{key}`
itself at the mount root — for KV v2, `DATABASE_URL` is read from
`GET /v1/secret/data/DATABASE_URL` under the configured mount, still with its value in a `value`
field. This is the natural shape for a single-project store, e.g. one migrated from another manager
where the secrets already live at the mount root. Because the project and profile name no path
segment under flat, they are not required.

```toml title="secretspec.toml"
[providers]
vault = "vault://vault.example.com:8200/secret?layout=flat"
```

## Use existing secrets

A secret's
Expand Down
61 changes: 53 additions & 8 deletions docs/src/content/docs/reference/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,29 +124,31 @@ protonpass://Work/{project}/{profile}/{key} # Custom vault and title temp

## Google Cloud Secret Manager Provider

**URI**: `gcsm://PROJECT_ID` - Stores secrets in Google Cloud Secret Manager
**URI**: `gcsm://PROJECT_ID[?layout=flat]` - Stores secrets in Google Cloud Secret Manager

```bash
gcsm://my-gcp-project # GCP project ID
gcsm://my-gcp-project # GCP project ID
gcsm://my-gcp-project?layout=flat # (0.17+) Secret id is the key alone
```

**Features**: Read/write, cloud sync, profiles, service account support
**Prerequisites**: `gcloud` CLI, authenticated, Secret Manager API enabled, build with `--features gcsm`
**Storage**: Secret name `secretspec-{project}-{profile}-{key}`
**Storage**: Secret name `secretspec-{project}-{profile}-{key}`, or the `{key}` alone under [`?layout=flat`](#layout-flat-017)

## AWS Secrets Manager Provider

**URI**: `awssm://[profile@]REGION` - Stores secrets in AWS Secrets Manager
**URI**: `awssm://[profile@]REGION[?prefix=PREFIX][&layout=flat]` - Stores secrets in AWS Secrets Manager

```bash
awssm://us-east-1 # Specific AWS region
awssm://production@us-east-1 # Specific AWS profile and region
awssm://us-east-1?layout=flat # (0.17+) Secret name is [prefix/]key
awssm:// # SDK default region and credentials
```

**Features**: Read/write, cloud sync, profiles, IAM/SSO authentication
**Prerequisites**: AWS credentials configured, build with `--features awssm`
**Storage**: Secret name `secretspec/{project}/{profile}/{key}`
**Storage**: Secret name `[prefix/]secretspec/{project}/{profile}/{key}`, or `[prefix/]{key}` under [`?layout=flat`](#layout-flat-017)

## Vault / OpenBao Provider

Expand All @@ -170,7 +172,7 @@ vault://127.0.0.1:8200/secret?tls=false # Disable TLS (dev mode)

**Features**: Read/write, KV v1 and v2, namespaces, OpenBao compatible; token and AppRole authentication; JWT/OIDC authentication (0.17+)
**Prerequisites**: Vault/OpenBao server, authentication credentials, build with `--features vault`
**Storage**: KV path `secretspec/{project}/{profile}/{key}` with a `value` field
**Storage**: KV path `secretspec/{project}/{profile}/{key}` with a `value` field, or the `{key}` alone at the mount root under [`?layout=flat`](#layout-flat-017)

## Bitwarden Secrets Manager Provider

Expand Down Expand Up @@ -204,16 +206,17 @@ akv://myvault?suffix=vault.azure.cn # Sovereign cloud (explicit suffix, bar

**Features**: Read/write, cloud sync, profiles, service principal/managed identity/workload identity auth
**Prerequisites**: An Azure Key Vault instance, authenticated via one of the methods above, build with `--features akv`
**Storage**: Secret name `secretspec--{base32(project)}--{base32(profile)}--{base32(key)}` (lowercase, unpadded Base32 preserves case and punctuation distinctions within Azure's case-insensitive secret-name namespace)
**Storage**: Secret name `secretspec--{base32(project)}--{base32(profile)}--{base32(key)}` (lowercase, unpadded Base32 preserves case and punctuation distinctions within Azure's case-insensitive secret-name namespace), or the `{key}` used verbatim as the secret name under [`?layout=flat`](#layout-flat-017)

## Infisical Provider (0.16+)

**URI**: `infisical://[HOST]/PROJECT_ID[?env=SLUG][&path=/PREFIX][&tls=false]` - Stores secrets in Infisical
**URI**: `infisical://[HOST]/PROJECT_ID[?env=SLUG][&path=/PREFIX][&layout=flat][&tls=false]` - Stores secrets in Infisical

```bash
infisical://app.infisical.com/7e2f1a4c-... # Infisical Cloud (US)
infisical://eu.infisical.com/7e2f1a4c-... # Infisical Cloud (EU)
infisical://vault.example.com/7e2f1a4c-...?env=prod # Read every profile from one environment
infisical://app.infisical.com/7e2f1a4c-...?layout=flat # (0.17+) Read from the environment root, no folders
infisical://localhost:8080/7e2f1a4c-...?tls=false # Self-hosted over plain HTTP
```

Expand All @@ -230,9 +233,51 @@ By default the SecretSpec profile names the Infisical environment, so a `product
the `production` environment. Projects whose environments do not correspond to profiles pin one with
`?env=`; the profile still names the folder, so profiles never share a secret.

`?layout=flat` (0.17+) drops the `{project}/{profile}` folders so secrets resolve at the environment
root (or at `?path=` when given) — the natural shape for a single-project store, e.g. one migrated
from another manager. The profile still names the environment, so profiles stay apart; combining
`?layout=flat` with a pinned `?env=` collapses them onto one root and is only safe when a single
profile is resolved against the store.

Values are read with Infisical's secret references expanded, matching its own CLI, so a value of
`postgres://${DB_USER}@host` arrives resolved.

## Layout: `flat` (0.17+)

:::note[Version compatibility]
Added in SecretSpec 0.17. `?layout=flat` is not available in SecretSpec 0.16 or earlier.
:::

`?layout=` is a **general provider setting**, spelled the same way across every provider whose
store has a hierarchy: Infisical, Vault, AWS Secrets Manager, Google Cloud Secret Manager and Azure
Key Vault. It controls how SecretSpec's `{project}/{profile}/{key}` naming convention maps onto the
store:

- **`nested`** (the default) keeps the `secretspec/{project}/{profile}` scaffolding, so many
projects and profiles can share one store without colliding. This is the shape SecretSpec creates.
- **`flat`** drops that scaffolding, so a convention secret is addressed by its **key alone**, at
the store root — under any provider-native container (a Vault mount) or user-supplied prefix (an
AWS `?prefix=`, an Infisical `?path=`), but with no project/profile segments. This is the natural
shape of a **single-project store**, e.g. one migrated from another secret manager where the
secrets already live at the root.

Because the flat layout no longer puts the project or profile in a name, those names are
unconstrained under it. Two caveats follow from addressing by key alone:

- **Profile separation.** Dropping the profile means distinct profiles no longer separate
themselves by name. A provider that pins its environment another way (an Infisical `?env=`) can
still keep profiles apart, but flat plus such a pin collapses every profile onto one key and gives
up profile separation deliberately — only safe when a single profile is ever resolved against the
store.
- **The key must fit the store.** With no `secretspec-` rewriting to fall back on, the key itself
must be a legal name in the backend. Azure Key Vault is the strictest: under flat the key is used
as the secret name verbatim, so it must match `^[0-9a-zA-Z-]+$` (an underscore, which the nested
layout would have Base32-encoded away, is refused).

Providers with no hierarchy (`dotenv`, `env`, `bws`) are already flat and ignore the setting.

An unreadable value (anything but `nested` or `flat`) is refused rather than guessed.

## Provider Selection

### Command Line
Expand Down
Loading