# Config/Secret Management Design **Author**: @aat ## Description This feature will support registration and retrieval of secrets and configuration keys for FTL modules, and tooling for the creation, retrieval, and enumeration of same. ## Glossary "Sensitive" secrets : In local development, a "sensitive" secret (which sounds odd) is one that shouldn't be persisted to source control. ## Goals - Provide simple configuration/secret management - Securely support secret handling during development and production. - Support bootstrapping configuration/secret defaults from the repository. - Support "sensitive" secrets during development (eg. actual API keys) ## Non-goals - We won't support dynamic secret/configuration updates initially. ## Design Most resources in FTL are defined declaratively in end-user code, then FTL itself takes care of provisioning them. Configuration and secrets are distinct however, in that declaring them simply specifies that they should exist, but the creation of the secret or configuration value itself must occur out of band. This constraint necessitates a design that is somewhat different from anything else in FTL. In order to support bootstrapping configuration and secret defaults when cloning an FTL project, we'll introduce a new configuration file `ftl-project.toml` that will be searched for using similar semantics to that of `.gitignore` files. Typically there would be a single file per repository. This file will contain a mapping from configuration and secret keys to URIs defining where the value is stored. In the `inline://` case, the value will be stored directly in the TOML file. The format is described in detail [below](#The-ftl-devtoml-format). Sensitive secrets that should not be persisted in Git will be stored in the system keychain. Finally, in production, configuration will be loaded from the FTL database, and secrets may be loaded from an existing cloud secrets manager such as ASM. These requirements imply that configuration and secrets will need a layered approach, where FTL is able to store and retrieve keys from different locations. Each configuration/secret store will have its own identifier, with a default storage provider selected based on deployment environment. Here's a hypothetical interaction with the proposed FTL CLI using these different providers: ``` $ ftl secret set --help Usage: ftl secret set <module> <id> Set a secret in the FTL cluster. The secret will be read from stdin. Arguments: <module> Module the secret resides in. <id> The identifier for the secret. Flags: -h, --help Show context-sensitive help. --keychain Use the system keychain (default). --1password Use 1Password secret reference. --git Store secrets in Git (not secure). $ ftl secret get githubAccessToken ftl: error: no such secret $ ftl secret set module githubAccessToken ASDFASDF ^D $ ftl secret get module githubAccessToken ASDFASDF $ ftl secret set --inline module encryptionKey ftl: warning: this secret storage method is not secure, do not store any sensitive data notASensitiveSecret ^D $ ftl secret set --1password module companyApiKey op://devel/yj3jfj2vzsbiwqabprflnl27lm/companyApiKey ``` ### The `ftl-project.toml` format Here's an example of the configuration file that might be created by the commands above: ```toml [modules.module.configuration] githubAccessToken = "keychain://githubAccessToken" [modules.module.secrets] encryptionKey = "inline://notASensitiveSecret" companyApiKey = "op://devel/yj3jfj2vzsbiwqabprflnl27lm/companyApiKey" ``` ### CLI Changes A group of commands will be added under `ftl config`: ``` ftl config get <module> <key> ftl config set <module> <key> <value> ftl config delete <module> <key> ftl config list [<module> ...] ``` And `ftl secret`: ``` ftl secret set <module> <key> ftl secret delete <module> <key> ftl secret list [<module> ...] ``` `ftl secret set` will prompt for a secret value. Note that there is no mechanism to retrieve secrets. This is deliberate to reduce the chance of secrets leaking, but may be revisited once FTL gets RBAC. ### Database changes We'll add a new database table for configuration values: ```sql -- Proto-encoded schema.Type CREATE DOMAIN schema_type_pb BYTEA; CREATE TABLE module_configuration ( id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc'), module_id BIGINT NOT NULL REFERENCES modules (id) ON DELETE CASCADE, "name" VARCHAR UNIQUE NOT NULL, "schema" schema_type_pb NOT NULL, -- JSON-encoded value. If NULL, the configuration key hasn't been set. value JSONB NULL ); ``` ### Controller changes Two new gRPC services will be created that the Controller will implement: 1. `ConfigService` will manage configuration using the available configuration providers. 2. `SecretService` will manage secrets using the available secret providers. Only `ConfigService` is shown below, as `SecretService` is identical except for identifier names. ```protobuf message ConfigKey { string module = 1; string key = 2; } message ConfigValue { ConfigKey key = 1; schema.Type schema = 2; google.protobuf.Struct value = 3; } message SetConfigRequest { string provider = 1; ConfigValue value = 2; } message SetConfigResponse {} message GetConfigRequest { ConfigKey key = 1; } message GetConfigResponse { ConfigValue value = 1; } message GetMultipleRequest { string module = 1; } message GetMultipleResponse { repeated ConfigValue values = 1; } message DeleteConfigRequest { string module = 1; string key = 2; } message DeleteConfigResponse {} message EnumerateConfigRequest { repeated string module = 1; } message EnumerateConfigResponse { repeated ConfigValue values = 1; } // A service for storing and retrieving configuration. service ConfigService { rpc SetConfig(SetConfigRequest) returns (SetConfigResponse); rpc GetConfig(GetConfigRequest) returns (GetConfigResponse); rpc DeleteConfig(GetConfigRequest) returns (SetConfigResponse); rpc EnumerateConfig(EnumerateConfigRequest) returns (EnumerateConfigResponse); } ``` ### Runner changes The Runner will be modified such that when a `Deploy()` request is received, the Runner will: 1. Retrieve all secrets and configuration values for the module being deployed, by issuing an `EnumerateConfig()` and an `EnumerateSecrets()` call to the Controller, respectively. 2. Encode each configuration value into an environment variable for the module being executed, in the following format: ```shell FTL_CONFIG_{{ .Key.Module|hex }}_{{ .Key.Name|hex }}={{ .Value|json|shellquote }} ``` 3. Encode each secret into an environment variable for the module being executed, in the following format: ```shell FTL_SECRET_{{ .Key.Module|hex }}_{{ .Key.Name|hex }}={{ .Value|json|shellquote }} ``` ### Runtime changes :::info **Note:** the Go and Kotlin runtimes are already retrieving configuration and secret values from environment variables. ::: The API currently looks like this: ```go var githubUsername = sdk.Config[string]("githubUsername") var githubToken = sdk.Secret[string]("githubToken") ``` Each runtime's schema extraction code will be updated to recognise these declarations, and create entries in the schema (described [below](#Schema-changes)). ### Schema changes Configuration and secrets are privately module scoped, like all other resources. We will add module-level stanzas for each type, with something like the following syntax: ``` config <identifier> <type> secret <identifier> <type> ``` eg. ``` module exemplar { config githubUsername String secret githubToken String } ``` In addition we will add reference tracking to verbs, though perhaps in a followup change. ``` verb getGitHubArtefact(Request) Response { verb someOtherVerb config githubUsername secret githubToken } mod