Secret management
How to keep passwords, tokens, and keys outside the job spec, with encryption at rest and versioned rotation.
Database password, API key, OAuth token, provider credential. All of these are secrets and none of them belong in the job spec.
For two reasons. First, specs are usually versioned in some repository. Anyone with read access to the repo gets access to the secret. Second, specs travel through logs, shared terminals, review screenshots. Each of those points is a leak waiting to happen.
HeroCtl ships with an integrated secret vault. You don't wire anything in from the outside.
Creating a secret
From the CLI, three forms:
# valor literal direto na linha de comando
heroctl secret create db_password --from-literal="s3nh4-l0nga-e-aleatoria"
# de um arquivo
heroctl secret create tls_key --from-file=./privkey.pem
# stdin (não fica no histórico do shell)
echo -n "valor-secreto" | heroctl secret create api_token --from-stdin
Through the web panel there is an equivalent form.
Listing and inspecting
# lista todos os segredos visíveis pelo seu token
heroctl secret list
# mostra metadados (versão, autor, data de criação) — nunca o valor
heroctl secret describe db_password
# mostra o valor (exige permissão explícita)
heroctl secret get db_password
heroctl secret get is the most sensitive operation in the system. It shows up in the audit log with the name of who read it, when, and from which IP.
Warning: In production, restrict
secret:readto the minimum number of roles. For most teams, no one needs to read values manually — the cluster injects them straight into the job.
Encryption at rest
Each secret is encrypted with AES-256 before being persisted. The cluster's encryption key is protected and is never stored alongside the encrypted data.
Operationally this means a backup of cluster state, without the key, is useless to an attacker. You can carry a snapshot on an external hard drive without worry.
The key itself is generated at cluster initialization and lives in a file protected by filesystem permissions (0600, owner root) on each server node. To rotate it:
heroctl cluster rekey
This command re-encrypts every secret with a new key and invalidates the old one. Do it during a maintenance window — the operation can take minutes on clusters with thousands of secrets.
Injecting into the job
Injection happens the moment the allocation starts. The decrypted value exists only in the process memory, never on disk.
job: api-pagamentos
env:
DATABASE_URL: ${secret.database_url}
STRIPE_KEY: ${secret.stripe_secret}
REDIS_PASSWORD: ${secret.redis_password}
The process inside the container sees the variables like any other. No application code change required.
To inject as a file (useful for TLS keys, certificates, service account JSON):
job: webhook-handler
files:
- path: /etc/app/credentials.json
content: ${secret.gcp_service_account}
mode: "0400"
The file is created on tmpfs (it never touches disk), with the indicated permission, before the main process starts.
Versioning and rotation
Every update to a secret creates a new version. The old version stays available for a configurable time window (default 7 days) for rollback cases.
# atualiza, mantendo histórico
heroctl secret update db_password --from-literal="senha-nova"
# lista versões
heroctl secret history db_password
# faz rollback para versão anterior
heroctl secret rollback db_password --to-version 3
By default, running jobs keep using the version that was active when they started. To force a re-read, redeploy the job:
heroctl job redeploy api-pagamentos
For a job that always needs the latest version without redeploy, declare:
env:
DATABASE_URL: ${secret.database_url:latest}
In this mode, the cluster automatically restarts the allocation when the secret changes.
Per-role permissions (Business+)
In the Business plan, you get fine-grained control over who can do what with which secret. The model works with hierarchical scopes:
policy "deploy-prod-secrets":
secret:
path: "prod/*"
capabilities: ["read", "list"]
secret:
path: "prod/admin/*"
capabilities: [] # nem listar
Attach the policy to a token or a role and you're done. See the RBAC document for the complete model.
Audit (Business+)
Every access is recorded:
heroctl audit secret --name db_password --since 30d
Tabular output with timestamp, user, action (read, update, delete), source (IP), and result. Export for external analysis:
heroctl audit secret --since 30d --format json > audit.jsonl
Configure alerts for anomalous patterns: reads outside business hours, the same secret read by different users in a short window, repeated permission failures.
Encrypted backup
Secret backups come out encrypted, ready to ship to S3-compatible storage:
# config do cluster
backup:
secrets:
enabled: true
schedule: "0 3 * * *" # 3h da manhã todo dia
destination:
type: s3
bucket: heroctl-backups
region: us-east-1
prefix: secrets/
credentials: ${secret.backup_s3_creds}
retention_days: 90
The snapshot is encrypted with the cluster key before being shipped. The bucket can be reachable for other purposes without risk — anyone with read access cannot decrypt anything.
To restore on a new cluster, you need the snapshot and the original cluster's key. Keep the key separate from the backup storage.
External vaults
For teams with compliance requirements that mandate use of an existing corporate vault, there is an optional integration:
secret_backend:
type: aws_kms
region: us-east-1
key_id: alias/heroctl-prod
Supported out of the box: AWS KMS, GCP KMS, HashiCorp Vault. In this mode, HeroCtl delegates encryption operations to the external vault. Secrets remain accessible through the same ${secret.name} syntax.
Use this if you already have the vault running and audited. For most teams, the native backend is enough and simpler.
Best practices
In order of importance:
- No hardcoding in the spec. Not in a commit, not temporarily, not "just for testing". The habit leaks.
- Separate dev / staging / prod. Use prefixes in the name (
dev/db_password,prod/db_password) or separate clusters. Never share production credentials with a development environment. - Periodic rotation. Minimum every 90 days for database and provider credentials. More frequently for API tokens that leak easily.
- Least privilege on reads. Almost no one needs
secret:read. Automatic injection into the job does not require the person who deployed it to have access to the value. - Monthly audit review. Look at who read what. Anomalies show up if you look.
- Don't log secrets. Configure the application to mask sensitive variables in logs. The cluster encrypts at rest, but if your app prints
print(os.environ)on startup, it's over.
Next steps
- Configure RBAC and tokens to limit who accesses what.
- Review metrics and logs to detect anomalous use.