Skip to content

Secrets

Some modules need a high-entropy secret that no other system owns: an encryption root key, or a key to sign tokens with. Runway can generate such a module-private secret on every Dedicated tenant that enables your module, rotate it on request, and deliver it to your pods as a file. runwayctl dedicated provision generates the value and stores it in the tenant’s AWS Secrets Manager; runwayctl dedicated deploy writes it into a Kubernetes Secret in your module’s namespace. You don’t generate, store, or copy the value yourself, and it never appears in Terraform state, plan output, or logs.

Runway doesn’t know what the secret is for: it delivers random bytes, and your module decides how to use them. For secrets that two modules share, such as a service token, see Connections.

This page assumes your module is set up for Dedicated. If it isn’t, start with Getting Started.

A secret is configured in three places, and each setting comes from the first one that sets it:

  1. The tenant’s override in the Tenant Model, under modular_features.modules.<module-name>.values_override.secrets.<name>.
  2. Your default in the module manifest, under spec.secrets.<name>.defaultConfig, which catalog create copies into the Module Catalog entry.
  3. The built-in default in the field reference.

<name> is the secret’s logical name, a name of your choice that the module manifest, the Tenant Model, and your Helm values share. Secrets have no presence in the Fairway manifest: declaring one is all your module needs to do.

Step 1: Declare the secret in your module manifest

Section titled “Step 1: Declare the secret in your module manifest”
.runway/module.yaml
apiVersion: runway/v3
kind: RunwayManifest
metadata:
name: example-service-dedicated
spec:
secrets:
root-key:
defaultConfig:
bytes: 32
keyringSize: 2
signing-key:
defaultConfig:
encoding: hex

Each entry has one field, defaultConfig, which is required and may be empty (defaultConfig: {}) to take every built-in default. Field names can be in camelCase or snake_case.

Two fields belong to the tenant and are rejected in the module manifest:

Step 2: Mount the secret in your Helm values

Section titled “Step 2: Mount the secret in your Helm values”

Runway writes the Secret secret-<name>, with a single key, <name>. Mount it with a secretVolumes entry in your Helm values. You choose the directory, and the file in it is named after the secret:

.runway/values-dedicated.yaml
secretVolumes:
- name: secret-root-key
path: /secrets/infrastructure/encryption
- name: secret-signing-key
path: /secrets/infrastructure/signing

With these values, your service reads /secrets/infrastructure/encryption/root-key and /secrets/infrastructure/signing/signing-key.

deploy checks your mounts against the secrets:

  • An enabled secret that no secretVolumes entry mounts is a warning: your module can’t read it, which usually means that the names don’t match.
  • A disabled secret that an entry still mounts fails the deployment before anything is changed, because its Secret doesn’t exist and your pods couldn’t start. Remove the mount, or enable the secret in the Tenant Model.

The file holds one value, or a keyring of several values, depending on keyring_size:

keyring_size Content of the file
1 (default) The current value, with no trailing newline.
2 or more The newest values, one per line, newest first, up to keyring_size. Until enough rotations have happened, the file has fewer lines: a single one before the first rotation.

Each value is bytes random bytes in the configured encoding: standard, padded base64 (the default), or hex. For example, with bytes: 32, encoding: base64, and keyring_size: 2, the file holds up to two lines of 44 base64 characters each, the current key first.

A tenant that is happy with your defaults needs no secrets block. To change a setting for one tenant, set only that field:

Tenant Model
{
"modular_features": {
"modules": {
"example-service-dedicated": {
"enabled": true,
"version": "v1.26.0",
"values_override": {
"secrets": {
"root-key": { "keyring_size": 3 },
"signing-key": { "version": 2 }
}
}
}
}
}
}

Entries are merged by logical name and then field by field. An explicit false or 0 overrides a default. A tenant can also add a secret that has no default; every field has a built-in default, so any entry is complete.

SecretConfig in runwayctl’s secret.proto. Unknown fields are rejected.

Field Type Default Description
enabled boolean true Provisions and delivers the secret. Disabling a secret that has been generated deletes it. See Enabling and disabling.
prevent_deletion boolean true Protects the value from deletion. Tenant Model only. See Deleting a secret.
bytes integer 32 Number of random bytes, from 16 to 128, drawn from a cryptographically secure source. See Changing the size or encoding.
encoding string base64 How each value is encoded in the file: base64 (standard, padded) or hex.
keyring_size integer 1 How many values the file holds, newest first, from 1 to 10. 1 is a single value, no keyring.
version integer 1 Rotation trigger, at least 1. Tenant Model only. See Rotating a secret.

