| name | secrets |
| description | Manage secrets for a Kamal deployment — the `.kamal/secrets` file, dotenv variable and command substitution, and the `kamal secrets` vault helpers (1Password, Bitwarden, LastPass, Bitwarden Secrets Manager, AWS Secrets Manager, Doppler, GCP Secret Manager, Passbolt). Use when the user says "kamal secrets," "where do I put my registry password," "manage secrets," "pull secrets from 1Password/Bitwarden/LastPass/Doppler," "kamal secrets fetch/extract/print," "secrets-common," "secrets.<destination>," "secrets_path," "secret vs clear env keys," "RAILS_MASTER_KEY," or "migrate from .env / kamal envify," "migrate local or file-based secrets into 1Password / a vault," or "move plaintext secret files into a password manager." Covers wiring secrets into `env.secret`/`env.clear` in deploy.yml, per-destination secrets files, and aliased secrets. For setting plain non-secret env values, see env. For registry login and KAMAL_REGISTRY_PASSWORD, see registry. For build-time secrets and args, see build. |
| metadata | {"version":"1.0.0"} |
Kamal Secrets
You are an expert in deploying applications with Kamal. Your goal is to help the user store deployment secrets safely, wire them into config/deploy.yml, and — when they use a password manager — pull them with the kamal secrets helpers instead of pasting plaintext.
In Kamal 2, secrets live in .kamal/secrets. Kamal uses dotenv to load that file automatically whenever a command needs the values, so there is no separate "generate secrets" step.
Start Here
Before asking the user questions, read what already exists:
.kamal/secrets (and .kamal/secrets-common, .kamal/secrets.<destination> if present) — what secrets are already defined and how they are sourced.
config/deploy.yml — the env block, especially which keys are listed under secret vs clear, and the registry block.
- Check whether
.kamal/secrets is git-ignored.
This tells you whether the user needs to add a new secret, move a plaintext value out of deploy.yml, switch to a vault helper, or set up per-environment secrets.
The .kamal/secrets File
.kamal/secrets is a dotenv file. Each line defines an environment variable that Kamal can reference. The right-hand side supports two kinds of substitution, evaluated on demand when you run a Kamal command:
| Technique | Syntax | Use it for |
|---|
| Variable substitution | NAME=$NAME | Pulling a value already in your shell/CI environment |
| Command substitution | NAME=$(command) | Reading from a file or a CLI (e.g. a password manager) |
# .kamal/secrets
# Pass a value through from the surrounding environment (e.g. set in CI)
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
# Read a value by running a command
RAILS_MASTER_KEY=$(cat config/master.key)
Do not commit real secret values. If you store secrets directly in .kamal/secrets, make sure the file is not checked into version control. Prefer variable substitution (from CI/your shell) or command substitution (from a password manager) so the file holds references, not plaintext.
KAMAL_REGISTRY_PASSWORD is the variable Kamal reads for registry authentication. For registry login itself, see the registry skill.
Referencing Secrets From deploy.yml
Defining a variable in .kamal/secrets does not, by itself, send it to your containers. You declare which variables are secret in the env block of config/deploy.yml.
List secret variable names under the secret key, and move any plain values under the clear key:
env:
clear:
DB_USER: app
secret:
- DB_PASSWORD
The names under secret must match variables defined in .kamal/secrets.
Behavior: Unlike clear values, secrets are not passed directly to the container. Kamal writes them to an env file on the host instead. For the full breakdown of clear, secret, and tags, see the env skill.
Aliased Secrets
When the environment variable name your app expects differs from the secret name, alias it with a : separator (ENV_NAME:SECRET_NAME). This is useful when the same ENV name needs different values in different contexts:
env:
secret:
- DB_PASSWORD:MAIN_DB_PASSWORD
tags:
secondary_db:
secret:
- DB_PASSWORD:SECONDARY_DB_PASSWORD
# .kamal/secrets
SECRETS=$(kamal secrets fetch ...)
MAIN_DB_PASSWORD=$(kamal secrets extract MAIN_DB_PASSWORD $SECRETS)
SECONDARY_DB_PASSWORD=$(kamal secrets extract SECONDARY_DB_PASSWORD $SECRETS)
Pulling Secrets From a Vault: kamal secrets
If the user keeps secrets in a password manager, use the kamal secrets helpers inside .kamal/secrets via command substitution. The helpers handle signing in, prompting for passwords, and efficiently fetching the secrets.
Subcommands
| Command | What it does |
|---|
kamal secrets fetch [SECRETS...] --adapter=ADAPTER --account=ACCOUNT | Fetch secrets from a vault |
kamal secrets extract | Extract a single secret from the results of a fetch call |
kamal secrets print | Print the secrets (for debugging) |
kamal secrets help [COMMAND] | Describe subcommands or one specific subcommand |
Key options on fetch: --adapter (-a) selects the password manager, --account selects the account, and --from scopes where to look (vault, folder, item, or project) depending on the adapter.
The fetch → extract Pattern
Call fetch once to pull every secret in a single sign-in, then extract each value out of that result. This avoids re-authenticating for every variable:
# .kamal/secrets
SECRETS=$(kamal secrets fetch ...)
REGISTRY_PASSWORD=$(kamal secrets extract REGISTRY_PASSWORD $SECRETS)
DB_PASSWORD=$(kamal secrets extract DB_PASSWORD $SECRETS)
Walk-Through: 1Password
- Install and configure the 1Password CLI.
- Fetch with the
1password adapter, pointing --from at a Vault/Item:
# .kamal/secrets
SECRETS=$(kamal secrets fetch --adapter 1password --account myaccount \
--from MyVault/MyItem REGISTRY_PASSWORD DB_PASSWORD)
REGISTRY_PASSWORD=$(kamal secrets extract REGISTRY_PASSWORD $SECRETS)
DB_PASSWORD=$(kamal secrets extract DB_PASSWORD $SECRETS)
extract accepts either the short secret name or a fully-qualified path, so all three of these resolve the same value:
kamal secrets extract REGISTRY_PASSWORD $SECRETS
kamal secrets extract MyItem/REGISTRY_PASSWORD $SECRETS
kamal secrets extract MyVault/MyItem/REGISTRY_PASSWORD $SECRETS
1Password: Interactive vs. Non-Interactive (CI) Auth
The op desktop-app integration authorizes reads transparently, but writes — creating
a vault or item — prompt for Touch ID / approval and will time out in non-interactive,
CI, or background contexts. For unattended deploys, authenticate with a service account
instead of --account:
# .kamal/secrets
export OP_SERVICE_ACCOUNT_TOKEN=$(cat .kamal/OP_SERVICE_ACCOUNT_TOKEN) # keep this file git-ignored
SECRETS=$(kamal secrets fetch --adapter 1password \
--from MyVault/MyItem REGISTRY_PASSWORD DB_PASSWORD)
A service account only sees vaults explicitly shared with it — grant it read access to the
deploy vault first, or fetch comes back empty.
Field references and extract: op item create "LABEL[password]=..." files the field under
an auto-generated section, so its reference becomes op://MyVault/MyItem/Section_xxxx/LABEL.
That still resolves — the adapter fetches by label=, and kamal secrets extract LABEL matches
on the key suffix (/LABEL), so neither depends on the section name.
Supported Adapters
Kamal ships helpers for several password managers and secret stores:
| Adapter | Service | Uses --account? |
|---|
1password | 1Password | Yes |
lastpass | LastPass | Yes (email) |
bitwarden | Bitwarden | Yes (email) |
bitwarden-sm | Bitwarden Secrets Manager | No (fetch all or ProjectID/all) |
aws_secrets_manager | AWS Secrets Manager | Yes (AWS CLI profile, usually default) |
doppler | Doppler | Ignored if given |
gcp | GCP Secret Manager | Yes (gcloud account) |
passbolt | Passbolt | Ignored if given |
Each adapter has its own --from semantics (vaults, folders, items, projects, GCP project IDs) and prefix conventions. For per-adapter fetch examples and the full options reference, see references/vault-adapters.md.
Secrets For Multiple Environments
When you deploy to multiple destinations, split secrets across files. Kamal looks for the -common file first, then the environment-specific file, with later values overriding earlier ones:
| Scenario | Files read (in order) |
|---|
| No destination | .kamal/secrets-common, then .kamal/secrets |
Destination <dest> | .kamal/secrets-common, then .kamal/secrets.<dest> |
When a destination is selected, the plain .kamal/secrets file is not read. Put values shared across every destination in .kamal/secrets-common, and per-environment values in .kamal/secrets.<destination>.
Changing the Path: secrets_path
secrets_path sets the base path to your secrets files. It defaults to .kamal/secrets. Kamal applies the same -common / .<destination> lookup relative to whatever you set:
secrets_path: /user_home/kamal/secrets
Debugging
Use kamal secrets print to print the resolved secrets when something is not being picked up. Treat its output as sensitive — it reveals secret values.
kamal secrets print
Migrating File-Based Secrets Into a Vault
When secrets currently live in git-ignored files (or as plaintext in .kamal/secrets) and the
user wants them in a password manager, move them without changing what gets deployed (the
op commands below are 1Password; other managers use their own item-creation commands):
-
Create the vault if it doesn't exist yet. Writes may need interactive approval (see the
1Password auth note above); a service-account or API token avoids the prompt.
op vault create MyVault
-
Create the item, loading each value straight from its file — so the plaintext never
lands in your shell output, and trailing newlines are stripped exactly as $(cat file)
already does:
op item create --vault MyVault --title MyItem --category "Secure Note" \
"REGISTRY_PASSWORD[password]=$(cat .kamal/REGISTRY_PASSWORD)" \
"DB_PASSWORD[password]=$(cat .kamal/DB_PASSWORD)"
-
Rewire .kamal/secrets to the fetch → extract pattern, and keep the old file-based lines
commented out for a quick rollback.
-
Verify without revealing values — fetch once, then compare checksums, not plaintext:
SECRETS=$(kamal secrets fetch --adapter 1password --from MyVault/MyItem DB_PASSWORD)
diff <(printf %s "$(kamal secrets extract DB_PASSWORD "$SECRETS")" | shasum) \
<(printf %s "$(cat .kamal/DB_PASSWORD)" | shasum)
-
Confirm the whole config resolves with kamal config (expect exit code 0), then delete
the now-redundant files. Keep config/credentials/*.key if you still edit Rails credentials
locally.
Migrating From Kamal 1
If the user is upgrading from Kamal 1, secrets have moved out of .env / .env.rb into .kamal/secrets:
kamal envify and kamal env have been removed. Secrets no longer have a separate lifecycle — substitution happens on demand. Replace envify with dotenv's variable and command substitution directly in .kamal/secrets.
.env is no longer auto-loaded into the environment, and values in .kamal/secrets are not loaded into the environment for use in deploy.yml either. If your deploy.yml referenced ENV[...] via ERB, load .env yourself:
# config/deploy.yml
<% require "dotenv"; Dotenv.load(".env") %>
servers:
- <%= ENV["SERVER_IP"] %>
Related Skills
- env: For setting plain (non-secret) env values, the
clear / secret keys in detail, and tags.
- registry: For registry login/logout and the
KAMAL_REGISTRY_PASSWORD secret it consumes.
- build: For build-time secrets and build args used while building your image.
Official docs: Secrets command · Secrets changes (upgrading) · Environment variables