Bradley Kirton's Blog

Published on Nov. 19, 2025

Go home

Managing a vault with Age

Age is a simple, modern and secure file encryption tool, format, and Go library.

I have started using it for encrypting application secrets to be stored in git. This scripts assumes you are keeping your secrets within a directory called deployment.

deployment
├── build.py
├── Caddyfile
├── env.enc
├── recipients.txt
├── sites-app-server.service
├── sites-beat.service
└── sites-celery.service

The script will decrypt the secrets using your private key and present them in $EDITOR. If you change the secrets a backup is created. The new secrets will be encrypted using the public keys in recipients.txt.

#!/usr/bin/env python

import argparse
import datetime
import os
import pathlib
import shlex
import shutil
import subprocess
import sys
import tempfile


def get_resolved_absolute_path(path: str | pathlib.Path) -> pathlib.Path:
    """Helper function for getting a path."""

    if isinstance(path, str):
        path = pathlib.Path(path)

    if path.is_absolute():
        return path

    return path.expanduser().resolve().absolute()


def get_identity_path() -> pathlib.Path | None:
    """Try get an identity file."""

    home_path = pathlib.Path.home()

    id_ed25519_path = home_path / ".ssh/id_ed25519"
    if id_ed25519_path.exists():
        return id_ed25519_path

    id_rsa_path = home_path / ".ssh/id_rsa"
    if id_rsa_path.exists():
        return id_rsa_path

    return None


if __name__ == "__main__":
    parser = argparse.ArgumentParser("Print the contents of a vault.")
    parser.add_argument("-i", "--identity", type=get_resolved_absolute_path, required=False)

    args = parser.parse_args()

    if not args.identity:
        identity_path = get_identity_path()
    else:
        identity_path = args.identity

    if not identity_path:
        print("ERROR: Failed to find identity file. Try specifying one with --identity=<identity-file-path>")
        sys.exit(1)

    current_timestamp = int(datetime.datetime.now().timestamp())
    base_path = pathlib.Path(__file__).parent.parent
    deployment_path = base_path / "deployment"
    recipients_path = deployment_path / "recipients.txt"
    env_path = deployment_path / "env.enc"
    env_backup_path = deployment_path / f"env.enc_{current_timestamp}.bak"
    age_path_raw = shutil.which("age")

    with tempfile.NamedTemporaryFile("wb") as stream:
        decrypt_command_raw = f"{age_path_raw} -d -i {identity_path} {env_path}"
        decrypt_command = shlex.split(decrypt_command_raw)
        decrypt_process = subprocess.run(decrypt_command, text=False, check=True, capture_output=True)

        env_content_pre_edit = decrypt_process.stdout
        stream.write(env_content_pre_edit)
        stream.flush()

        editor_command_raw = os.path.expandvars(f"$EDITOR {stream.name}")
        editor_command = shlex.split(editor_command_raw)
        subprocess.run(editor_command, check=True, capture_output=False)

        env_content_post_edit = pathlib.Path(stream.name).read_bytes()

        if env_content_post_edit == env_content_pre_edit:
            sys.exit(0)

        env_backup_path.write_bytes(env_content_pre_edit)
        print(f"Created backup {env_backup_path.name}")

        encrypt_command_raw = f"{age_path_raw} -e -R {recipients_path} -o {env_path} {stream.name}"
        encrypt_command = shlex.split(encrypt_command_raw)
        encrypt_process = subprocess.run(encrypt_command, check=True, capture_output=True)

        print(f"Wrote encrypted data to {env_path}")