Skip to main content

Git-friendly encrypted .env files with cleartext keys and sealed values (SOPS-inspired structural encryption).

Project description

dotseal

Git-friendly encrypted .env files with cleartext keys and sealed values — an offline-first environment-variable manager for Python, inspired by Mozilla SOPS but built natively for the Python ecosystem.

dotseal performs structural encryption: it leaves your .env keys in cleartext and encrypts only the values. The result is a .env.enc file you can safely commit, review in pull requests, and merge — because the diff still shows which variables changed, just not their secret contents.

  DATABASE_URL=ENC[AES_GCM,data:Zm9vYmFy...]
- DEBUG=ENC[AES_GCM,data:TXVzaWM=]
+ DEBUG=ENC[AES_GCM,data:b3RoZXI=]
  API_KEY=ENC[AES_GCM,data:c2VjcmV0...]
  • No OS dependencies. Pure Python on top of cryptography. No age, gpg, sops, openssl CLI, or Go binaries required.
  • Authenticated encryption. AES-256-GCM (AEAD) with a fresh nonce per value.
  • Tamper-evident & swap-proof. Each value is bound to its variable name as Additional Authenticated Data (AAD), so ciphertext can't be moved between keys.
  • Runtime loader. Decrypt straight into os.environ — no cleartext file ever touches disk.

Installation

pip install dotseal

Requires Python 3.8+.


Quickstart

# 1. Generate a master key (saved to .dotseal.key and gitignored)
dotseal init

# 2. Write a normal .env file
cat > .env <<'EOF'
DATABASE_URL=postgres://user:pass@localhost:5432/db
DEBUG=True
API_KEY=super-secret
EOF

# 3. Encrypt it → .env.enc (commit this; never commit .env or the key)
dotseal encrypt

# 4. Decrypt when you need it back
dotseal decrypt

What gets committed?

File Commit it? Contents
.env.enc ✅ Yes Keys in cleartext, values encrypted
.env ❌ No Full cleartext secrets
.dotseal.key Never The master key (auto-added to .gitignore)

CLI Reference

dotseal init

Generates a new cryptographically secure master key, writes it to .dotseal.key (mode 0600), and adds it to .gitignore (creating one if needed). Prints the key fingerprint (not the key) so you can verify which key encrypted a file. Use --force to replace an existing key (this makes existing .env.enc files undecryptable).

dotseal encrypt [input] [output]

Encrypts the values of a cleartext env file. Defaults: .env.env.enc. Idempotent — values that are already encrypted are left untouched.

dotseal decrypt [input] [output]

Decrypts values back to cleartext. Defaults: .env.enc.env. The output is written with owner-only (0600) permissions since it contains secrets.

dotseal edit [file]

SOPS-style editing. Decrypts .env.enc to a temporary file (mode 0600), opens it in $EDITOR (falling back to nano), and re-encrypts on save. The temp file is securely overwritten and deleted afterward. If the file doesn't exist yet, you get a fresh template to start from.

Common options

All commands except init accept:

  • -k, --key <base64> — provide the master key directly (overrides env var and key file).
  • --key-file <path> — use a specific key file instead of auto-discovery.

Key Management

The master key is resolved in this order (first match wins):

  1. An explicit --key argument (CLI) or master_key= argument (loader).
  2. The DOTSEAL_MASTER_KEY environment variable.
  3. A local .dotseal.key file (searched for in the current directory and upward through parent directories).

The key is a base64-encoded 32-byte (AES-256) value. Generate one programmatically with:

from dotseal import generate_master_key
print(generate_master_key())

Runtime Loader (no cleartext on disk)

load_env is a drop-in replacement for python-dotenv's load_dotenv — it just reads an encrypted .env.enc instead of a cleartext .env. Call it once at startup and your secrets are available as ordinary environment variables through the os module:

import os
from dotseal import load_env

# Resolves the key from DOTSEAL_MASTER_KEY or .dotseal.key
load_env()                                # reads ".env.enc" by default

os.getenv("DATABASE_URL")                 # now available, like any env var

Signature:

def load_env(
    dotenv_path: str = ".env.enc",
    *,
    master_key: str | None = None,
    override: bool = False,
    encoding: str = "utf-8",
) -> bool:
    ...
  • override=False (default): existing process env vars win (12-factor friendly).
  • override=True: decrypted values overwrite anything already in os.environ.
  • Returns True if at least one variable was set (matching load_dotenv). Want the values as a dict instead? Use decrypt_to_dict (below).

Other programmatic helpers:

from dotseal import encrypt_text, decrypt_text, decrypt_to_dict, load_key_bytes

key = load_key_bytes("BASE64KEY==")
enc = encrypt_text("FOO=bar\n", key)      # -> ".env.enc" text
cleartext = decrypt_text(enc, key)        # -> ".env" text
mapping = decrypt_to_dict(enc, key)       # -> {"FOO": "bar"}

