>_ INITIALIZING DATABANKS...
>_ LOADING UI MODULES...
>_ DECRYPTING ASSETS...
>_ SECURING CONNECTION...
>_ SYSTEM READY.

Engineering a Static Vault: Build-Time Encryption

Python Cryptography AES-256 GitHub Actions Architecture
USER: SYS_ADMIN | DATE: 2026-03-26 | REF_PID: Blog-Site
Cover for Engineering a Static Vault: Build-Time Encryption

The fundamental problem with a static site is right there in the name: it is static. When Astro builds a website, every file in the public directory is compiled into plain HTML, CSS, and JavaScript. Anyone can open their browser’s developer tools and read the entire source code.

So, how do you host a private, password-protected Notion alternative on a platform where every file is publicly served?

You don’t protect the website. You protect the data.

The Build-Time Intercept

Instead of building a Node.js server with authentication middleware, I decided to air-gap the private data using a CI/CD pipeline.

Before astro build ever runs in GitHub Actions, a custom Python script (encrypt_vault.py) intercepts my /vault directory. It reads all my private Markdown files, packages them into an array, and encrypts the entire payload into a single encrypted.json file.

This JSON file is the only thing the static site sees. Astro happily serves it to the public, but without the master key, it is just cryptographic noise.

The Math: PBKDF2 and AES-256-GCM

If an attacker downloads encrypted.json, their only option is to attempt to brute-force the password. To make this mathematically infeasible, the Python script uses two distinct cryptographic layers.

1. Key Derivation (PBKDF2) We don’t use the master password directly to encrypt the data. Instead, we use it to generate a 256-bit key. We generate a random 16-byte salt and run the password through the SHA-256 hashing algorithm 100,000 times.

This iteration count is crucial. It creates an artificial delay. A legitimate user only has to compute it once to unlock the vault (taking ~100 milliseconds). But an attacker trying to guess billions of passwords a second will see their supercomputer grind to a halt.

2. The Encryption (AES-256-GCM) Once the key is derived, we encrypt the text using AES-256 in Galois/Counter Mode (GCM). GCM is beautiful because it handles both encryption and authentication. It generates a “tag” at the end of the ciphertext. If an attacker tries to tamper with the JSON file and flip a single bit, the tag verification will fail, and the decryption will instantly abort.

The Python Engine

Here is the exact implementation of the cryptographic trapdoor running on the build server:

# Generating the cryptographic trapdoor
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Hash import SHA256

# 1. Generate unique random parameters for this specific build
salt = get_random_bytes(16)
iv = get_random_bytes(12) 

# 2. Derive the 256-bit key using 100k iterations
key = PBKDF2(
    PASSWORD.encode(), 
    salt, 
    dkLen=32, 
    count=100000, 
    hmac_hash_module=SHA256
)

# 3. Encrypt and authenticate the payload
cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
ciphertext, tag = cipher.encrypt_and_digest(json_payload.encode('utf-8'))

Because the salt and iv (Initialization Vector) are regenerated randomly on every single GitHub push, the resulting encrypted.json will look completely different every time I deploy the site, even if the private markdown files haven’t changed.

The server does the heavy lifting, the static site hosts the locked box, and the client browser is left to handle the keys.

0%