Skip to content

feat(secrets): env-var resolution for *_file paths and string fields#34

Merged
albedosehen merged 1 commit into
mainfrom
feat/env-var-secrets
May 9, 2026
Merged

feat(secrets): env-var resolution for *_file paths and string fields#34
albedosehen merged 1 commit into
mainfrom
feat/env-var-secrets

Conversation

@albedosehen
Copy link
Copy Markdown
Contributor

Operators driving rota.yaml from a secret manager (Doppler, Vault agent, systemd LoadCredentialEncrypted=, plain compose env_file:) no longer have to pre-stage plaintext files on disk.

Two patterns

*_file: paths accept the sentinel env:NAME. When set, rota reads the named env var as the secret value instead of opening a file. Existing real paths keep working unchanged.

namecheap:
  api_key_file: env:NAMECHEAP_API_KEY

Applied to:

  • namecheap.api_key_file
  • cloudflare.api_token_file
  • email password_file
  • webhook bearer_token_file
  • ACME hmac_key_file
  • SurrealDB audit password_file

NOT applied to ACME account_credentials_file because that's a read-write persistent state file, not a secret reference.

String fields get ${VAR} interpolation at config-load time. Multiple refs per string are supported.

namecheap:
  username: ${NAMECHEAP_USERNAME}
  client_ip: ${NAMECHEAP_CLIENT_IP}

Applied via RotaConfig::resolve_env to all operator-set strings: namecheap (username, api_user, client_ip), acme (directory_url, contact_email), surrealdb audit (endpoint, namespace, database, username), alert email (smtp_host, username, from, to[]), alert webhook (url).

Failure mode

Unset referenced variable -> Error::ConfigInvalid at RotaConfig::load. Misconfigured deploys fail loud at boot rather than silently calling Namecheap with an empty key.

Implementation

Lives alongside existing redaction in rota_core::secrets:

  • read_secret(path) (sync, used at startup; file reads here are <1KB)
  • expand_env(s) (${VAR} interpolation)

RotaConfig::load calls resolve_env after serde_yaml parse so backends see fully-resolved values.

Tests

8 new unit tests in rota-core: file read with trim, env-prefix dispatch, missing-env error, ${} interpolation (single + multiple), passthrough, unterminated-brace error. Total: rota-core 40 / rota-daemon 106 / rota-cli 5, all passing.

Docs

New "Secrets and environment variables" section in book/src/configuration.md showing Doppler, systemd, Vault Agent, and plain compose patterns. rota.example.yaml gains a usage block + alternate namecheap example.

CI verified locally

  • cargo fmt --all --check clean
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo test --workspace --locked 151 tests pass

Operators driving rota.yaml from a secret manager (Doppler, Vault
agent, systemd LoadCredentialEncrypted=, plain compose env_file)
no longer have to pre-stage plaintext files on disk. Two patterns:

* Every `*_file:` field accepts the path-prefix sentinel `env:NAME`.
  When set, rota reads the named env var as the secret value instead
  of opening a file. Existing real paths keep working unchanged.
  Applied to namecheap.api_key_file, cloudflare.api_token_file,
  email password_file, webhook bearer_token_file, ACME hmac_key_file,
  surrealdb audit password_file. Skipped for ACME
  account_credentials_file because that is a read-write persistent
  state file, not a secret reference.

* Every operator-set String field gets `${VAR}` interpolation
  against the process environment at config-load time. Multiple
  refs per string are supported. Applied via RotaConfig::resolve_env
  to namecheap.username, namecheap.api_user, namecheap.client_ip,
  acme.directory_url, acme.contact_email, surrealdb audit
  endpoint/namespace/database/username, alert email
  smtp_host/username/from/to[], alert webhook url.

An unset referenced variable returns Error::ConfigInvalid at startup
rather than silently letting a downstream API call go out with an
empty value.

Implementation lives alongside existing redaction in rota_core::secrets:
read_secret(path) and expand_env(s). RotaConfig::load calls
resolve_env after serde parse so consumers see fully-resolved values.

Tests: 8 new unit tests in rota-core covering file read with trim,
env-prefix dispatch, missing-env error, ${} interpolation single +
multiple, passthrough, unterminated-brace error.

Docs: new "Secrets and environment variables" section in
book/src/configuration.md showing the Doppler, systemd, Vault, and
plain compose patterns. rota.example.yaml header gains a usage
block plus an alternate namecheap example.
@albedosehen albedosehen merged commit df2048f into main May 9, 2026
1 check passed
@albedosehen albedosehen deleted the feat/env-var-secrets branch May 9, 2026 12:55
albedosehen added a commit that referenced this pull request May 9, 2026
The docs workflow had been silently failing at the
actions/configure-pages@v5 step on every push to main since PR #31
introduced it (PRs #31, #32, #34 all red on this leg) because Pages
was never enabled at the repo level. The site at
oneiriq.github.io/rota/ was returning HTTP 404 the whole time despite
the docs PRs claiming it was shipping.

Manually POSTed to /repos/Oneiriq/rota/pages with build_type=workflow
to unblock the current state, and run 25601961989 confirms the
workflow now builds and deploys end-to-end (oneiriq.github.io/rota/
returns 200 with the mdBook content).

Adding enablement: true here as the durable fix so a future fork or a
repo where Pages gets disabled doesn't silently regress to the same
hidden 404 state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant