kage
kage stores Kubernetes Secret values in an
age-encrypted file and synchronizes them
to a Kubernetes namespace.
It is intentionally small and opinionated. For a more general and powerful
secret-management system—with correspondingly more configuration and
complexity—see SOPS.
Installation
Install the latest version with Go:
go install github.com/paulgmiller/kage@latest
Make sure Go's binary directory (normally $(go env GOPATH)/bin) is on your
PATH.
Kage currently uses ~/.ssh/id_ed25519 to decrypt secret files. Applying
secrets also requires a working Kubernetes configuration at
~/.kube/config.
The decrypted file is dotenv-like. Start each Kubernetes Secret with a
#secret:<name> header:
#secret:api
API_TOKEN=replace-with-a-secret
DATABASE_URL="postgres://user:password@example/db"
#secret:worker
QUEUE_TOKEN=replace-with-another-secret
Secret names must be valid Kubernetes DNS subdomains. Keys may not be
duplicated within a secret, and values must contain at least five characters.
The file passed to kage must be encrypted with age. To allow kage to update
or re-encrypt it, place a recipients.txt file in the same directory:
# One age or SSH recipient per line
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
For example, with the age command installed:
age -R secrets/recipients.txt -o secrets/envtest secrets/envtest.plaintext
Remove the plaintext copy securely after confirming that the encrypted file
can be read.
Usage
Kage uses focused subcommands for each operation. The default encrypted file is
secrets/envtest; select another file with the persistent --secret-file (or
-f) option.
Inspect secret names and masked values:
kage check --secret-file secrets/envtest
Preview changes to existing secrets in a namespace:
kage apply --secret-file secrets/envtest --namespace my-namespace
Apply changes:
kage apply --secret-file secrets/envtest --namespace my-namespace --confirm
Add or update a value in the encrypted file:
kage set --secret-file secrets/envtest 'api/API_TOKEN=new-secret-value'
Create an encrypted file from every Secret currently in a namespace:
kage create --secret-file secrets/envtest --namespace my-namespace
This reads the encryption recipients from secrets/recipients.txt, just like
set and reencrypt. Secret names and keys are sorted so that decrypting
repeated exports produces stable plaintext. The command refuses to create an
invalid kage file, including when the namespace contains no Secrets or a value
shorter than five characters.
If the encrypted file does not exist, set creates it using the adjacent
recipients.txt. This is the simplest way to start a new file:
mkdir -p secrets
cp ~/.ssh/id_ed25519.pub secrets/recipients.txt
kage set --secret-file secrets/envtest 'api/API_TOKEN=new-secret-value'
Re-encrypt the file using its adjacent recipients.txt, for example after
changing the recipient list:
kage reencrypt --secret-file secrets/envtest
Show all command-line options:
kage --help
Kage creates opaque Kubernetes Secrets and marks them with the
managed-by: github.com/paulgmiller/kage annotation.
Loading secrets locally
Applications written in Go can use kage.Load() similarly to
godotenv.Load(). It first loads .env,
then decrypts and loads secrets/envtest when ~/.ssh/id_ed25519 matches a
recipient used to encrypt the file. When called from a subdirectory, Kage looks
for the encrypted file in each parent directory through the Git root:
package main
import (
"log"
"os"
"github.com/paulgmiller/kage/pkg/kage"
)
func main() {
if err := kage.Load(); err != nil {
log.Fatal(err)
}
apiToken := os.Getenv("API_TOKEN")
_ = apiToken
}
Select a different encrypted file with the optional argument or the
KAGE_SECRET_FILE environment variable. An argument takes precedence over the
environment variable:
if err := kage.Load("secrets/development"); err != nil {
log.Fatal(err)
}
Relative configured paths use the same upward search. Absolute paths are used
exactly as supplied.
Like godotenv, kage.Load() does not overwrite environment variables that are
already set. This makes it possible to use ordinary values from .env and
encrypted local values from secrets/envtest, while allowing the shell or CI
environment to take precedence.
End-to-end test
The end-to-end test requires Docker on a Linux host. It builds an isolated
test image, creates a kind cluster, and verifies two Secrets:
./test/e2e.sh
Without --confirm, kage apply is a dry run: it reports both creations and
updates without changing the target namespace.