YourBOX Secret Management
Purpose
Section titled “Purpose”ADR-0008 commits YourBOX to a single env contract regardless of environment. This page documents the keys, how each environment populates them, and how the application boot sequence validates them.
Env Contract
Section titled “Env Contract”YourBOX requires the following environment variables at boot. Missing a required key crashes the Symfony kernel loudly.
| Key | Required | Carried by | Example |
|---|---|---|---|
APP_ENV |
yes | All environments | dev, vps, staging, prod |
APP_DEBUG |
yes | All environments | 1 only when APP_ENV=dev |
APP_SECRET |
yes | All environments | 32+ random bytes hex |
DB_DSN |
yes | All environments | postgresql://user:pass@host:5432/yourbox |
STORAGE_DSN |
yes | All environments | s3://key:secret@minio.local/yourbox?region=... |
MEILISEARCH_URL |
yes | All environments | https://meili.yourbox.site |
MEILISEARCH_KEY |
yes | All environments | API key |
OIDC_ISSUER |
prod, staging | Authentik tenant URL | https://auth.yourbox.site/application/o/yourbox/ |
OIDC_CLIENT_ID |
prod, staging | Authentik client | yourbox |
OIDC_CLIENT_SECRET |
prod, staging | Authentik client | secret |
SMTP_DSN |
yes | All environments | smtps://user:pass@smtp.example:465 |
SMTP_FROM |
yes | All environments | admin@yourbox.site |
The keys are documented in dev.env at the repository root (committed,
placeholder values). The same file is both the development env and the env
contract template; no separate .env.example exists. New keys land in
dev.env first, then in Env::REQUIRED if non optional, then in the OpenBao
KV path.
The Env helper (YourBOX\Bootstrap\Env) exposes a small typed API and a boot check:
namespace YourBOX\Bootstrap;
final class Env{ // Keys required at boot by plan 002. Additional keys (STORAGE_DSN, // MEILISEARCH_*, OIDC_*) are added as later plans deliver those features. public const REQUIRED = [ 'APP_ENV', 'APP_DEBUG', 'APP_SECRET', 'DB_DSN', 'SMTP_DSN', 'SMTP_FROM', ];
public static function require(string $key): string; public static function get(string $key, string $default = ''): string; public static function bool(string $key, bool $default = false): bool; public static function dbDsn(): array; public static function smtpDsn(): array; public static function bootCheck(): void;}The application calls Env::bootCheck() at startup. It crashes before handling its
first request when a required key is missing. The invocation site moves to the Symfony
kernel in plan 003.
Local Development And CI
Section titled “Local Development And CI”The .sops.yaml file at the repository root carries one creation_rules
entry with a single recipient: the maintainer’s age public key.
creation_rules: - path_regex: 'prod\.env$' age: # admins (full rights, can rotate production secrets and add recipients) - age1...There is no dev / reader role. The original two-tier model was collapsed
because no second recipient existed and dev.env.enc had no audience. The
boundary between local and production is the file separation: dev.env is
committed plaintext (placeholders), prod.env.enc is encrypted and the
plaintext prod.env is gitignored.
CODEOWNERS on .sops.yaml gates admin-list changes; existing admins approve
new admin keys.
First time on the repository (dev onboarding)
Section titled “First time on the repository (dev onboarding)”A new contributor runs:
git clone <repo>cd yourboxmise trust && mise installThe committed dev.env already loads through mise’s _.file = "dev.env" and
the Symfony kernel boots. Editing dev.env is allowed. Values stay on the
workstation. No age key is required to start writing code.
Joining the admin list (production access)
Section titled “Joining the admin list (production access)”When a contributor needs to access production secrets, they run:
mise run age-bootstrapThe script generates an age key under ~/.config/sops/age/keys.txt if
absent, then appends the public key to .sops.yaml. The resulting PR
requires existing-admin approval. After merge, an existing admin runs:
mise run secrets-rekeygit add . && git commit -m 'chore(sops): rekey for updated admin list'The rekey re-encrypts prod.env.enc with the new recipient list. The new
admin pulls and can decrypt prod.env.enc via SOPS.
Fresh clone bootstrap
Section titled “Fresh clone bootstrap”dev.env ships with the repo, so the post install hook does no decryption.
Production secrets are never auto-decrypted on a workstation; admins
explicitly run sops --decrypt prod.env.enc > prod.env when needed and
delete the cleartext file when done. A pre commit hook with gitleaks rejects
accidental commits of credentials. The pre commit configuration is shared
with every other app in the workspace.
CI test stage runs against ephemeral PostgreSQL and MinIO containers and
mints disposable test credentials inline. CI deploy stages decrypt
prod.env.enc using an age private key delivered via OpenBao.
VPS Smoke Test
Section titled “VPS Smoke Test”The Ansible playbook for YourBOX on the VPS copies the same prod.env.enc from
the repository, decrypts it on the VPS using an age private key stored on a tmpfs at
boot, then keeps the cleartext file mode at 0400 owned by the application user.
The compose stack reads it through env_file: prod.env.
The age private key on the VPS never lands on disk. It is loaded into tmpfs at first boot from a Cloudflare R2 path protected by a short lived presigned URL, the key file is read into the SOPS keyring environment and the tmpfs mount discards on every reboot.
Azure Deployment
Section titled “Azure Deployment”Terraform builds DB_DSN and SMTP_DSN from individual Terraform variables using
format() and urlencode(), then injects the full env contract into the App Service
via the azurerm_linux_web_app.app_settings block. The Azure App Service surfaces
these settings as environment variables in the container; the application reads them
through YourBOX\Bootstrap\Env. No secrets file lands on disk or in the container image.
app_settings = { "APP_ENV" = "prod" "APP_DEBUG" = "0" "APP_SECRET" = var.APP_SECRET "DB_DSN" = local.db_dsn # mysql://user:urlencode(pass)@fqdn:3306/db "SMTP_DSN" = local.smtp_dsn # smtps://user:urlencode(pass)@host:465 "SMTP_FROM" = var.SMTP_FROM}The Terraform variables (APP_SECRET, DB_SQL_PASSWORD, SMTP_PASSWORD, etc.) are
supplied by GitLab CI variables scoped to the production environment. Rotating a secret
is a CI variable update followed by a terraform apply.
OpenBao replacing the Terraform-variable source is future work. When that swap happens
the application contract (DB_DSN, SMTP_DSN, etc.) stays identical; only the
Terraform locals that build the DSN strings change.
Homelab Kubernetes
Section titled “Homelab Kubernetes”The External Secrets Operator reads from OpenBao and writes a Kubernetes Secret. The
YourBOX Deployment projects the secret as environment variables. The mapping lives in
the Helm chart under k8s/charts/yourbox/templates/externalsecret.yaml:
apiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata: name: yourbox-env namespace: yourboxspec: refreshInterval: 5m secretStoreRef: name: openbao-prod kind: ClusterSecretStore target: name: yourbox-env template: type: Opaque dataFrom: - extract: key: yourbox/prodThe pod template references the secret through envFrom:
envFrom: - secretRef: name: yourbox-envRotating a secret in production is a single OpenBao write. The External Secrets controller refreshes the Kubernetes Secret within five minutes, and a rolling restart of the YourBOX Deployment picks up the new value.
Onboarding A New Secret
Section titled “Onboarding A New Secret”The checklist below applies whenever the application reads a new variable.
- Add the variable to
dev.envwith a placeholder value and a comment. - If the variable is required at boot, add it to
Env::REQUIRED. - Wire the variable in
app/config/services.yamlor in the relevant Symfony configuration file. - Write the value to OpenBao under
kv/yourbox/stagingandkv/yourbox/prod. - Update
prod.env.encwith the new key via SOPS edit. Runmise run secrets-rekeyif the age recipient list also changed. - Update this page with a row in the env contract table.
Things That Are Out Of Scope
Section titled “Things That Are Out Of Scope”- The application never reads files at runtime to obtain secrets. Files are an injection vehicle, not a storage location.
- The container image carries no secret values. Trivy scans and image inspection cannot leak credentials.
- Authentik client secrets, MinIO root credentials, OpenBao unseal keys and any
other platform secret live in OpenBao only. The application path
(
kv/yourbox/*) is read only and scoped to YourBOX.