Sergen Tanguc
Selected Works
// Case Study

OneSeal

Secrets as code, encrypted by default, type-safe by construction

Secrets, configs, and platform outputs as code — typed, versioned, encrypted. One Terraform state file in, one type-safe TypeScript SDK out.

Rust Secrets Management Platform Engineering CLI Infrastructure as Code Age Encryption
View on GitHub

The Problem

Every team eventually hits the same wall: 50 environment variables to manage, a Postgres password buried in a Slack thread, an API key copied from the wiki that was last updated in 2021. The actual source of truth is scattered across Terraform outputs, .env files, secret managers, and developer laptops — and none of it is type-safe.

The failure modes are predictable. process.env.DATBASE_URL (note the typo) silently returns undefined and takes down production at 3 AM. A new developer spends their first day asking “where do I find the Redis password?” on Teams. A rotation event means updating 50 variables across 8 services instead of one key.

OneSeal treats this as a code generation problem.

The Solution

OneSeal takes platform secrets — Terraform state outputs, for now — and generates a versioned, type-safe TypeScript SDK with all sensitive values encrypted using Age (X25519 + ChaCha20). The SDK is safe to commit to a private Git repository and installed like any other dependency.

The before/after is stark:

Before:

// runtime errors waiting to happen
const dbPass = process.env.POSTGRES_PASSWORD; // undefined?
const apiKey = process.env.STRIPE_KEY; // or was it STRIPE_API_KEY?

After:

import { State } from '@contoso/my-infra';
const state = await new State().initialize();
// full type safety — IDE knows the exact structure
const db = state.database.postgresql;
const stripeKey = state.payments.stripe.secretKey;

No more typos. No more “ask Sarah, she set it up.” The SDK is self-documenting by construction.

Architecture

The workflow has three stages:

Generate (build/CI time): The CLI reads a Terraform state file, extracts all outputs, encrypts those flagged sensitive = true using Age with the team’s public keys, and emits a versioned npm package containing the typed SDK and the encrypted blobs.

Terminal window
# generate SDK from Terraform state for prod and staging
oneseal generate terraform.tfstate \
--name @contoso/my-infra \
--public-key-path ./oneseal-keys/ # directory of .pub files — one per team member

Distribute (package registry or private Git): The generated SDK is committed to a private repository or pushed to a private npm registry. It follows the same versioning and review process as any other dependency.

Consume (runtime): Applications install the SDK normally. At initialization, the SDK decrypts secrets in-memory using the private Age key — from ~/.oneseal/age.key locally, or from the ONESEAL_AGE_PRIVATE_KEY environment variable in CI/CD.

// secrets are decrypted in-memory on initialize()
// nothing is ever written to disk in plaintext
const state = await new State().initialize();

Multi-Recipient Encryption

Each team member gets their own Age keypair. When generating the SDK, all public keys are provided — OneSeal encrypts each secret for all recipients simultaneously. Adding a new developer means adding their .pub file to the team keys repository and regenerating. No need to re-share secrets through side channels.

Terminal window
# generate-key creates a keypair stored in ~/.oneseal/
oneseal generate-key
# CI/CD gets its own key — private key goes in the secret store
oneseal generate-key --output ./ci

Encryption Details

  • Algorithm: Age with X25519 (Curve25519) for key exchange, ChaCha20 for data encryption
  • Selective: only Terraform outputs marked sensitive = true are encrypted — non-sensitive config (URLs, feature flags, resource IDs) is plaintext in the SDK
  • Ephemeral: each secret uses a fresh symmetric key, providing forward secrecy
  • Git-safe: encrypted blobs are compact and stable in diffs

CI/CD Integration

The Docker image fits naturally into existing pipelines:

- name: Generate OneSeal SDK
run: |
docker run --rm \
-v ${{ github.workspace }}:/app \
-v ${{ github.workspace }}/prod.tfstate:/tmp/prod.tfstate:ro \
stanguc/oneseal:latest generate \
--public-key "${{ secrets.ONESEAL_AGE_PUBLIC_KEY }}" \
--state-path /tmp/prod.tfstate \
--name @contoso/my-infra \
--output-directory /app/@contoso/my-infra
- name: Commit SDK
working-directory: '@contoso/my-infra'
run: |
git add .
git commit -m "feat: update infrastructure outputs SDK"
git push origin main

One secret in the CI environment (ONESEAL_AGE_PRIVATE_KEY), access to all of them at runtime.

Why Not Just Use…

Vault / AWS Secrets Manager? These are storage solutions — keep using them. OneSeal complements them by removing runtime network dependencies from application code. Instead of calling Vault at startup (retry logic, network failures, VPN requirements), you call initialize() on a local package. Secrets are fetched once during SDK generation, not on every app start.

Plain environment variables? No compile-time safety, no version history, no structure, no audit trail. process.env is a flat namespace of untyped strings that fails at runtime.

.env files? The same, but also committed to repos in plaintext “temporarily” for six years.

Status

Currently in v0.1.0. Terraform state is the only supported input source. Python, Go, and PHP SDK targets are in progress. Planned integrations include .env files, Pulumi state, HashiCorp Vault KV, and AWS Secrets Manager.