File Format

# Generated by dotseal. DO NOT EDIT VALUES MANUALLY.
DATABASE_URL=ENC[AES_GCM,data:<base64(nonce ‖ ciphertext ‖ tag)>]
DEBUG=ENC[AES_GCM,data:...]
# dotseal: v=1 alg=AES_GCM key_fp=7ef08b59e6a945e4
  • Each value's payload is base64(12-byte nonce ‖ ciphertext ‖ GCM tag).
  • The variable name is bound as AAD, so values cannot be swapped between keys.
  • The trailing # dotseal: metadata line records the algorithm and a key fingerprint (a one-way hash of the key). On decrypt, the fingerprint is checked first so a wrong key fails fast with a clear message instead of a cryptic crypto error.
  • Comments and blank lines are preserved. Values containing spaces, #, or newlines are safely quoted/escaped on decryption.

CI/CD Integration

The pattern is always the same: provide the master key via the DOTSEAL_MASTER_KEY environment variable (from your platform's secret store), commit only .env.enc, and either decrypt to a file or load at runtime.

GitHub Actions

Store the key as a repository/environment secret named DOTSEAL_MASTER_KEY.

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      DOTSEAL_MASTER_KEY: ${{ secrets.DOTSEAL_MASTER_KEY }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install dotseal

      # Option A: decrypt to a real .env for tools that expect a file
      - run: dotseal decrypt .env.enc .env

      # Option B: load at runtime inside your app (no cleartext file)
      - run: python -c "from dotseal import load_env; load_env(); import app"

Docker

Bake only the encrypted file into the image and pass the key at runtime:

FROM python:3.12-slim
WORKDIR /app
RUN pip install dotseal
COPY .env.enc .
COPY . .
# App calls load_env() on startup.
CMD ["python", "main.py"]
docker run -e DOTSEAL_MASTER_KEY="$(cat .dotseal.key)" my-image
# main.py
from dotseal import load_env
load_env()   # picks up DOTSEAL_MASTER_KEY from the container env

Kubernetes

Store the master key in a Secret and expose it as DOTSEAL_MASTER_KEY:

env:
  - name: DOTSEAL_MASTER_KEY
    valueFrom:
      secretKeyRef:
        name: dotseal
        key: master-key

Security Notes & Limitations

  • AES-256-GCM provides confidentiality and integrity. Tampered ciphertext or a wrong key is rejected rather than silently producing garbage.
  • AAD binding prevents an attacker who can edit the committed .env.enc from relocating a high-privilege secret onto a low-privilege variable name.
  • Key fingerprint is a domain-separated SHA-256 hash truncated to 8 bytes; it reveals nothing about the key itself.
  • Memory hygiene is best-effort. dotseal overwrites the mutable key buffers it controls, but Python's immutable str/bytes and garbage collector mean secrets can still linger in memory. Do not rely on this for protection against an attacker with live process access.
  • The master key is the whole ballgame. Anyone with the key can decrypt everything. Rotate it by re-encrypting with dotseal init --force followed by encrypt, and store it only in trusted secret managers.
  • This tool is a single-key symmetric scheme. It does not implement multi-recipient/asymmetric key sharing (a SOPS + age/KMS feature).

Development

uv venv && uv pip install -e ".[dev]"
uv run pytest

CI runs the full test suite on Python 3.8 through 3.14 (see .github/workflows/test.yml).

The test suite covers crypto round-trips, edge-case values (empty strings, !!@#$%=, unicode, multi-line, large), structural parsing, the runtime loader (asserting no side-effect files are written), and the full CLI lifecycle including edit.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dotseal-0.1.0.tar.gz (58.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dotseal-0.1.0-py3-none-any.whl (19.4 kB view details)

Uploaded Python 3

File details

Details for the file dotseal-0.1.0.tar.gz.

File metadata

  • Download URL: dotseal-0.1.0.tar.gz
  • Upload date:
  • Size: 58.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for dotseal-0.1.0.tar.gz
Algorithm Hash digest
SHA256 96f74a4e4abde1f8c270f8c2d9cc784981e9628b8e63b8948cacef233f838e13
MD5 460c68c17a8a962e26babc9c53384084
BLAKE2b-256 da0ca72b5e1bc5f3d181cc1bb95abd7f7d761483b30f5558eaf7a6e7c36acdea

See more details on using hashes here.

File details

Details for the file dotseal-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: dotseal-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 19.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for dotseal-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 71d46880e10098e56fe92b5ba2c26989c74deaf783c3e3fd0e05534e0dd0cdf5
MD5 b9313bc13a83a207ea928f5087ececae
BLAKE2b-256 e3fd974784c72b2cb24137bc8517e79141adeefc1275a7b80404d6fdd62a36f3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page