Skip to main content
Version: 1.x

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.

Compared to Kubernetes...

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:

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 the get function of the wasmcloud: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.

yaml
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'

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:

yaml
- 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 that shows how secrets can be defined in a wasmCloud application:

yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
  name: App with secrets
  annotations:
    version: v0.0.1
    description: 'HTTP hello world demo in Rust'
spec:
  components:
    - name: http-component
      type: component
      properties:
        image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0
        secrets:
          - name: test
            properties:
              backend: vault
              key: 'path/to/key'
      traits:
        # Govern the spread/scheduling of the component
        - type: spreadscaler
          properties:
            replicas: 1
        - type: link
          properties:
            target: httpclient
            namespace: wasi
            package: http
            interfaces: [outgoing-handler]
        - type: link
          properties:
            namespace: wasmcloud
            package: postgres
            interfaces: [managed-query]
            target:
              name: sql-postgres
              secrets:
                - name: db-password
                  properties:
                    backend: vault
                    key: secrets/myapp/db-password
                    version: 1
              config:
                - name: foo
                  properties:
                    value: whatever

    # Add a capability provider that enables HTTP access
    - name: httpserver
      type: capability
      properties:
        image: ghcr.io/wasmcloud/http-server:0.20.0
      traits:
        # Link the httpserver to the component, and configure the HTTP server
        # to listen on port 8080 for incoming requests
        - type: link
          properties:
            namespace: wasi
            package: http
            interfaces: [incoming-handler]
            target:
              name: http-component
              secrets: ...
            source:
              config:
                - name: default-http
                  properties:
                    address: 127.0.0.1:8080
              secrets:

    - name: httpclient
      type: capability
      properties:
        image: ghcr.io/wasmcloud/http-client:0.9.0

    - name: sqldb-postgres
      type: capability
      properties:
        image: ghcr.io/wasmcloud/provider-sqldb-postgres:0.1.0

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.

bash
# 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:

bash
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:

bash
wash secrets get my-secret

Using secrets​

warning

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:

wit
package wasmcloud:example;

world component {
  import wasmcloud:secrets/store@0.1.0-draft;
  import wasmcloud:secrets/reveal@0.1.0-draft;
}
rust
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
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")
        };
        // ...
    }
}
rust
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")
        };
    }
}

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:

bash
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:

bash
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:

bash
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:

bash
# 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):

bash
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:

bash
WASMCLOUD_SECRETS_TOPIC=wasmcloud.secrets \
    WASMCLOUD_ALLOW_FILE_LOAD=true \
    NATS_CONNECT_ONLY=true \
    wash up --detached
bash
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:

bash
➜ 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:

bash
➜ curl -H "password: opensesame" 127.0.0.1:8080/counter
Counter /counter: 1

Passing in an invalid password will still fail the authentication check:

bash
➜ 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:

bash
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:

bash
wash down
docker compose down

Secret flow​

The diagrams below illustrate secrets flow across common use-cases.

Start component with secret​

diagram of secret flow when starting component with secret

  1. wash asks host to start component with SECRET_foo
  2. Host fetches secret reference from the NATS KV bucket
  3. NATS KV bucket returns secret reference to the host
  4. Host fetches secret foo from secrets backend
  5. Backend authorizes secret fetch by component JWT
  6. Backend returns secret foo's value, encrypted, to the host
  7. Host starts component, stores secret in host handler with secrecy crate
  8. Host publishes component_scaled event

Start provider with secret​

diagram of secret flow when starting provider with secret

  1. wash asks host to start provider with SECRET_foo
  2. Host fetches secret reference from the NATS KV bucket
  3. NATS KV bucket returns secret reference to the host
  4. Host fetches secret foo from secrets backend
  5. Backend authorizes secret fetch by provider JWT
  6. Backend returns secret foo's value, encrypted, to the host
  7. Host starts provider, passes value to provider via stdin and passes xkey private key for future secrets
  8. Host publishes provider_started event

diagram of secret flow when publishing link with secret

  1. wash publishes link with SECRET_foo
  2. Host fetches secret reference from the NATS KV bucket
  3. NATS KV bucket returns secret reference to the host
  4. Host fetches secret foo from secrets backend
  5. Backend authorizes secret fetch by source/target JWT
  6. Backend returns secret foo's value, encrypted, to the host
  7. Host encrypts secret on the link with host private key, provider public key
  8. Host...
    1. publishes put_link message with encrypted secret
    2. publishes link_put event
  9. Provider decrypts secret on link with provider private key, host public key

Fetch secret within component​

diagram of secret flow when fetching secret within component

  1. Component sends wasmcloud:secrets/get call to host
  2. Host returns secret reference
  3. Component sends wasmcloud:secrets/reveal call to host
  4. Host exposes secret from secrecy crate's in-memory store, returning value to component

Learn more​