Managing secrets in wasmCloud
Patterns
Most runtimes give you one way to pass a secret to a workload: put it in the environment and hope. wasmCloud is the exception. It has a pluggable secrets API, so a component asks the lattice for a secret, and a backend you run decides whether to answer.
seekrit ships a backend for it. The backend runs on your lattice, holds the service token, decrypts locally, and seals each value to the key of the host that asked. The seekrit API never sees a plaintext, and neither does anything between the backend and the component.
helm repo add seekrit https://charts.seekrit.dev
helm install seekrit-wasmcloud seekrit/seekrit-wasmcloud \
--namespace wasmcloud --create-namespace \
--set profiles.checkout.token=skt_... \
--set profiles.checkout.issuers[0]=ACCOUNT_PUBLIC_KEY \
--set nats.url=nats://nats:4222
Why a backend and not an environment variable
Every other host seekrit integrates with makes you choose between two bad positions: the platform knows about your secrets and therefore holds them, or the platform knows nothing and you smuggle values in through the environment.
wasmCloud's secrets API is the seam that makes both possible at once.
wash config put and the secrets: block in a wadm manifest work exactly as
they do with any backend, so operators use the tooling they already know. The
value is fetched at use, from your backend, on your infrastructure.
The comparison that makes this concrete is the other WebAssembly integration seekrit ships. On Spin, or on wasmtime, the only way to keep decryption inside the sandbox is a component the guest imports — and the platform has no idea any of it happened. On wasmCloud the platform participates. That is why wasmCloud gets two integrations and every other Wasm host gets one.
The trust model is the issuer, not the subject
A wasmCloud secret request carries an nkeys JWT identifying the component, and the first thing to understand is what that signature proves.
nkeys tokens are self-issued. Anyone can generate a keypair, sign a token
claiming any sub they like, and it verifies. Verification proves that whoever
holds the issuer key vouched for that subject. It does not prove the subject
is who the name suggests.
So the backend's profiles anchor on the issuer:
profiles:
checkout:
token: skt_...
issuers: ["ACCOUNT_PUBLIC_KEY"] # who signed the component
applications: ["checkout"] # narrows further
applications is the wadm application name, which is unsigned metadata. It is
useful for narrowing and it is never enough on its own — a profile that matches
on nothing but applications is refused when the chart renders, rather than
quietly trusted.
Matchers AND together, so declaring more always narrows. Selection is default-deny: an identity matching no profile is refused, and an identity matching two is also refused rather than resolved to either. Picking one would hand a workload an environment nobody unambiguously granted it.
Each profile's token is bound to one app environment, so the key in a config
entry names a secret within that environment and cannot reach outside it.
Two steps that fail silently
Both of these leave the backend running and the lattice simply never asking it, with no error naming either side.
Hosts must allow the secrets subtree. Start each host with the subject prefix the backend listens under, and make sure its NATS user may publish there:
wasmcloud --secrets-topic-prefix wasmcloud.secrets
Each secret needs a config entry naming the backend:
wash config put SECRET_database_url \
backend=seekrit \
key=DATABASE_URL \
type=secret.wasmcloud.dev/v1alpha1
Check reachability before deploying anything:
nats req 'wasmcloud.secrets.v1alpha1.seekrit.server_xkey' ''
A public key comes back. A timeout means one of those two steps, or NATS permissions — not seekrit.
What the backend refuses
Two request fields are rejected rather than ignored, because silently dropping a constraint returns a value the caller believes is narrower than it is:
version— seekrit's read path has no per-secret versions. Leave it unset or setlatest; any other pin is an error.field— a seekrit secret is a single string. Name it inkey.
Rotation
Each profile re-resolves on a timer, 60 seconds by default, so a rotated secret reaches components without a redeploy.
Startup is fail-closed and refreshes are lenient: a bad token or unreachable API stops the pod coming up, rather than registering a backend that answers "not found" for everything, while a failed refresh keeps the last good snapshot and retries. The refresh interval is also the bound on that leniency — a revoked token stops working within one tick of the API answering again.
If you want the value never to leave the guest
The backend does hold a plaintext for a moment, in its own memory, on your
infrastructure. If that is one moment too many, the
WASI secrets component is the alternative: your component
imports seekrit:secrets/store and decrypts inside its own linear memory.
It works on wasmCloud and on every other WASI 0.2 host.
The trade is visibility. The component is invisible to the platform — wash
and wadm do not know your secrets exist. The backend is the opposite, and for
most teams on wasmCloud that is the one worth having. Reach for the component
when the guest is the only thing you are willing to trust.
The full setup, including multi-replica key handling and telemetry, is at /docs/guides/wasmcloud.