Secrets
Overview
Production applications often require secrets at runtime to access internal or external systems. wasmCloud provides first-class support for secrets in distributed environments.
Secrets in wasmCloud are treated as a special piece of configuration. Secrets are stored in encrypted and secure stores; the host retrieves secrets from a secrets backend and provides them to components and providers that request them. Secrets backends are reachable by hosts via NATS using known NATS subject prefixes. Each secrets backend satisfies a common API so that anyone can write a backend and add an implementation to their cluster.
wasmCloud's secrets support is similar to the Kubernetes Secrets Store CSI driver, rather than the core Secrets API. Secrets are fetched from external stores and are encrypted in transit using xkeys—x25519 keypairs compatible with NaCl Seal and Open operations—and only ever stored in-memory using the Rust secrecy
crate.
When to use secrets
In wasmCloud, secrets should be used for sensitive data that should never be stored in plaintext. Anytime your application needs an API key, password, token, or otherwise protected data, you should use a secret instead of embedding that into your application. By design, secrets are never logged or accessible in-memory, and when they are dropped they are wiped in-memory. The host has a means to fetch the secret on behalf of a component or provider, but not to use it itself.
How secrets work in wasmCloud
Secrets support requires wasmCloud 1.1 or higher. Utilizing secrets requires:
- A secrets backend (for example, the NATS KV secrets backend or the Vault secrets backend)
- A secret in the relevant secret store
- A component to consume the secret using the WebAssembly Interface Type (WIT) secrets API, or a capability provider to receive the secret at runtime
- A secret definition scoped for the relevant usage (entity-scoped or link-scoped) in the wasmCloud Application Deployment Manager (wadm) application manifest
The complete path of a request for a secret is illustrated in this diagram:
- When a component starts with a secret requirement, the host forms a request for the secret.
- The request is passed to the wasmCloud host's secrets handler, which resolves the secret reference and requests the secret from the proper secrets backend process.
- The secrets backend mediates with a secrets store—in the pictured examples, that might be a completely external store like Vault or the key-value store already available as part of NATS.
- A component makes a request for a named secret (
my-secret
) using theget
function of thewasmcloud:secrets
interface, and the host provides the secret resource to the component. - A component reveals a secret by calling the
reveal
function on the secret resource.
Defining secrets in application manifests
Components and providers must specify which secrets they require as part of their manifest definitions. A secret can be defined in two different places: at the top level of a component or a provider, at the same level as config, or in the configuration of a link. This gives us the opportunity to distinguish between the two contexts and have the host merge them together as needed.
In order to support providing additional backend-specific information in the application manifest, we use the policies draft specification from the Open Application Model (OAM). These are defined in the application manifest at the top level under spec
. See below for examples.
Entity-scoped secrets
An entity-scoped secret is a secret that is defined at the top level of a component or provider. This means that the secret is available in any function defined by a component, including those that are implemented to handle a specific interface attached to a link. This is useful for secrets that are used across multiple interfaces or functions.
spec:
policies:
# Policy for vault, including role name and mount path
- name: vault
type: policy.secret.wasmcloud.dev/v1alpha1
properties:
backend: 'vault'
role_name: 'demo-role'
mount_path: 'jwt'
# Policy for aws-secrets-manager, including IAM role
- name: aws-secrets-manager
type: policy.secret.wasmcloud.dev/v1alpha1
properties:
backend: 'aws-secrets-manager'
iam_role: 'arn:aws:iam::123456789012:role/demo-role'
components:
- name: http-component
type: component
properties:
image: ghcr.io/wasmcloud/test-fetch-with-token:0.1.0-fake
secrets:
# wasmCloud will fetch this secret from the vault backend
- name: some-api-token
properties:
policy: vault
key: secrets/test/value
version: 1
# wasmCloud will fetch this secret from the aws-secrets-manager backend
- name: my-other-secret
properties:
policy: aws-secrets-manager
key: secret-name
version: 'be01a5fb-7ebb-4ae9-8ea0-0902e8940bc0'
Link-scoped secrets
A link-scoped secret is only accessible in the context of a link. For a component, this means that it is only available in the functions that are implemented to handle the interfaces that defined by the link. For a provider, this means that is supplied when the link is created. This is intended for secrets such as database connection strings, API tokens, etc, that are needed in the context of a component communicating with another component or a provider. An example of what this looks like is shown below:
- type: link
properties:
namespace: wasmcloud
package: postgres
interfaces: [managed-query]
target:
name: sql-postgres
secrets:
- name: db-password
properties:
policy: vault
key: secrets/myapp/db-password
version: 1
Sample manifest
Below is a sample manifest (from the secrets example in the wasmCloud monorepo) that shows how secrets can be defined in a wasmCloud application:
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: keyvalue-counter-auth
annotations:
description: 'HTTP counter application with authentication, using the wasmcloud:secrets interface'
wasmcloud.dev/authors: wasmCloud team
wasmcloud.dev/source-url: https://github.com/wasmCloud/wasmCloud/blob/main/examples/security/secrets/component-keyvalue-counter-auth/wadm.yaml
wasmcloud.dev/homepage: https://github.com/wasmCloud/wasmCloud/tree/main/examples/security/secrets/component-keyvalue-counter-auth
wasmcloud.dev/categories: |
http,http-server,keyvalue,secrets,rust,example
spec:
# The policy block allows reuse of secrets backend configurations across components and providers
policies:
- name: nats-kv
type: policy.secret.wasmcloud.dev/v1alpha1
properties:
backend: nats-kv
components:
- name: counter
type: component
properties:
image: file://./component-keyvalue-counter-auth/build/component_keyvalue_counter_auth_s.wasm
id: auth-counter
# Provide the api_password secret to this component, fetching from nats-kv
secrets:
- name: api_password
properties:
policy: nats-kv
key: api_password
traits:
# Govern the spread/scheduling of the component
- type: spreadscaler
properties:
instances: 100
# Link the component to Redis on the default Redis port
#
# Establish a unidirectional link to the `kvredis` (the keyvalue capability provider),
# so the `counter` component can make use of keyvalue functionality provided by the Redis
# (i.e. using a keyvalue cache)
- type: link
properties:
namespace: wasi
package: keyvalue
interfaces: [atomics, store]
target:
name: kvredis
config:
- name: redis-url
properties:
url: 127.0.0.1:6379
# Provide the redis_password secret to the target of the link, kvredis. This is used
# to connect to Redis with the password at runtime.
secrets:
- name: redis_password
properties:
policy: nats-kv
key: redis_password
# Add a capability provider that enables Redis access
- name: kvredis
type: capability
properties:
image: file://./provider-keyvalue-redis-auth/build/wasmcloud-example-auth-kvredis.par.gz
id: auth-kvredis
# Provide the default_redis_password secret to the provider. The Redis provider can use this
# secret as needed to authenticate with Redis.
secrets:
- name: default_redis_password
properties:
policy: nats-kv
key: default_redis_password
# Add a capability provider that enables HTTP access
- name: httpserver
type: capability
properties:
image: ghcr.io/wasmcloud/http-server:0.23.1
id: auth-http-server
traits:
# Link the httpserver to the component, and configure the HTTP server
# to listen on port 8080 for incoming requests
#
# Since the HTTP server calls the `counter` component, we establish
# a unidirectional link from this `httpserver` provider (the "source")
# to the `counter` component (the "target"), so the server can invoke
# the component to handle a request.
- type: link
properties:
namespace: wasi
package: http
interfaces: [incoming-handler]
target:
name: counter
source:
config:
- name: default-http
properties:
address: 0.0.0.0:8080
Defining secrets on the command line
Secrets can also be defined on the command line using the wash
CLI. This is useful for testing and development purposes. The wash secrets
command allows you to define secrets in the same way as in the application manifest, and operates similarly to the wash config
command. Additional properties can be supplied with the --property
flag which is the equivalent of using the policy
field in the application manifest.
Note that the wash secrets
command is meant to define secret configuration that the host can use to fetch secrets from a secrets backend. It does not actually store the secret itself.
# wash secrets put <name> <backend> <key> --version <version> --property <property>...
wash secrets put my-secret vault secrets/test/value \
--version 1 \
--property role_name=demo-role \
--property mount_path=jwt
This will create a configuration called SECRET_my-secret
. When you start your component, provider, or link, simply supply the named configuration SECRET_<secret-name>
in the configuration field. For example:
wash start component ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 \
hello-world \
--config SECRET_my-secret
To retrieve a secret configuration from the config store, use the wash secrets get
command:
wash secrets get my-secret
Using secrets
wasmCloud ensures that secrets are never logged, stored, or transmitted in plaintext. However, if you reveal
a secret in a component or a capability provider, it is your responsibility to handle the secret securely.
For example, if you reveal a secret in a component and then log it or use the secret value as an argument to a different function, the secret may be exposed in logs or over the NATS network. Be sure to handle secrets securely in your code.
Components
Components access secrets using the wasmcloud:secrets
WIT interface. Support is built into the wasmCloud host to fetch secrets from a secrets backend and provide them to components at runtime.
Ensure you have the wasmcloud:secrets
interface in your component's WIT world:
package wasmcloud:example;
world component {
import wasmcloud:secrets/store@0.1.0-draft;
import wasmcloud:secrets/reveal@0.1.0-draft;
}
fn is_allowed(provided_password: String) -> bool {
// Resource secret, not actually loaded
let password = wasmcloud::secrets::store::get("api_password").expect("failed to get password");
// Revealed secret
match wasmcloud::secrets::reveal::reveal(&password) {
wasmcloud::secrets::store::SecretValue::String(s) => s == provided_password,
wasmcloud::secrets::store::SecretValue::Bytes(b) => String::from_utf8_lossy(&b) == provided_password,
}
}
Capability Providers
Providers access secrets by receiving them at startup time and at link time. At start time wasmCloud will give the capability provider a pair of encryption keys that are generated on-the-fly for this specific host and provider pair, and will use these keys to send the secrets to the provider. The provider can then decrypt the secrets using the same keys. This is all done transparently to the provider developer and is handled by the wasmCloud provider SDKs.
Entity scoped secrets
- Rust
- Go
impl SecretsExampleProvider {
pub async fn run() -> anyhow::Result<()> {
initialize_observability!("secrets-example-provider", None::<std::ffi::OsString>);
let HostData {
secrets, config, ..
} = load_host_data().context("failed to load host data")?;
let SecretValue::String(password) = secrets.get("password").context(
format!("password secret not found: {:?}", secrets),
)?
else {
bail!("password secret not a string")
};
// ...
}
}
import (
"log"
"github.com/wasmCloud/provider-sdk-go"
)
func main() {
provider, err := provider.New()
// Access the entity-scoped secret from the host
password := wasmcloudprovider.HostData().Secrets["password"]
if password.String.Reveal() != realPassword {
log.Fatal("Password does not match", err)
}
}
Link scoped secrets
- Rust
- Go
impl Provider for SecretsExampleProvider {
async fn receive_link_config_as_target(&self, link: LinkConfig<'_>) -> anyhow::Result<()> {
info!(?link.source_id, "handling link for component");
let SecretValue::String(password) = link
.secrets
.get("password")
.with_context(|| format!("password secret not found: {:?}", link.secrets))?
else {
bail!("password secret not a string")
};
}
}
import (
"errors"
"github.com/wasmCloud/provider-sdk-go"
)
func main() {
p := &Provider{}
wasmcloudprovider, err := provider.New(
provider.TargetLinkPut(p.handleNewTargetLink),
)
if err != nil {
return err
}
}
func handleNewTargetLink(handler *Provider, link provider.InterfaceLinkDefinition) error {
handler.Logger.Info("Handling new target link", "link", link)
// Access the link-scoped secret from the host
password := link.TargetSecrets["password"].String.Reveal()
if password != realPassword {
handler.Logger.Error("Password does not match")
return errors.New("Password does not match")
}
return nil
}
Example: Using a NATS KV backend
This is an example of an application that has a capability provider and component that refer to a secret value. The secret itself is encrypted and stored in a NATS KV secrets backend instance, and wasmCloud fetches this secret at runtime based on the signed identity of the component/provider.
This example is a modified version of the http-keyvalue-counter application that authenticates with a Redis database that requires a password, and serves an HTTP API that requires an authentication password header.
Prerequisites
Clone this repository locally:
git clone https://github.com/wasmCloud/wasmCloud.git
cd wasmCloud
Running this example
Build the keyvalue counter auth component, and the keyvalue redis auth provider:
wash build -p component-keyvalue-counter-auth
wash build -p provider-keyvalue-redis-auth
Run the example docker compose for the necessary infrastructure, generating secret keys:
export ENCRYPTION_XKEY_SEED=$(wash keys gen curve -o json | jq -r '.seed')
export TRANSIT_XKEY_SEED=$(wash keys gen curve -o json | jq -r '.seed')
docker compose up -d
Place the necessary secrets in the NATS KV backend:
# Ensure the TRANSIT_XKEY_SEED is still exported in your environment above
# or the decryption of the secret will fail
secrets-nats-kv put api_password --string opensesame
secrets-nats-kv put redis_password --string sup3rS3cr3tP4ssw0rd
# You can also put the password using an environment variable
SECRET_STRING_VALUE=sup3rS3cr3tP4ssw0rd secrets-nats-kv put default_redis_password
Allow your component and provider to access these secrets at runtime (this is a NATS KV backend specific step, other secrets backends like Vault will handle authorization externally with policies):
component_key=$(wash inspect ./component-keyvalue-counter-auth/build/component_keyvalue_counter_auth_s.wasm -o json | jq -r '.component')
provider_key=$(wash inspect ./provider-keyvalue-redis-auth/build/wasmcloud-example-auth-kvredis.par.gz -o json | jq -r '.service')
secrets-nats-kv add-mapping $component_key --secret api_password
secrets-nats-kv add-mapping $provider_key --secret redis_password --secret default_redis_password
Lastly, run wasmCloud and deploy the application:
WASMCLOUD_SECRETS_TOPIC=wasmcloud.secrets \
WASMCLOUD_ALLOW_FILE_LOAD=true \
NATS_CONNECT_ONLY=true \
wash up --detached
wash app deploy ./wadm.yaml
You can check the status of your application by running wash app list
. Once it's deployed, you can make requests to the application.
Making authenticated requests
You can first verify that unauthenticated requests to Redis and the component are denied:
➜ redis-cli -u redis://127.0.0.1:6379 keys '*'
(error) NOAUTH Authentication required.
➜ curl 127.0.0.1:8080/counter
Unauthorized
Then, authenticating passes the check in the component:
➜ curl -H "password: opensesame" 127.0.0.1:8080/counter
Counter /counter: 1
Passing in an invalid password will still fail the authentication check:
➜ curl -H "password: letmein" 127.0.0.1:8080/counter
Unauthorized
If you want to inspect the Redis database directly, you can provide the password in the URI:
redis-cli -u redis://sup3rS3cr3tP4ssw0rd@127.0.0.1:6379 get /counter
Cleanup
When finished with this example, simply shutdown wasmCloud and the resources running in Docker:
wash down
docker compose down
Secret flow
The diagrams below illustrate secrets flow across common use-cases.
Start component with secret
wash
asks host to start component withSECRET_foo
- Host fetches secret reference from the NATS KV bucket
- NATS KV bucket returns secret reference to the host
- Host fetches secret
foo
from secrets backend - Backend authorizes secret fetch by component JWT
- Backend returns secret
foo
's value, encrypted, to the host - Host starts component, stores secret in host handler with
secrecy
crate - Host publishes
component_scaled
event
Start provider with secret
wash
asks host to start provider withSECRET_foo
- Host fetches secret reference from the NATS KV bucket
- NATS KV bucket returns secret reference to the host
- Host fetches secret
foo
from secrets backend - Backend authorizes secret fetch by provider JWT
- Backend returns secret
foo
's value, encrypted, to the host - Host starts provider, passes value to provider via stdin and passes xkey private key for future secrets
- Host publishes
provider_started
event
Publish link with secret
wash
publishes link withSECRET_foo
- Host fetches secret reference from the NATS KV bucket
- NATS KV bucket returns secret reference to the host
- Host fetches secret
foo
from secrets backend - Backend authorizes secret fetch by source/target JWT
- Backend returns secret
foo
's value, encrypted, to the host - Host encrypts secret on the link with host private key, provider public key
- Host...
- publishes
put_link
message with encrypted secret - publishes
link_put
event
- publishes
- Provider decrypts secret on link with provider private key, host public key
Fetch secret within component
- Component sends
wasmcloud:secrets/get
call to host - Host returns secret reference
- Component sends
wasmcloud:secrets/reveal
call to host - Host exposes secret from
secrecy
crate's in-memory store, returning value to component
Learn more
- For more information on implementing a secrets backend, see the Secrets Backends page in our Operator Guide.