Secrets — store, issue, rotate, and encrypt the credentials machines use
What it is
A secret is any sensitive value software needs but shouldn't expose: a database password, an API token, an encryption key. trstctl is a full secrets platform alongside its certificate work — it stores secrets encrypted, hands out short-lived ones on demand, rotates them safely, encrypts data on behalf of apps, syncs secrets to other platforms, and governs who can read or change them.
The mental model: think of a bank. The vault stores valuables encrypted (the secret store). The safe-deposit clerk issues a temporary key that self-destructs after an hour (dynamic secrets). The armored-car service moves valuables to other branches (secret sync). The teller window encrypts your deposit without you ever seeing the master key (encryption-as-a-service). And every action needs ID and is logged (auth + approvals + audit).
One honest note up front. Most of the secrets domain is now served (
GAP-006): the secret store (CRUD + rotation), one-time secret sharing, the dynamic PKI secret, and machine login are mounted on the running control plane under/api/v1/secrets/*(off by default —secrets.enable_api— and fail-closed when off). Secret-sync to external stores (internal/secretsync) is still built-and-tested library code with no served surface yet. So most of this page is a live endpoint today; sync you still drive via its Go APIs. See Current limitations. This page is honest about that throughout.
Why it exists
Leaked secrets are one of the most common breach causes, because the traditional
approach — long-lived secrets copied into config files, environment variables, images,
and CI — spreads them everywhere and never expires them. trstctl attacks the problem
from every side: encrypt them properly at rest, prefer short-lived/dynamic secrets that
can't be hoarded, rotate the long-lived ones automatically, never let a secret value
touch a log or a disk it shouldn't, and put approvals and a tamper-evident audit trail
around access. Secret material is always held in []byte and zeroized, never a Go
string (non-negotiable AN-8).
How it works
How every secret is encrypted at rest
trstctl uses envelope encryption (non-negotiable AN-3, all in
internal/crypto). Each secret is encrypted with a fresh per-secret data key (DEK,
AES-256-GCM), and that DEK is itself encrypted under a master key-encryption key (KEK).
The encryption is bound to the secret's tenant and path, so a sealed blob can't be moved
elsewhere. The KEK is loaded at startup from TRSTCTL_SECRETS_KEK_FILE (0600),
held only transiently, and zeroized. To rotate protection you re-wrap small DEKs, not all
your data. Code: internal/crypto/envelope.go.
The native secret store (F63)
A versioned key-value store: every Put creates a new version (old versions stay
queryable), Delete writes a tombstone (history is retained), and the whole version list
can be reconstructed from the event log (AN-2) — the store is a projection, not a
primary write. Writes are idempotent by key (AN-5), tenant-isolated with cross-tenant
denial (AN-1), and the ready-to-mount APIServer enforces per-secret RBAC.
Code: internal/secretstore (Store, Put/Get/Versions/Rollback/Delete, APIServer).
The developer secrets experience (F64)
Two pieces make secrets pleasant and safe for developers. A CLI injector runs your
program with secrets in its environment without ever writing them to disk (only the
variable names are audited, never values). An SDK caches secrets and auto-refreshes
them before expiry, and on a revocation it evicts the cache and fails safe rather than
serving a stale, revoked secret. Code: internal/secretscli, internal/secretsdk.
Dynamic secrets (F65) and PKI-as-a-secrets-engine (F67)
Instead of a long-lived secret to steal, dynamic secrets are minted on demand,
scoped, and time-limited by a lease; when the lease expires trstctl
revokes the underlying credential automatically — even across a restart, because the
revocation intent is written to a durable outbox (AN-6). Seven
backends ship behind one interface: PostgreSQL, MySQL, MongoDB, AWS STS, GCP IAM, Azure
service principal, and Redis/SSH; all pass a lifecycle conformance test. PKI-as-a-
secrets-engine plugs the same lease machinery into certificate issuance — a developer
requests a short-lived certificate exactly like a database password, and the leaf key is
generated locked and destroyed immediately (AN-8). Code: internal/dynsecret,
internal/leaseworker, internal/pkisecret.
Secret rotation (F37)
The rotation engine replaces a long-lived secret in four rollback-safe phases: stage
the new version, cut consumers over, verify they're healthy, retire the old one. If
cutover or verification fails, it automatically rolls back so the application is never
left broken; each phase is audited and, in production, delivered via the outbox so a crash
mid-rotation strands nothing. Code: internal/rotation (Engine.Rotate).
Ephemeral API keys (F38)
For high-churn automation, trstctl issues short-lived credentials gated by
attestation: prove what you are, get a sub-hour credential,
let it expire (no CRL needed). Every request needs an idempotency key (AN-5) and
nothing is minted unless attestation verifies. Code: internal/ephemeral.
Encryption-as-a-service & KMIP (F66)
The transit service encrypts, decrypts, HMACs, and signs data using named keys the
application never sees — ciphertexts are versioned (trv:<version>:...) so a key
rotation can re-wrap old data, and intermediate plaintext is zeroized (AN-8). For
legacy enterprise gear (databases, storage arrays) trstctl also answers KMIP, the
standard key-management protocol, with TLS client-cert auth and key material zeroized on
destroy. Code: internal/transit, internal/kmip.
Secret sync (F68)
trstctl can push secrets outward to the platforms that need them — Kubernetes, GitHub
Actions, GitLab CI, Terraform, Vercel, AWS Parameter Store, or a generic webhook — via the
durable outbox (at-least-once, no half-writes, AN-6), and it detects drift by
comparing hashes when a target is changed out-of-band. Code: internal/secretsync.
The auth-method framework (F58)
Before a workload can read a secret, it has to authenticate to trstctl. The auth-method
framework is that login layer: a workload presents a credential (a token, an OIDC JWT, a
Kubernetes SA token, cloud IAM, etc.), trstctl verifies it through internal/crypto
(timing-safe), and issues a scoped, time-bounded session. Credential bytes are never
logged (AN-8); every attempt is audited (AN-2). Code: internal/authmethod.
Secret scanning bridge (F39) and sharing & approvals (F60)
The scanning bridge ingests findings from gitleaks and trufflehog into the
credential graph and risk view — structurally excluding the secret
value (the parsers never read it) — and can auto-trigger the
compromise workflow. Secret sharing creates one-time,
self-destructing links (viewed once, then deleted; expiry-bounded), and change
approvals put a dual-control approval gate on secret mutations.
Code: internal/secretscan, internal/secretshare.
Use it
These run through their Go APIs today. The shapes:
// Native store: versioned, envelope-encrypted put/get
store.Put(ctx, "db/password", []byte("s3cr3t"), "idem-key") // -> version 1
val, _ := store.Get(ctx, "db/password") // latest live version
// Dynamic secret: a 1-hour Postgres credential, auto-revoked at lease end
lease, _ := dyn.Issue(ctx, "postgresql", "readonly", time.Hour, "req-1")
// Transit: encrypt without the app ever holding the key
ct, _ := keyring.Encrypt(ctx, "app-key", []byte("hello"), nil) // -> "trv:1:..."
The secretstore.APIServer exposes the store over HTTP (PUT/GET /secrets/<path>, with
Idempotency-Key and tenant headers) once mounted.
Pitfalls & limits
- Serving status: the secret store, one-time sharing, the dynamic PKI secret, and
machine login are served on the running control plane under
/api/v1/secrets/*(GAP-006; enable withsecrets.enable_api, off by default and fail-closed). Secret sync (internal/secretsync) is not yet wired — it remains library code. Track the remaining tail in Current limitations. - Protect the KEK. Everything at rest is only as safe as
TRSTCTL_SECRETS_KEK_FILE; in production back it with an HSM/KMS. - Dynamic beats static. Prefer dynamic/ephemeral secrets over long-lived ones; if you must store a long-lived secret, put it on a rotation schedule.
- KMIP/transit wire interop is tested against reference clients but confirm your specific appliance's KMIP profile.
- Sync is push + drift-detect, not a two-way merge — trstctl is the source of truth.
Reference
- At rest: envelope encryption (AES-256-GCM DEK wrapped by KEK); config
TRSTCTL_SECRETS_KEK_FILE. - Store:
Put/Get/GetVersion/Versions/Rollback/Delete/Purge;APIServer(PUT/GET /secrets/<path>,Idempotency-Key). - Dynamic backends:
postgresql,mysql,mongodb,aws-sts,gcp-iam,azure-sp,redis-ssh, pluspki. - Transit:
Encrypt/Decrypt/Rewrap/HMAC/Sign/Verify, versionedtrv:<n>:ciphertext. - Sync targets: Kubernetes, GitHub Actions, GitLab CI, Terraform, Vercel, AWS Parameter Store, webhook.
- Events:
secret.version.written,rotation.*,auth.session.issued,secretscan.finding.
See also
Workload identity (attestation behind ephemeral secrets) · Issuance & certificate authorities (HSM-backed KEK; PKI engine) · Incident response & JIT (compromise + approvals) · Discovery & inventory (finding existing secrets) · Current limitations · glossary: secret, envelope encryption, KEK/DEK, dynamic secret, lease, transit, KMIP
Covers: F37, F38, F39, F63, F64, F65, F66, F67, F68, F58, F60