diff --git a/website/docs/3-concepts/7-configs.md b/website/docs/3-concepts/7-configs.md index b4a54922..959c22a9 100644 --- a/website/docs/3-concepts/7-configs.md +++ b/website/docs/3-concepts/7-configs.md @@ -234,3 +234,8 @@ If config changes don't take effect: 1. Run `uc deploy` to redeploy with new config content 2. Check that you're modifying the correct config file 3. Verify the config is properly mounted in the container with `docker exec cat ` on the remote machine + +## See also + +- [Secrets](8-secrets.md): Pass sensitive values such as passwords, tokens, and keys to your services +- [Compose support matrix](../8-compose-file-reference/1-support-matrix.md): Which Compose features Uncloud supports diff --git a/website/docs/3-concepts/8-secrets.md b/website/docs/3-concepts/8-secrets.md new file mode 100644 index 00000000..297a09f0 --- /dev/null +++ b/website/docs/3-concepts/8-secrets.md @@ -0,0 +1,146 @@ +# Secrets + +Secrets allow you to keep **sensitive data** for your services out of your Compose file and version control. This +includes database passwords, API tokens, TLS or SSH private keys. + +Uncloud provides an extended support for +[Compose secrets](https://github.com/compose-spec/compose-spec/blob/main/09-secrets.md), allowing you to fetch secret +values at deploy time from your password manager, a cloud secrets store, or local files. + +## How it works + +Two things go into your Compose file: + +1. Define a secret in the top-level `secrets` section. This tells Uncloud how to get the secret value. +2. Reference that secret value in a service's `environment` using the `secret://` format. This tells Uncloud to + resolve the secret and set the environment variable to its value at deploy time. + +```yaml title="compose.yaml" +services: + api: + image: myapp:latest + environment: + DB_PASSWORD: secret://db_password + TLS_KEY: secret://tls_key + +secrets: + db_password: + # Read the secret value from 1Password by running this command locally. + x-command: op read "op://prod/myapp/db_password" + tls_key: + # Read the secret value from a local file. Do NOT commit this file to the repository. + file: ./secrets/tls_key.pem +``` + +When you run `uc deploy`, it sees each `secret://` reference, resolves the secret on your local machine, and sets +the environment variable to its value. For example, it runs the command for `db_password` and reads the file for +`tls_key`. The values never land in your Compose file or git. They go straight into the deployment. + +Only secrets you reference in the services that you currently deploy get resolved. A secret referenced in multiple +places or by several services resolves only once, and the value is reused. + +Secrets are resolved after building images (if any) and before preparing the deployment plan. + +:::info note + +Secrets can only be used to set environment variables in your services. \ +Mounting a secret as a file inside the container is not supported yet. For mounting config files, +see [Configs](7-configs.md). + +::: + +## Secret sources + +Uncloud supports multiple sources for fetching secret values. Choose the one for each secret that fits your workflow. + +### Arbitrary command + +The `x-command` extension runs a command and uses its output as the secret value. It's the most versatile option that +lets you pull a secret from a system Keychain, password managers, cloud services and secrets stores, or any CLI tool you +already use. + +```yaml title="compose.yaml" +secrets: + # 1Password + db_password: + x-command: op read "op://prod/myapp/db_password" + # Bitwarden + api_token: + x-command: bw get password api-token + # Infisical + smtp_password: + x-command: infisical secrets get --env=prod SMTP_PASSWORD --plain --silent + # AWS Secrets Manager + stripe_key: + x-command: aws secretsmanager get-secret-value --secret-id stripe --query SecretString --output text +``` + +The command runs on the machine where you run `uc deploy`, in the same directory as your Compose file and with the same +environment. So it uses the CLIs and credentials you are already logged into. + +If your password manager needs you to unlock it or touch a hardware key, the prompt shows up right in your terminal. +Uncloud never stores your provider credentials. + +A few details worth knowing: + +- The command runs directly without a shell. To use pipes or variables, call a shell yourself: `sh -c 'cmd1 | cmd2'`. +- A single trailing newline is trimmed from the output, because most CLI tools add one. Everything else is kept as is. +- The command has 1 minute to finish before `uc` times out and aborts the deployment. + +`x-command` is a short form for `driver: exec`. Use the long form if you prefer to be explicit: + +```yaml title="compose.yaml" +secrets: + db_password: + driver: exec + driver_opts: + command: op read "op://prod/myapp/db_password" +``` + +:::tip + +To pull secrets from many providers through a single CLI, or to keep **encrypted secrets in your git repository**, +consider [fnox](https://fnox.jdx.dev/). It works with AWS, GCP, Azure, HashiCorp Vault, 1Password, Bitwarden, and more, +and prints a plain value you can read with `x-command`: + +```yaml title="compose.yaml" +secrets: + db_password: + x-command: fnox get DB_PASSWORD +``` + +::: + +### File + +Read the content of a local file as the secret value. The file path is relative to the Compose file location. + +```yaml title="compose.yaml" +secrets: + db_password: + file: ./secrets/db_password.txt +``` + +The file content is used verbatim without trimming any whitespaces. + +:::warning + +Do NOT commit these files to your repository and ensure they are protected with proper file permissions. For example, +make the file readable only by your user with `chmod 600 ./secrets/db_password.txt`. + +::: + +## Things to keep in mind + +- Secrets resolve on the machine where you run `uc deploy`, not on the cluster. The cluster only receives the final + value. +- Secret values passed as environment variables are stored **unencrypted** as part of the service specification in the + distributed cluster store. +- Docker also stores the resolved environment variables **unencrypted** in each container's configuration at + `/var/lib/docker/containers//config.v2.json` on the machine running the container. Anyone with `root` access to + that machine or able to run `docker inspect` can read them. + +## See also + +- [Configs](7-configs.md): Mount non-sensitive configuration files into your containers +- [Compose support matrix](../8-compose-file-reference/1-support-matrix.md): Which Compose features Uncloud supports diff --git a/website/docs/8-compose-file-reference/1-support-matrix.md b/website/docs/8-compose-file-reference/1-support-matrix.md index fd5494c9..47caebaf 100644 --- a/website/docs/8-compose-file-reference/1-support-matrix.md +++ b/website/docs/8-compose-file-reference/1-support-matrix.md @@ -42,7 +42,7 @@ If you rely on a specific Compose feature that is not supported by Uncloud, plea | `ports` | ⚠️ Limited | `mode: host` only, use [`x-ports`](2-extensions.md#x-ports) for HTTP/HTTPS | | `privileged` | ✅ Supported | Run containers in privileged mode | | `pull_policy` | ✅ Supported | `always`, `missing`, `never` | -| `secrets` | ❌ Not supported | Use configs or environment variables | +| `secrets` | ⚠️ Limited | Reference secrets in `environment`, see [Secrets](../3-concepts/8-secrets.md). File mounts not supported | | `security_opt` | ❌ Not supported | | | `shm_size` | ✅ Supported | Shared memory size | | `stop_grace_period` | ✅ Supported | Time to wait after SIGTERM before SIGKILL | diff --git a/website/docs/8-compose-file-reference/2-extensions.md b/website/docs/8-compose-file-reference/2-extensions.md index 9ebfda5d..b7516cfb 100644 --- a/website/docs/8-compose-file-reference/2-extensions.md +++ b/website/docs/8-compose-file-reference/2-extensions.md @@ -117,3 +117,23 @@ The hook container also gets `UNCLOUD_HOOK_PRE_DEPLOY=true` environment variable See [Pre-deploy hooks](../4-guides/1-deployments/5-pre-deploy-hooks.md) for more details, usage examples, and failure handling. + +## `secrets.*.x-command` + +Resolve a secret by running a command locally and using its output as the value. Define a secret under the top-level +`secrets` section, then reference it as `secret://` in a service's `environment`: + +```yaml +services: + api: + image: myapp + environment: + DB_PASSWORD: secret://db_password + +secrets: + db_password: + # Read the secret value from 1Password by running this command locally. + x-command: op read "op://prod/myapp/db_password" +``` + +See [Secrets](../3-concepts/8-secrets.md) for more details and supported secret sources.