Logical names are lowercase letters, digits, and hyphens, starting and ending with a letter or digit, at most 45 characters.

provision creates the secret runway/<module-name>/secret/<name> in the tenant’s AWS Secrets Manager, encrypted with alias/<tenant-id>-tenant-secrets-kms-key. Each Secrets Manager version of it holds one value and carries the staging label runway-v<version>, which ties it to the Tenant Model’s version. provision writes a new version only when version increases, so re-running it changes nothing.

deploy then writes the Kubernetes Secret secret-<name> in the module’s namespace, from the newest versions, up to keyring_size. Before it changes anything in the cluster, it checks that the newest stored version is the Tenant Model’s version and that its value has bytes bytes, and fails otherwise. An older version means that provision hasn’t run since version was increased.

  1. Increase version in the Tenant Model, and provision the module. Runway generates a new value and stores it as a new version, next to the old ones.
  2. Deploy the module, which updates the Kubernetes Secret. The new value is the first line of the file, and the values that were current before follow, up to keyring_size.
  3. Your module notices the change when it re-reads the file.

Only ever increase version: a value lower than the newest stored version fails. Skipping numbers is harmless, because version is only a trigger.

The platform doesn’t know when your module is done with the older values. That is up to your module, which must document its own rotation procedure:

Use case First value Older values Safe to rotate again when
Encryption key Encrypts new data, and decrypts. Decrypt data not yet re-encrypted. All data has been re-encrypted with the new key. Rotating keyring_size times before that drops a key that data still needs from the file.
Signing key Signs. Verify tokens signed before the rotation. The longest token lifetime has passed.

With keyring_size: 1, a rotation replaces the value outright: anything encrypted or signed with the old value can no longer be decrypted or verified. Use it only if your module can tolerate that cut-over.

A rotation deletes nothing: older versions stay in Secrets Manager and only drop out of the file. Lowering keyring_size shortens the file at the next deployment, and raising it brings older values back. Runway keeps the ten newest versions labelled, which covers the largest keyring_size; Secrets Manager eventually removes older versions.

bytes applies to new values only. Changing it needs a rotation in the same Tenant Model change: without a new version, deploy fails, because the current value has the old size. After the rotation, a keyring holds values of different sizes until the older ones drop out.

encoding applies to every value in the file at the next deployment. It changes what your module reads, so change it only if your module accepts both encodings, or together with a release that does.

Every secret has an enabled field. Unset means enabled. What disabling does depends on whether provision has already generated the secret on the tenant:

  • Not generated yet: Runway doesn’t generate a disabled secret. This is how a secret is off by default: set enabled: false in the defaultConfig, and a tenant turns the secret on with enabled: true. A tenant can also set enabled: false before the secret’s first provision.
  • Already generated: disabling the secret deletes it. provision fails unless prevent_deletion: false was provisioned in an earlier change, and deletes the value otherwise. See Deleting a secret.

A generated secret can’t be disabled and kept. To stop using a secret without losing it, keep it enabled and remove its secretVolumes entry; deploy then warns that the secret isn’t mounted.

For a disabled secret, deploy deletes the secret-<name> Secret, after the Helm release has been installed. Remove the secretVolumes entry first: a disabled secret that is still mounted fails the deployment.

A secret is deleted only when it is disabled and prevent_deletion is false. Deletion takes two Tenant Model changes, so that a single edit can never delete a value:

  1. Set prevent_deletion: false for the secret, while it is still enabled, and provision the module.
  2. Set enabled: false, and provision the module again. Runway deletes the secret with a 30-day recovery window.

Disabling a protected secret, or setting prevent_deletion: false and enabled: false in the same change, fails provision with an error that names prevent_deletion and these two steps. During the recovery window, enabling the secret again fails with an error that names the aws secretsmanager restore-secret command to restore it.

Removing a secret’s declaration, from the module manifest or, for a secret without a default, from the Tenant Model, deletes nothing: Runway no longer knows the secret, so its value stays in Secrets Manager and its Secret stays in the cluster. To delete it, declare it in the Tenant Model again and take the two steps above.