wasmCloud Security: Actor Authentication, Capability Providers, and Lattice Trust
Problem
wasmCloud runs WebAssembly actors (components) across a distributed lattice — a cluster of wasmCloud hosts connected via NATS messaging. Each actor is isolated behind a capability contract: it can only invoke capabilities (HTTP client, key-value store, blob store, messaging) that are explicitly linked at runtime by an operator. No capability, no access.
This default-deny capability model is wasmCloud’s primary security advantage over traditional microservice architectures. An actor cannot open a socket, write to disk, or call an external API unless a capability provider link is established.
The security model is only as strong as its trust infrastructure:
- NKEY authentication. Actors, providers, and hosts are identified by Ed25519 NKEY pairs. If a private NKEY is leaked, an attacker can impersonate any identity in the lattice.
- Lattice authentication. All inter-host communication transits NATS. Without NATS credential files and JWT-based authentication, the lattice is unauthenticated.
- OCI actor distribution. Actors are distributed as OCI artifacts. Without signature verification at load time, a tampered actor image is indistinguishable from a legitimate one.
- Capability link configuration. Links between actors and providers are established by operators. A misconfigured link grants an actor capabilities beyond its intended scope.
- Account and operator hierarchy. wasmCloud uses a chain of trust: Operator keys sign Account keys, Account keys sign Actor/Provider/Host keys. If the operator key is compromised, all downstream trust is broken.
By 2026, wasmCloud is deployed in multi-tenant serverless platforms, edge computing, and as the runtime for AI agent tooling. The blast radius of a trust compromise scales with the deployment breadth.
Target systems: wasmCloud 1.0+; NATS 2.10+ with JetStream; wash (wasmCloud shell) 0.27+; wasmcloud-operator 0.3+ (Kubernetes); cosign 2.4+ for actor signing.
Threat Model
- Adversary 1 — NKEY private key theft: An attacker obtains an actor’s private NKEY from a compromised host, build server, or leaked secret. They sign arbitrary actor JWTs that the lattice trusts.
- Adversary 2 — Unauthenticated lattice access: A wasmCloud lattice uses NATS without authentication. An attacker on the internal network subscribes to NATS subjects, intercepting lattice control messages and actor RPC calls.
- Adversary 3 — Tampered OCI actor image: An attacker substitutes a backdoored actor image in the OCI registry. Without signature verification, wasmCloud loads and runs it.
- Adversary 4 — Capability link escalation: An operator accidentally links an untrusted user-facing actor to the key-value provider for the payments namespace. The actor now has read/write access to payment data.
- Adversary 5 — Operator key compromise: The top-level operator NKEY is compromised. The attacker issues new account and actor credentials, gaining control of the entire lattice.
- Access level: Adversary 1 has file system access on a host or build server. Adversary 2 has internal network access. Adversary 3 has OCI registry write access. Adversary 4 is a legitimate operator with a misconfiguration. Adversary 5 has access to the operator key material.
- Objective: Run arbitrary code in the lattice, intercept actor RPC calls, exfiltrate data through mislinked capabilities.
- Blast radius: NKEY compromise = impersonation of all actors signed by that key. Operator key compromise = control of entire account’s trust chain. Misconfigured capability link = data exfiltration from a capability the actor shouldn’t have.
Configuration
Step 1: Generate and Protect NKEYs
wasmCloud uses Ed25519 NKEYs for all identity. The key hierarchy:
Operator NKEY (top level; sign account keys)
└── Account NKEY (sign actor/provider/host keys)
├── Actor NKEYs (per-actor identity)
├── Provider NKEYs (per-provider instance)
└── Host NKEYs (per-wasmCloud host)
# Install nk (NKEY CLI tool).
cargo install nkeys --features cli
# Generate the operator key pair (protect this like a root CA private key).
nk gen operator
# Output:
# Public Key: Oxxxxxxxxxxxxxxxxxxxxxxxx (starts with O = Operator)
# Seed: SOxxxxxxxxxxxxxxxxxxxxxxxx (starts with SO = Seed Operator; KEEP PRIVATE)
# Generate account key pair.
nk gen account
# Public Key: Axxxxxxxxxxxxxxxxxxxxxxxx (A = Account)
# Seed: SAxxxxxxxxxxxxxxxxxxxxxxxx
# Generate actor key pair (one per actor).
nk gen module
# Public Key: Mxxxxxxxxxxxxxxxxxxxxxxxx (M = Module/Actor)
# Seed: SMxxxxxxxxxxxxxxxxxxxxxxxx
# Generate host key pair (one per wasmCloud host).
nk gen server
# Public Key: Nxxxxxxxxxxxxxxxxxxxxxxxx (N = Server/Host)
# Seed: SNxxxxxxxxxxxxxxxxxxxxxxxx
Store seeds in a secrets manager — never in source code or environment variables:
# Store in HashiCorp Vault.
vault kv put secret/wasmcloud/operator seed="SOxxxxxxx"
vault kv put secret/wasmcloud/account seed="SAxxxxxxx"
# Retrieve at deployment time (never persist to disk).
OPERATOR_SEED=$(vault kv get -field=seed secret/wasmcloud/operator)
Step 2: Sign Actors with wash and cosign
Actors must be signed to be loadable in the wasmCloud lattice. The actor JWT embeds:
- The actor’s public key (from its NKEY)
- Declared capability claims (what the actor is allowed to use)
- Signing key chain (account key signed by operator key)
- Expiry (optional but recommended)
# Build the actor WASM component.
wash build
# Sign the actor.
# The signed actor embeds a JWT that the host verifies at load time.
wash claims sign ./target/wasm32-wasip2/release/my_actor.wasm \
--name "payments-processor" \
--version 1.2.3 \
--rev 1 \
--issuer $(vault kv get -field=seed secret/wasmcloud/account) \
--subject $(vault kv get -field=seed secret/wasmcloud/actor-payments) \
--cap wasmcloud:httpserver \
--cap wasmcloud:keyvalue \
--expires-in-days 90 \
-o payments-processor_s.wasm
# Verify the signed actor.
wash claims inspect payments-processor_s.wasm
# Output shows: issuer, subject, capabilities, expiry.
Additionally sign the OCI image with cosign for registry-level integrity:
# Push the signed actor as an OCI artifact.
wash push ghcr.io/myorg/payments-processor:v1.2.3 payments-processor_s.wasm
# Sign the OCI artifact with cosign (keyless).
cosign sign --yes ghcr.io/myorg/payments-processor:v1.2.3
Configure wash to verify cosign signatures before loading:
# wasmcloud.yaml
actor_signing:
required: true
verify_cosign: true
cosign_policy:
certificate_identity: "https://github.com/myorg/wasmcloud-actors/.github/workflows/build.yml@refs/heads/main"
certificate_oidc_issuer: "https://token.actions.githubusercontent.com"
Step 3: Authenticate the NATS Lattice
All wasmCloud hosts communicate via NATS. Secure the NATS cluster:
# nats-server.conf
port: 4222
# TLS for client connections.
tls {
cert_file: "/etc/nats/tls/server.crt"
key_file: "/etc/nats/tls/server.key"
ca_file: "/etc/nats/tls/ca.crt"
verify: true # Require client TLS.
}
# NATS account/user authentication via decentralised JWT (NKEYs).
operator: "/etc/nats/operator.jwt"
# System account for internal monitoring.
system_account: SYS
# Resolver for account JWTs.
resolver: {
type: full
dir: "/etc/nats/accounts"
}
Generate NATS credentials for wasmCloud hosts:
# Using nsc (NATS security credentials tool).
nsc add operator --name wasmcloud-lattice
nsc add account --name wasmcloud-account
nsc add user --name wasmcloud-host-1
nsc generate creds --name wasmcloud-host-1 > host-1.creds
# Each host gets its own credential file.
nsc generate creds --name wasmcloud-host-2 > host-2.creds
Configure wasmCloud hosts with the credential file:
wasmcloud --nats-credsfile /etc/wasmcloud/host-1.creds \
--nats-host nats.internal:4222 \
--lattice-prefix prod-lattice
Step 4: Capability Link Policy
Capability links are the policy enforcement point. Define which actors can use which providers with which configuration:
# Link the payments actor to the KV store — with a specific bucket configuration.
wash link put \
Mxxxxxxx-payments-actor-key \
Vxxxxxxx-kv-provider-key \
wasmcloud:keyvalue \
values='{"bucket":"payments-data","prefix":"pay:"}'
# The KV provider enforces the bucket configuration:
# the payments actor can only read/write keys prefixed with "pay:" in "payments-data".
# It cannot access any other bucket.
Enumerate and audit all links periodically:
# List all active capability links in the lattice.
wash get links
# Output:
# Actor ID | Provider ID | Contract ID | Link Name | Values
# Mxxxxxx | Vxxxxxx | wasmcloud:keyvalue | default | {"bucket":"payments-data"}
# Mxxxxxx | Vxxxxxx | wasmcloud:httpserver | default | {"address":"0.0.0.0:8080"}
# Any link not in the approved list should be investigated and removed.
wash remove link <actor-key> <contract-id> <link-name>
Enforce link policy with OPA (using wash’s policy hooks):
# link_policy.rego
package wasmcloud.links
default allow = false
allow {
input.actor_public_key == known_actors[_]
input.provider_contract == "wasmcloud:keyvalue"
input.link_values.bucket in allowed_buckets[input.actor_public_key]
}
known_actors = {"Mxxxxxx-payments-actor"}
allowed_buckets = {
"Mxxxxxx-payments-actor": {"payments-data"}
}
Step 5: Host-Level Security Configuration
# Run wasmCloud with policy server enforcement.
wasmcloud \
--policy-service-endpoint http://opa.internal:8181/v1/data/wasmcloud \
--nats-credsfile /etc/wasmcloud/host.creds \
--host-seed $(vault kv get -field=seed secret/wasmcloud/hosts/host-1) \
--allow-file-load false \ # Actors loaded from OCI only; no local filesystem.
--lattice-prefix prod \
--js-domain wasmcloud # JetStream domain for state persistence.
On Kubernetes via wasmcloud-operator:
apiVersion: core.oam.dev/v1beta1
kind: WasmCloudHostConfig
metadata:
name: prod-host-config
namespace: wasmcloud
spec:
lattice: prod
secretName: wasmcloud-nats-creds # Kubernetes Secret with NATS credentials.
hostLabels:
region: us-east-1
environment: production
policyService: http://opa-policy.security.svc.cluster.local:8181/v1/data/wasmcloud
registryCredentialSecretName: wasmcloud-registry-creds
podSpec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: wasmcloud-host
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
Step 6: Actor Capability Claims Minimisation
When signing actors, declare only the capabilities the actor uses. The wasmCloud host rejects links to undeclared capabilities:
# Minimal capability set for a read-only API actor.
wash claims sign my-api-actor.wasm \
--cap wasmcloud:httpserver \ # Receive HTTP requests.
--cap wasmcloud:keyvalue \ # Read cache.
# NOT: wasmcloud:blobstore # Actor doesn't write files.
# NOT: wasmcloud:messaging # Actor doesn't produce messages.
--name "api-read-only" \
--issuer $ACCOUNT_SEED \
--subject $ACTOR_SEED
# If an operator tries to link this actor to the blob store:
# Error: actor does not have capability claim for wasmcloud:blobstore
Step 7: Rotate NKEYs and Actor Signing Keys
# Rotate an actor NKEY when the seed is suspected compromised.
# 1. Generate a new actor key pair.
nk gen module > new-actor.nkey
# 2. Re-sign the actor with the new key.
wash claims sign my-actor.wasm \
--subject $(cat new-actor.nkey | grep Seed | awk '{print $2}') \
--issuer $ACCOUNT_SEED \
[other flags] \
-o my-actor-new-key_s.wasm
# 3. Push the re-signed actor to the OCI registry.
wash push ghcr.io/myorg/my-actor:v1.2.4 my-actor-new-key_s.wasm
# 4. Update the running lattice to use the new actor version.
wash update actor <host-id> <old-actor-public-key> ghcr.io/myorg/my-actor:v1.2.4
# 5. Update all capability links from the old public key to the new one.
wash remove link <old-actor-public-key> wasmcloud:keyvalue default
wash link put <new-actor-public-key> <provider-key> wasmcloud:keyvalue default
# 6. Revoke the old NKEY by removing it from the account's trust list.
nsc revoke add user --name old-actor-key-name
Step 8: Telemetry
wasmcloud_actor_starts_total{actor_name, host_id} counter
wasmcloud_actor_invocations_total{actor, provider, contract} counter
wasmcloud_actor_errors_total{actor, error_type} counter
wasmcloud_link_established_total{actor, provider, contract} counter
wasmcloud_link_removed_total{actor, provider, contract} counter
wasmcloud_host_up{host_id, lattice} gauge
nats_auth_failure_total{server, client} counter
wasmcloud_policy_denial_total{actor, capability} counter
Alert on:
nats_auth_failure_total— NATS credential failure; possible key compromise or misconfiguration.wasmcloud_policy_denial_total— link attempted with an undeclared capability; investigate the actor deployment.wasmcloud_actor_errors_totalspike — unexpected actor failure; possible capability misconfiguration or actor bug.wasmcloud_link_established_totalfor unexpected actor/provider pairs — unauthorised link; remove and audit.
Expected Behaviour
| Signal | Default wasmCloud | Hardened wasmCloud |
|---|---|---|
| Unauthenticated NATS access | Allowed if no auth configured | NATS credentials required; connection rejected |
| Unsigned actor loaded | Loaded without verification | Rejected; JWT signature required |
| Actor uses undeclared capability | Link established; actor uses it | Link rejected; actor lacks the capability claim |
| OCI image tampered | Loaded silently | cosign verification fails; actor not started |
| NKEY seed leaked | Attacker can impersonate any actor | Rotate seed; re-sign actors; old key revoked |
| Capability link to wrong bucket | Actor accesses all buckets | Provider enforces configuration; only declared bucket accessible |
Trade-offs
| Aspect | Benefit | Cost | Mitigation |
|---|---|---|---|
| NKEY identity for all components | Cryptographic identity; no shared secrets | Key generation and rotation overhead | Automate via wash and Vault; treat NKEYs like TLS certificates. |
| Signed actors with capability claims | Host refuses undeclared capabilities | Actor re-sign required after capability change | Build signing into CI; re-sign on every release automatically. |
| Policy server enforcement | Link-level policy; OPA for complex rules | Additional latency per link establishment | Links are established at deploy time, not per-request; latency is one-time. |
| NATS JWT authentication | Decentralised; no single secret file | nsc tooling complexity | Document key generation; use nsc push to a NATS resolver for centralised management. |
| cosign actor verification | OCI-level integrity | Requires OIDC CI pipeline for keyless signing | Reuses Sigstore infrastructure already in place for container signing. |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Actor signing key seed leaked | Attacker can sign actors with the compromised identity | Unexpected actor versions in the lattice | Rotate the actor NKEY; re-sign all actors; revoke old key in the account. |
| NATS credential expired | wasmCloud host cannot connect to lattice | Host disappears from wash get hosts; actor invocations fail |
Rotate NATS credentials; update the host’s creds file or Kubernetes Secret. |
| OCI cosign verification fails | Actor fails to start on host | Host logs show cosign verification failed; actor count drops |
Verify the actor was signed in CI; check cosign policy configuration. |
| Misconfigured capability link | Actor has access to wrong bucket/topic | Data anomalies in unrelated services | Remove the incorrect link; add correct link; audit all links after incidents. |
| Operator key compromised | All downstream actors/accounts can be spoofed | Unusual actor activity; unexpected key identities in lattice | Emergency: rotate operator key; re-issue all account and actor keys; re-sign all actors. |
| Policy server unreachable | Link establishment blocked (fail-closed) | wasmcloud_policy_denial_total spike; actors cannot start |
Restore policy server; or configure fail-open temporarily (document the risk). |