Skip to content

Add secret-store backed config references for secret values #684

@ChristianPavilonis

Description

@ChristianPavilonis

Problem

Several application settings are secret material today, but they are authored directly in trusted-server.toml and then stored as part of the canonical runtime config payload. This increases plaintext exposure and makes it harder for provisioning to manage secrets safely.

We should establish a first-class secret reference pattern for config fields that should be backed by a provider secret store. The goal is for secret-bearing fields to be clearly marked in code, represented in TOML as references, and optionally generated/provisioned by the CLI instead of requiring users to paste plaintext values into config files.

Current secret-bearing config fields

Current obvious candidates include:

[publisher]
proxy_secret = "..."

[edge_cookie]
secret_key = "..."

[[handlers]]
username = "..."
password = "..."

Notes:

  • publisher.proxy_secret is secret material.
  • edge_cookie.secret_key is secret material.
  • handlers.password is secret material.
  • handlers.username may be sensitive/redacted, but is not necessarily secret-store material.
  • Request-signing private keys are already intended to live in signing_keys.
  • The Fastly runtime API token is already intended to live in api-keys/api_key, but that location is hardcoded and should become provider-scoped.
  • Future integrations may add API keys, client secrets, shared secrets, etc.

Today Redacted<String> helps avoid logging/displaying secret values, but it does not encode the stronger semantic that a value should be sourced from a secret store.

Proposed direction

Introduce a dedicated secret value/reference type for config fields that should be backed by a secret store.

A near-term TOML shape could be:

[providers.fastly.secrets]
default_store_name = "ts_secrets"

[publisher]
domain = "example.com"
cookie_domain = ".example.com"
origin_url = "https://origin.example.com"
proxy_secret = { secret = "publisher/proxy_secret" }

[edge_cookie]
secret_key = { secret = "edge_cookie/secret_key" }

[[handlers]]
id = "admin"
path = "^/admin"
username = "admin"
password = { secret = "handlers/admin/password" }

With a provider default secret store, this:

proxy_secret = { secret = "publisher/proxy_secret" }

means:

provider = fastly
store = ts_secrets
key = publisher/proxy_secret

For advanced cases, a field could override the default store:

proxy_secret = { store = "publisher_secrets", key = "proxy_secret" }

Default secret refs and generation

Each secret-bearing field should have metadata describing:

  • config path
  • default secret key
  • whether the secret can be generated
  • generation policy, if any
  • whether inline values are allowed during migration/dev

Possible defaults:

Field Default key Generate? Suggested generator
publisher.proxy_secret publisher/proxy_secret yes random base64, 32 bytes
edge_cookie.secret_key edge_cookie/secret_key yes random hex/base64, 32 bytes
handlers.<id>.password handlers/<id>/password maybe generated password/base64
Fastly runtime API token provider-scoped key, e.g. fastly/runtime_api_key or current api_key no user-provided
Request-signing private keys key ID based yes Ed25519 keypair

The CLI should be able to generate values for fields where users do not need to know the value, such as publisher.proxy_secret and edge_cookie.secret_key.

Handler passwords need extra care because the operator may need to know or distribute the generated password. We may want generation for handler passwords to be opt-in or require explicit confirmation.

Suggested Rust shape

Separate redaction from secret-store semantics.

Current:

Redacted<String>

Possible future shape:

pub enum SecretString {
    Inline(Redacted<String>),
    Ref(SecretRef),
    DefaultRef,
}

pub struct SecretRef {
    pub store: Option<String>,
    pub key: String,
}

Config structs could then explicitly mark secret-store-backed fields:

pub struct Publisher {
    pub domain: String,
    pub cookie_domain: String,
    pub origin_url: String,
    pub proxy_secret: SecretString,
}

pub struct EdgeCookie {
    pub secret_key: SecretString,
}

pub struct Handler {
    pub id: Option<String>,
    pub path: String,
    pub username: Redacted<String>,
    pub password: SecretString,
}

Field metadata can start as a manual trait rather than a derive macro:

pub struct SecretFieldDescriptor {
    pub path: &'static str,
    pub default_key: &'static str,
    pub generation: SecretGeneration,
    pub required: bool,
}

pub enum SecretGeneration {
    None,
    RandomBase64 { bytes: usize },
    RandomHex { bytes: usize },
}

A derive/annotation approach could be considered later, but a manual descriptor keeps the first implementation simpler.

Runtime resolution model

Use a two-phase settings model:

RawSettings      -> contains SecretString refs/default refs/inline values
ResolvedSettings -> contains resolved Redacted<String> values for runtime use

Runtime flow:

load canonical config payload
parse RawSettings
resolve secrets through RuntimeServices.secret_store()
validate resolved settings
serve request with one immutable resolved settings snapshot

Important behavior:

  • Runtime should not generate missing secrets.
  • Runtime should fail closed when a required secret ref cannot be resolved.
  • Canonical app config should not contain secret material.
  • Config hashes should not change when secret values rotate, only when refs/config change.

CLI/provisioning behavior

Provisioning should understand secret refs and missing generated secrets.

Possible plan output:

Missing secrets in Fastly secret store `ts_secrets`:

- publisher.proxy_secret -> publisher/proxy_secret
  action: generate random base64 32 bytes

- edge_cookie.secret_key -> edge_cookie/secret_key
  action: generate random base64 32 bytes

- handlers.admin.password -> handlers/admin/password
  action: requires value or explicit --generate-handler-passwords

This could be exposed through dedicated commands later:

ts secrets plan
ts secrets apply

or folded into existing provisioning:

ts provision fastly plan
ts provision fastly apply

The important part is that CLI provisioning should be able to create/populate provider secret stores before uploading the canonical app config.

Migration policy question

We need to decide how to handle inline secrets.

Recommended transitional policy:

  • Allow inline secrets during local/dev workflows.
  • Make ts config validate warn when inline secrets are present.
  • Make production provisioning warn or reject inline secrets unless explicitly allowed.
  • Add an extraction/generation path that uploads secrets and rewrites or canonicalizes config to secret refs.

Longer-term policy could require refs for deployable production config.

Acceptance criteria

  • Define a first-class SecretString / SecretRef config representation.
  • Separate redaction semantics from secret-store semantics in the settings model.
  • Identify initial secret-bearing fields and assign default secret keys.
  • Decide whether provider default secret store config lives under providers.fastly.secrets.
  • Add a runtime secret resolution phase before producing the final settings snapshot.
  • Ensure runtime fails closed for missing required secret refs.
  • Ensure canonical runtime config does not include generated/plaintext secret material when refs are used.
  • Decide whether inline secrets are allowed, warned, or rejected for provisioning.
  • Teach provisioning to plan/create missing generated secrets for supported fields.
  • Add docs explaining secret refs, default keys, generation behavior, and migration from inline values.

Open questions

  1. Should SecretString support inline values permanently, or only for migration/dev?
  2. Should DefaultRef be represented explicitly in TOML, or should omitted secret fields imply their default refs?
  3. Should ts config init emit explicit refs or omit generated-secret fields entirely?
  4. Should handler passwords be generated by default, opt-in, or never generated?
  5. Do handlers need a stable id field so default secret keys are not based on array indexes?
  6. Should secret refs participate in the application config hash? Secret values should not, but refs likely should.
  7. Should CLI upload a transformed canonical config with refs without rewriting the local authoring file?

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions