Sign inSign up

cplieger/docker-age

By cplieger

Updated about 4 hours ago

Decrypt age-encrypted .env files in your repo at deploy time — Go binary on distroless

Image
Security
Integration & delivery
0

10K+

cplieger/docker-age repository overview

docker-age

Decrypt age-encrypted .enc files to their plaintext siblings at deploy time so your orchestrator can read them: .env files, any other config, or a single file piped through stdin/stdout. Ciphertext stays tracked in git; plaintext is generated next to it and never committed.

What it does

Walks a mounted directory tree (or a single .enc file you name), finds every <name>.enc ciphertext source (binary or armored age format), and atomically writes its decrypted plaintext to the sibling <name>: apps/x/.env.enc becomes apps/x/.env. The source is never modified, so your working tree stays clean: git pull always applies rotated secrets, and the generated plaintext is just re-derived on the next pass. An --ext filter narrows a walk by the OUTPUT suffix (--ext .env selects .env.enc sources); a - target switches to a stdin-to-stdout pipe for a single file. Designed to run as a pre_deploy step before docker compose up reads the files.

The age-decrypt binary is a single static Go executable on gcr.io/distroless/static:nonroot:

  • decrypt --ext .env: decrypt every .env.enc in REPO_ROOT to its .env sibling (the deploy use case)
  • decrypt /path: decrypt a specific .enc file or every .enc source under a directory tree
  • decrypt -: pipe, stdin ciphertext in, stdout plaintext out
  • health: file-based health probe for Docker HEALTHCHECK

The decrypt subcommand always requires you to say what to decrypt (an extension filter, a path, or -). Server mode (no subcommand) is the always-on container entrypoint that idles as a docker exec target (see the Server mode note under Subcommands for details).

Why this design
  • Ciphertext and plaintext are separate planes: <name>.enc is tracked in git and never touched; <name> is generated and gitignored. Your compose file references apps/<x>/.env like usual, git status stays meaningful on live checkouts, and a git pull can never conflict with a decrypted secret (the failure mode of in-place rewriting, which v2 used)
  • Fail-closed: a .enc source that will not decrypt, an unreadable subtree, or stray age ciphertext sitting at a plaintext path (an un-migrated secret) all exit non-zero and block the deploy; ciphertext can never be silently consumed as config
  • Multi-identity: the key file may hold several identities (one per line); a file encrypted to any one of them decrypts, so key rotation is just adding the new key alongside the old
  • Concurrency-safe: parallel invocations on the same stable tree won't collide, so simultaneous deploys are safe
  • Atomic: a failed decrypt never leaves a half-written .env, and a source can never be corrupted (it is opened read-only)
  • Scoped source reads: symlinks, hardlinks, FIFOs, devices, and directories are rejected as sources, and pathname resolution is confined to the mounted tree; under --ext any matching nonregular plaintext path fails the pass
  • Distroless + nonroot: minimal attack surface; no shell, no package manager, no extra binaries
  • Per-file bounds: each encrypted input is capped at 10 MB and each decrypted output at 1 MB; plaintext is published mode 0600
  • File-based health marker: works with Docker's no-shell distroless healthcheck (HEALTHCHECK CMD ["/age-decrypt", "health"])

Pull

docker pull cplieger/docker-age:latest

Also published to ghcr.io/cplieger/docker-age with identical images and tags. Release versions are tagged vX.Y.Z alongside latest.

Quick start

# Example compose for docker-age. See the README for all configuration options and hardening.
# Trigger (.env.enc -> .env): docker exec age /age-decrypt decrypt --ext .env
services:
  age:
    image: ghcr.io/cplieger/docker-age:latest
    container_name: age
    restart: unless-stopped  # always-on: stays up between deploys as an exec target

    environment:
      IDENTITY_PATH: "/age/keys.txt"  # required; age identity file

    volumes:
      - "/path/to/age-keys:/age:ro"  # dir holding the age identity (keys.txt)
      - "/path/to/repo:/repo"  # tree to decrypt (or a folder of repos)

Documentation

License

Apache-2.0. See LICENSE.

The image carries the license text of every bundled component under /usr/share/licenses/, including the BSD-3-Clause text of the age library it links.

Tag summary

Content type

Image

Digest

sha256:57750acd4

Size

3 MB

Last updated

about 4 hours ago

docker pull cplieger/docker-age