Skip to content

Latest commit

 

History

History
187 lines (124 loc) · 5.09 KB

File metadata and controls

187 lines (124 loc) · 5.09 KB

Coti Secret Manager: lightweight encrypted secrets for Docker services

Coti Secret Manager is a tiny Python utility for handling a single encrypted secret (e.g., a private key) stored as a JSON file, with the password supplied at runtime.

It’s designed for Docker environments where you want:

  • the encrypted secret JSON stored on a shared/bind volume (e.g. ./secrets/my-secret.json)
  • the password injected only at service startup
  • the password written to tmpfs inside the container (e.g. /run/secrets/coti_sk.pw)
  • the service to wait for the password, decrypt in memory, then delete the password file

Install

pip

Install from GitHub (example):

pip install "git+https://github.com/<you>/coti-secret.git@main"

Import name is coti_secret (underscores), even if the distribution name is coti-secret.


On the production service machine using docker-compose

Recommended production approach:

  • install nothing with pip on the host
  • use a tool container (part of your docker-compose.yml) to run encryption and password injection commands

You’ll typically have:

  • your main service container (e.g. my-docker-service)
  • a helper tool container (e.g. coti-secret-tool) with Python + coti_secret installed

Example compose snippet:

services:
  coti-secret-tool:
    image: ghcr.io/<you>/coti-secret-tool:latest
    volumes:
      - ./secrets:/keystore
    entrypoint: ["python", "-m", "coti_secret.cli"]

  my-docker-service:
    image: your-service-image:latest
    volumes:
      - ./secrets:/keystore:ro
    tmpfs:
      - /run/secrets:rw,noexec,nosuid,nodev
    environment:
      ENCRYPTED_FILE: /keystore/my-secret.json
      PW_FILE: /run/secrets/coti_sk.pw

CLI

local

Encrypt secret

Option A: run the CLI locally (if you installed with pip):

python -m coti_secret.cli encrypt --out ./secrets/my-secret.json
# paste secret via stdin, then enter password

Option B: run the CLI via the Docker tool container:

docker compose run --rm coti-secret-tool encrypt --out /keystore/my-secret.json
# paste secret via stdin, then enter password

The output file will be written to ./secrets/my-secret.json.


On production using docker-compose

Encrypt secret

Run encryption inside the tool container so the production host doesn’t need Python tooling installed:

docker compose run --rm coti-secret-tool encrypt --out /keystore/my-secret.json

You will:

  • paste the secret
  • enter the password (hidden)
  • the tool writes ./secrets/my-secret.json

Save the pw on tmpfs (startup)

Start the service:

docker compose up -d

Then inject the password into the running container’s tmpfs (stdin → file). Example:

docker compose exec -T my-docker-service sh -lc "umask 077; cat > /run/secrets/coti_sk.pw; chmod 600 /run/secrets/coti_sk.pw"
# then type/paste the password + Enter, Ctrl-D

Why tmpfs?
Deleting a file on tmpfs is meaningfully safer than attempting to “shred” a disk-backed file (journaling/COW/SSD wear-leveling make disk shredding unreliable).


Reading secrets

Python code

import os
from coti_secret import SecretManager, SecretType

encrypted_file = os.environ["ENCRYPTED_FILE"]
pw_file = os.environ.get("PW_FILE", "/run/secrets/coti_sk.pw")

# choose behavior by environment
st = SecretType.for_environment(os.getenv("ENVIRONMENT", "prod"), pw_file=pw_file)

secret_bytes = SecretManager.get(encrypted_file, secret_type=st)

# secret_bytes are now decrypted in memory
# pw_file is shredded+deleted (best-effort) after attempt

SecretManager.get():

  • waits until the password source is available (pw-file strategy)
  • decrypts into memory
  • deletes the pw file (best-effort shred + unlink)

Dev vs prod usage

prod (recommended)

  • Encrypted secret JSON lives on a volume: ./secrets/my-secret.json
  • Password is injected at startup into container tmpfs: /run/secrets/coti_sk.pw
  • SecretType.for_environment("prod") expects the password file (waits until it appears)

Pros: password is never stored on disk or in shared volumes.

dev (convenience)

In dev you may prefer a password in an env var:

export COTI_SK_PW="dev-password"
export ENVIRONMENT="dev"

In code:

from coti_secret import SecretManager, SecretType

st = SecretType.for_environment("dev", pw_env_var="COTI_SK_PW", delete_env_var_after_read=True)
secret = SecretManager.get("./secrets/my-secret.json", secret_type=st)

Warning: env vars are easier to leak into logs/process inspection and are generally weaker than tmpfs password injection.


Future: Using enclave

A strong next step for high-security production is to avoid password-based decryption entirely and move to an enclave-backed approach such as:

  • decrypting inside a hardware-backed environment (enclave/TEE)
  • using a cloud KMS with envelope encryption
  • using HSMs / hardware tokens so raw private keys never exist in service memory

This changes the model from “decrypt secret at startup” to “perform crypto operations without exporting the key.”