From 7b96267b6b049069c9c1acfe49b188a524360abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bajsarowicz?= Date: Wed, 30 Sep 2026 23:03:23 +0200 Subject: [PATCH 1/2] docs: document pluggable secrets providers (1Password, HashiCorp Vault) --- configuration/secrets.md | 210 +++++++++++++++++++++++++++++++++++++++ index.md | 1 + 2 files changed, 211 insertions(+) create mode 100644 configuration/secrets.md diff --git a/configuration/secrets.md b/configuration/secrets.md new file mode 100644 index 0000000..c76648b --- /dev/null +++ b/configuration/secrets.md @@ -0,0 +1,210 @@ +# Secrets Providers + +Development projects often need a handful of secrets that should not sit in plaintext in the project's `.env` file: a `COMPOSER_AUTH` token, a licence key, or a sandbox API key. Warden can read those values from a secrets manager and inject them into the PHP containers on `warden env up`. + +Secrets injection is opt-in. Set `WARDEN_SECRETS` to a provider name, and that provider is asked for the values; when `WARDEN_SECRETS` is unset, nothing changes and the environment behaves exactly as before. + +## How it works + +```{mermaid} +sequenceDiagram + autonumber + participant Dev as Developer + participant Core as Warden core + participant CLI as Provider CLI (op / vault) + participant Compose as Docker Compose + participant PHP as php-fpm / php-debug + + Dev->>Core: warden env up + Core->>CLI: read secrets + CLI-->>Core: KEY=VALUE lines + Core->>Core: filter reserved names + Core->>Compose: compose up with a names-only override,
values exported to this process only + Compose->>PHP: container environment + Note over Core: override removed on exit +``` + +- Injection and re-reading happen only on `warden env up`, and never for an environment of type `local`. `warden env start` and `warden env restart` reuse the existing containers, and a container keeps the environment it was created with, so the values are still there; they are just not read from the provider again. +- Only the `php-fpm` and `php-debug` containers receive the variables. +- `warden shell` inherits the variables from the running container, so they are available in a shell without extra setup. +- Every `warden env up` reads the secrets again, so a rotated value arrives on the next `warden env up`. + +## Choosing a provider + +| Provider | `WARDEN_SECRETS` value | Required variables | Optional variables | CLI needed | +|----------|------------------------|--------------------|--------------------|------------| +| [1Password](https://developer.1password.com/docs/cli/) | `1password` | `WARDEN_OP_ENVIRONMENT_ID` or a `.env.op` file | `WARDEN_OP_ACCOUNT` | `op` (2.33.0-beta.02 or later for Environments) | +| [HashiCorp Vault](https://developer.hashicorp.com/vault) | `vault` | `WARDEN_VAULT_PATH` | `WARDEN_VAULT_MOUNT` (default `secret`), `WARDEN_VAULT_ADDR`, `WARDEN_VAULT_NAMESPACE` | `vault` and `jq` | + +## 1Password + +The 1Password provider has two modes. Pick one per project: a 1Password Environment, or secret references listed in a `.env.op` file. + +```{mermaid} +flowchart TD + B{"WARDEN_OP_ENVIRONMENT_ID set?"} -->|yes| C{".env.op present?"} + C -->|yes| D["warning: use one of them"] + C -->|no| E["Environments mode"] + B -->|no| F{".env.op present?"} + F -->|yes| G["Items mode"] + F -->|no| H["warning: set one"] +``` + +| Mode | Configured by | 1Password side | CLI call | Best for | +|------|---------------|----------------|----------|----------| +| Environments | `WARDEN_OP_ENVIRONMENT_ID` | A variable set managed in 1Password | `op environment read` | A variable set a team manages in one place | +| Items | a `.env.op` file | Individual fields of existing vault items | `op read` | Picking specific fields from existing items, with the references reviewed in git | + +### Environments + +Add the following to the project's `.env` file: + +```bash +WARDEN_SECRETS=1password +WARDEN_OP_ENVIRONMENT_ID= +``` + +Warden does not read a 1Password credential from the project `.env`. The `op` CLI authenticates on its own, using the signed-in desktop app integration or a session token from `op signin`. On a host signed in to more than one 1Password account, set `WARDEN_OP_ACCOUNT` to the account to read from: + +```bash +WARDEN_OP_ACCOUNT= +``` + +1Password Environments require 1Password CLI 2.33.0-beta.02 or later. See the [1Password Environments documentation](https://developer.1password.com/docs/environments/) for creating an environment and adding variables to it. + +### Items (secret references) + +Add the following to the project's `.env` file: + +```bash +WARDEN_SECRETS=1password +``` + +and a `.env.op` file in the project root next to `.env` with the references: + +``` +COMPOSER_AUTH=op://project-x/composer/composer-auth +NEW_RELIC_KEY="op://project-x/New Relic/license key" +``` + +- The `.env.op` file holds references only, so it is meant to be committed. +- Each reference is resolved with `op read` on `warden env up`, and the field's content becomes the variable's value. See the [secret reference syntax](https://developer.1password.com/docs/cli/secret-reference-syntax/). +- If one reference cannot be read, the whole injection is skipped. +- Lines that are not `KEY=op://…` are ignored and reported by line number. +- Values with line breaks are skipped with a warning, so a multi-line field such as a private key cannot be injected this way. +- `WARDEN_OP_ACCOUNT` applies to every reference. +- `WARDEN_OP_ENVIRONMENT_ID` must be unset to use this mode. + +## HashiCorp Vault + +Add the following to the project's `.env` file: + +```bash +WARDEN_SECRETS=vault +WARDEN_VAULT_PATH=myproject/development +``` + +The remaining settings are optional: + +```bash +WARDEN_VAULT_MOUNT=secret +WARDEN_VAULT_ADDR=https://vault.example.com:8200 +WARDEN_VAULT_NAMESPACE=admin +``` + +- `WARDEN_VAULT_MOUNT` defaults to `secret`. +- `WARDEN_VAULT_ADDR` and `WARDEN_VAULT_NAMESPACE` default to whatever the Vault CLI is already configured with. +- The secret path is relative to the mount, may not contain `..`, and may not start with `/`. + +Warden does not read a Vault credential from the project `.env`. The `vault` CLI authenticates on its own, using the host's `VAULT_TOKEN` or the token helper configured after `vault login`. + +Both KV v1 and KV v2 secrets engines are supported. Warden reads the pairs from `.data.data` for KV v2 and from `.data` for KV v1. + +Values of type object, array or null, and strings that contain a line break, cannot be injected. They are skipped and the skipped names are listed in a warning. Keys that are not valid shell identifiers (letters, digits and underscores, not starting with a digit) are dropped silently, because they cannot be used as environment variable names. + +The `jq` command is required alongside the `vault` CLI. + +## Reserved variables + +Warden never injects the variables it manages itself, because an external source must not change how the stack is built: + +- `CHOWN_DIR_LIST` +- `COMPOSER_MEMORY_LIMIT` +- `COMPOSER_VERSION` +- `HISTFILE` +- `NODE_VERSION` +- `SSH_AUTH_SOCK` +- `SSH_AUTH_SOCK_PATH_ENV` +- `XDEBUG_VERSION` + +Any key under a `WARDEN_`, `TRAEFIK_` or `PHP_` prefix is also skipped. When a provider returns a reserved name, Warden warns `Variables reserved by Warden were not injected: ` and starts the environment without it. + +## Security notes + +:::{important} +- Warden writes no secret values to disk. The temporary compose override holds variable names only, and it is removed when `warden env up` exits. +- The values are exported only to the `docker compose` process that starts the environment. Once the containers run, the values live in the container environment and are visible to anyone who can run `docker inspect` on the host, exactly like any other compose `environment:` entry, so treat this as central storage and rotation of development secrets, not as container isolation. +- Provider output is parsed, never evaluated, so a value cannot execute a command on the host. +- The secrets manager credentials themselves never come from the project `.env`; the provider CLI authenticates on its own. +- Keep development secrets separate from production ones. +::: + +## Troubleshooting + +Every failure below is a warning. `warden env up` still starts the environment; it just starts without the secrets. + +| Warning | Cause | Fix | +|---------|-------|-----| +| `1Password CLI (op) could not be found; …` / `Vault CLI (vault) could not be found; …` / `jq could not be found; …` | The provider CLI, or `jq` for Vault, is not on the `PATH`. | Install the missing tool and re-run `warden env up`. | +| `Unknown secrets provider '' in WARDEN_SECRETS. Available providers: …; skipping secret injection.` | `WARDEN_SECRETS` names a provider that has no `utils/secrets/.sh` file. | Use one of the listed provider names. | +| `WARDEN_SECRETS must be a provider name (lowercase letters, digits, dashes). …` | `WARDEN_SECRETS` contains characters outside lowercase letters, digits and dashes. | Correct the value. | +| `WARDEN_OP_ENVIRONMENT_ID is not a valid 1Password Environment ID; …` / `WARDEN_VAULT_PATH is not a valid Vault secret path; …` | The ID or path fails the provider's validation. The same pattern applies to `WARDEN_VAULT_MOUNT`, `WARDEN_VAULT_NAMESPACE` and `WARDEN_VAULT_ADDR`. | Fix the value in the project `.env`. | +| `Both WARDEN_OP_ENVIRONMENT_ID and .env.op are configured; use one of them. Skipping secret injection.` | A 1Password Environment ID and a `.env.op` file are both present. | Keep one mode and remove the other. | +| `Set WARDEN_OP_ENVIRONMENT_ID or add a .env.op file with secret references; skipping secret injection.` | Neither 1Password mode is configured. | Set the environment ID, or add `.env.op`. | +| `Could not read 1Password Environment '' (Environments require 1Password CLI 2.33.0-beta.02 or later); …` / `Could not read 1Password secret reference for ; …` / `Could not read Vault secret '/'; …` | The provider CLI is not signed in, lacks permission, or cannot reach the service. | Sign in with the provider CLI, check access, then re-run. | +| `Ignored .env.op lines that are not KEY=op://… references: ` | `.env.op` contains lines that are not `KEY=op://…` references. | Fix or remove those lines; they are not injected. | +| `1Password values with line breaks cannot be injected and were skipped: ` | A referenced field's value contains a line break. | Store a single-line value in that field, or inject it another way. | +| `Variables reserved by Warden were not injected: ` | The provider returns one or more names Warden manages itself. | Rename the variable in the secrets manager. | +| `Secrets provider '' returned no variables; …` / `Secrets provider '' defines only variables reserved by Warden; …` | The provider returned an empty result, or only reserved names. | Check the environment ID or secret path, and that it holds injectable variables. | + +## Writing a provider + +A provider is a shell file at `utils/secrets/.sh` that implements the two functions from the core contract: + +```{mermaid} +flowchart LR + A["core sources utils/secrets/PROVIDER.sh"] --> B["secretsProviderRequireConfig
validate configuration"] + B --> C["secretsProviderRead
fetch secrets"] + C --> D["KEY=VALUE lines on stdout"] + D --> E["core filters and injects"] +``` + +A minimal provider looks like this: + +```bash +#!/usr/bin/env bash +[[ ! ${WARDEN_DIR} ]] && >&2 echo -e "\033[31mThis script is not intended to be run directly!\033[0m" && exit 1 + +function secretsProviderRequireConfig { + if [[ ! "${WARDEN_EXAMPLE_PATH:-}" =~ ^[A-Za-z0-9_./-]+$ ]]; then + warning "WARDEN_EXAMPLE_PATH is not a valid path; skipping secret injection." + return 1 + fi +} + +function secretsProviderRead { + if ! command -v example >/dev/null 2>&1; then + warning "Example CLI (example) could not be found; skipping secret injection." + return 1 + fi + + example read "${WARDEN_EXAMPLE_PATH}" +} +``` + +Rules for a provider: + +- Prefix the provider's own settings with `WARDEN__` so they cannot collide with Warden's own variables. +- Never read the secrets manager credentials from the project `.env`; let the provider CLI authenticate on its own. +- On failure, print a warning that names the problem and return non-zero. The core then skips injection and the environment starts without secrets. +- Never print secret values to stderr or write them to disk; the only output that carries values is the `KEY=VALUE` lines on stdout. diff --git a/index.md b/index.md index a43ba4f..7ea5ee8 100644 --- a/index.md +++ b/index.md @@ -10,6 +10,7 @@ Under the hood `docker-compose` is used to control everything which Warden runs * Dnsmasq to serve DNS responses for `.test` domains eliminating manual editing of `/etc/hosts` * An SSH tunnel for connecting from Sequel Pro or TablePlus into any one of multiple running database containers. * Warden issued wildcard SSL certificates for running https on all local development domains. +* Optional injection of per-project secrets from a secrets manager such as 1Password or HashiCorp Vault. * Full support for Magento 1, Magento 2, Laravel, Symfony 4, Shopware 6 on both macOS and Linux. * Ability to override, extend, or setup completely custom environment definitions on a per-project basis. From 677e4b8f89b7febd24338d6f36771e54bfd1506a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bajsarowicz?= Date: Wed, 30 Sep 2026 23:21:21 +0200 Subject: [PATCH 2/2] docs: cover host-reserved names, extra PHP services and 1Password masking --- configuration/secrets.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/configuration/secrets.md b/configuration/secrets.md index c76648b..cc8669e 100644 --- a/configuration/secrets.md +++ b/configuration/secrets.md @@ -13,7 +13,7 @@ sequenceDiagram participant Core as Warden core participant CLI as Provider CLI (op / vault) participant Compose as Docker Compose - participant PHP as php-fpm / php-debug + participant PHP as PHP containers Dev->>Core: warden env up Core->>CLI: read secrets @@ -25,9 +25,10 @@ sequenceDiagram ``` - Injection and re-reading happen only on `warden env up`, and never for an environment of type `local`. `warden env start` and `warden env restart` reuse the existing containers, and a container keeps the environment it was created with, so the values are still there; they are just not read from the provider again. -- Only the `php-fpm` and `php-debug` containers receive the variables. +- The `php-fpm` and `php-debug` containers always receive the variables. When `WARDEN_PHP_SPX`, `WARDEN_BLACKFIRE` or `WARDEN_MAGENTO2_GRAPHQL_SERVER` is enabled, the matching `php-spx`, `php-blackfire` or `php-graphql` container receives them too. - `warden shell` inherits the variables from the running container, so they are available in a shell without extra setup. - Every `warden env up` reads the secrets again, so a rotated value arrives on the next `warden env up`. +- The values are exported into the `docker compose` process, so they are also available for `${VAR}` interpolation in the compose files, including the project's `.warden/warden-env.yml`, on the first and every later `warden env up`. ## Choosing a provider @@ -52,7 +53,7 @@ flowchart TD | Mode | Configured by | 1Password side | CLI call | Best for | |------|---------------|----------------|----------|----------| -| Environments | `WARDEN_OP_ENVIRONMENT_ID` | A variable set managed in 1Password | `op environment read` | A variable set a team manages in one place | +| Environments | `WARDEN_OP_ENVIRONMENT_ID` | A variable set managed in 1Password | `op environment read --no-masking` | A variable set a team manages in one place | | Items | a `.env.op` file | Individual fields of existing vault items | `op read` | Picking specific fields from existing items, with the references reviewed in git | ### Environments @@ -64,6 +65,8 @@ WARDEN_SECRETS=1password WARDEN_OP_ENVIRONMENT_ID= ``` +Values are injected as 1Password returns them. + Warden does not read a 1Password credential from the project `.env`. The `op` CLI authenticates on its own, using the signed-in desktop app integration or a session token from `op signin`. On a host signed in to more than one 1Password account, set `WARDEN_OP_ACCOUNT` to the account to read from: ```bash @@ -126,24 +129,20 @@ The `jq` command is required alongside the `vault` CLI. ## Reserved variables -Warden never injects the variables it manages itself, because an external source must not change how the stack is built: +Warden refuses to inject some names, so a secrets source cannot change how the stack is built. There are two groups. + +Managed by Warden: the variables Warden uses to build the stack: `CHOWN_DIR_LIST`, `COMPOSER_MEMORY_LIMIT`, `COMPOSER_VERSION`, `HISTFILE`, `NODE_VERSION`, `SSH_AUTH_SOCK`, `SSH_AUTH_SOCK_PATH_ENV`, `XDEBUG_VERSION`, plus every name under the `WARDEN_`, `TRAEFIK_` or `PHP_` prefixes. -- `CHOWN_DIR_LIST` -- `COMPOSER_MEMORY_LIMIT` -- `COMPOSER_VERSION` -- `HISTFILE` -- `NODE_VERSION` -- `SSH_AUTH_SOCK` -- `SSH_AUTH_SOCK_PATH_ENV` -- `XDEBUG_VERSION` +Would change the host process that runs `docker compose`: `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL`, `PWD`, `OLDPWD`, `IFS`, `TMPDIR`, `ENV`, `BASH_ENV`, `SHELLOPTS`, `BASHOPTS`, `PS4`, `CDPATH`, `GLOBIGNORE`, `TERM`, `LANG`, plus every name under the `DOCKER_`, `COMPOSE_`, `BUILDKIT_`, `BUILDX_`, `LD_`, `DYLD_`, `BASH_FUNC_`, `LC_`, `OP_`, `VAULT_` or `MUTAGEN_` prefixes. -Any key under a `WARDEN_`, `TRAEFIK_` or `PHP_` prefix is also skipped. When a provider returns a reserved name, Warden warns `Variables reserved by Warden were not injected: ` and starts the environment without it. +When a provider returns a reserved name, Warden warns `Variables reserved by Warden were not injected: ` and starts the environment without it. ## Security notes :::{important} - Warden writes no secret values to disk. The temporary compose override holds variable names only, and it is removed when `warden env up` exits. - The values are exported only to the `docker compose` process that starts the environment. Once the containers run, the values live in the container environment and are visible to anyone who can run `docker inspect` on the host, exactly like any other compose `environment:` entry, so treat this as central storage and rotation of development secrets, not as container isolation. +- Names that would steer that `docker compose` process itself, such as `PATH` and the Docker client and loader variables, are refused outright. - Provider output is parsed, never evaluated, so a value cannot execute a command on the host. - The secrets manager credentials themselves never come from the project `.env`; the provider CLI authenticates on its own. - Keep development secrets separate from production ones. @@ -162,9 +161,10 @@ Every failure below is a warning. `warden env up` still starts the environment; | `Both WARDEN_OP_ENVIRONMENT_ID and .env.op are configured; use one of them. Skipping secret injection.` | A 1Password Environment ID and a `.env.op` file are both present. | Keep one mode and remove the other. | | `Set WARDEN_OP_ENVIRONMENT_ID or add a .env.op file with secret references; skipping secret injection.` | Neither 1Password mode is configured. | Set the environment ID, or add `.env.op`. | | `Could not read 1Password Environment '' (Environments require 1Password CLI 2.33.0-beta.02 or later); …` / `Could not read 1Password secret reference for ; …` / `Could not read Vault secret '/'; …` | The provider CLI is not signed in, lacks permission, or cannot reach the service. | Sign in with the provider CLI, check access, then re-run. | +| `1Password returned masked values; skipping secret injection.` | The `op` CLI ignored `--no-masking` and returned concealed values, for example an older beta. | Update the 1Password CLI to 2.33.0-beta.02 or later. | | `Ignored .env.op lines that are not KEY=op://… references: ` | `.env.op` contains lines that are not `KEY=op://…` references. | Fix or remove those lines; they are not injected. | | `1Password values with line breaks cannot be injected and were skipped: ` | A referenced field's value contains a line break. | Store a single-line value in that field, or inject it another way. | -| `Variables reserved by Warden were not injected: ` | The provider returns one or more names Warden manages itself. | Rename the variable in the secrets manager. | +| `Variables reserved by Warden were not injected: ` | The provider returns one or more names Warden reserves, because they build the stack or steer the `docker compose` process. | Rename the variable in the secrets manager. | | `Secrets provider '' returned no variables; …` / `Secrets provider '' defines only variables reserved by Warden; …` | The provider returned an empty result, or only reserved names. | Check the environment ID or secret path, and that it holds injectable variables. | ## Writing a provider