██╗ ██╗ ██████╗ ██████╗ ████████╗███████╗██╗ ██╗
██║ ██║██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝╚██╗██╔╝
██║ ██║██║ ██║██████╔╝ ██║ █████╗ ╚███╔╝
╚██╗ ██╔╝██║ ██║██╔══██╗ ██║ ██╔══╝ ██╔██╗
╚████╔╝ ╚██████╔╝██║ ██║ ██║ ███████╗██╔╝ ██╗
╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
Vault Orchestrated Rotation & Tamper-Evident eXchange
A single-binary credential rotation daemon written in Crystal.
VORTEX is a daemon that watches your credentials and automatically rotates them before they become a security risk. You define rotation policies in code, and VORTEX enforces them — rotating secrets on schedule, logging every action in a tamper-proof audit trail, and alerting your team when something needs attention.
It talks to AWS Secrets Manager, HashiCorp Vault, GitHub, and local .env files out of the box. Adding a new backend is a single file.
Every major breach in recent years — SolarWinds, Codecov, Snowflake, Microsoft's Storm-0558 — had one thing in common: a credential that lived too long. VORTEX exists to make sure that doesn't happen to you.
| Feature | What It Does |
|---|---|
| 🔄 Automatic Rotation | Four-step rotation contract (generate → apply → verify → commit) with dual-version safety and automatic rollback on failure |
| 📜 Policy as Code | Write rotation policies in Crystal. The compiler catches typos — if it builds, your policies are valid |
| 🛡️ Tamper-Proof Audit Log | Three independent integrity layers: SHA-256 hash chain + HMAC ratchet + Ed25519-signed Merkle batches |
| 🔐 Envelope Encryption | AES-256-GCM with per-row data keys, wrapped by a master key. Every secret gets its own encryption key |
| 🖥️ Live TUI Dashboard | Real-time terminal UI showing rotation status, progress bars, and event streams — no external framework needed |
| 🤖 Telegram Bot | Two-tier command bot — viewers see status, operators can trigger rotations on the go |
| 📋 Compliance Bundles | One command generates a signed evidence ZIP for SOC 2, PCI-DSS, ISO 27001, or HIPAA auditors |
| 🔌 Plugin Architecture | Add new backends by dropping a single file — the register_as macro handles everything at compile time |
Tip
First time here? Follow the Step-by-Step Walkthrough (WALKTHROUGH.md) — it covers everything from installing Crystal to running in production, with zero assumptions about your background.
git clone https://github.com/GENESIS-PROKEY/Vortex.git
cd Vortex
shards install && shards build vortex --release
./bin/vortex democurl -fsSL https://raw.githubusercontent.com/GENESIS-PROKEY/Vortex/main/scripts/install.sh | bashTip
This project uses just as a command runner. Run just to see all available recipes.
Install just: curl -sSf https://just.systems/install.sh | bash -s -- --to ~/.local/bin
just demo # ⚡ Tier 1 — zero-deps, SQLite + .env rotator (<1 second)
just tui-demo # 🖥️ Live TUI preview with synthetic events (8 seconds)
just demo-full # 🐳 Tier 2 — Docker Compose: Postgres + LocalStack + Vault + fake-GitHub
just demo-full-down # 🧹 Tear down the Docker stackThis section walks you through every major system in VORTEX, from the moment you start the daemon to the moment a credential gets rotated.
When VORTEX boots, it follows a strict 11-step initialization sequence:
① Validate secrets ─→ VORTEX_HMAC_KEY_HEX and VORTEX_KEK_HEX must exist (hard-fail)
② Open database ─→ SQLite or PostgreSQL, auto-detected from --db flag
③ Run migrations ─→ Creates tables, indexes, append-only triggers on first boot
④ Build crypto layer ─→ AES-256-GCM envelope from KEK + optional Ed25519 signer
⑤ Load policies ─→ Compiled into the binary — already validated by Crystal compiler
⑥ Register rotators ─→ Checks env vars: AWS keys set? Register AWS rotator. Vault token? Register Vault. etc.
⑦ Start event bus ─→ Central nervous system — all components communicate through here
⑧ Start subscribers ─→ Audit writer, log notifier, rotation worker, policy evaluator
⑨ Start scheduler ─→ Fires a tick every 60 seconds (configurable)
⑩ Wire Telegram bot ─→ Only if TELEGRAM_TOKEN is set
⑪ Block on SIGINT ─→ Graceful shutdown: drains audit queue (2s timeout), then exits
After boot, the daemon prints which rotators are active and which integrations are wired:
vortex running. PID 4242, tick 60s, db postgres://****:****@db.internal:5432/vortex_prod
rotators: env_file, aws_secretsmgr, vault_dynamic, github_pat
telegram: enabled
If a rotator is missing from that list, its environment variables aren't set.
VORTEX uses an event-driven architecture. Every component communicates through a central event bus — nobody calls each other directly.
┌─────────────────────┐
Scheduler ──────►│ │──────► Audit Subscriber (Block — never drops)
│ EVENT BUS │──────► Rotation Worker (Block — must dispatch)
Telegram ──────►│ (Crystal channels) │──────► TUI (Drop — stale UI is fine)
│ │──────► Telegram Bot (Drop — network flaky)
Orchestrator ───►│ │──────► Log Notifier (Drop — best-effort)
└─────────────────────┘
How overflow works:
Each subscriber gets its own buffered channel. When a subscriber's buffer is full:
- Block mode — The bus waits up to 5 seconds for space, then drops the event and logs a warning. Used for critical subscribers (audit, rotation worker) that must not miss events.
- Drop mode — The bus immediately moves on and logs a warning. Used for best-effort subscribers (TUI, Telegram) where missing an event isn't a disaster.
This design means a slow Telegram connection can never stall the audit log, and a crashed TUI can never block credential rotations.
Policies are Crystal source files that compile directly into the binary. Here's one:
policy "aws-prod-databases" do
match { |c| c.kind.aws_secretsmgr? && c.tag(:env) == "prod" }
max_age 30.days
warn_at 25.days
enforce :rotate_immediately
notify_via :telegram, :structured_log
on_rotation_failure :alert_critical
endThe evaluation flow:
Scheduler fires tick
│
▼
Policy Evaluator wakes up
│
├── For each policy, query credentials matching the policy that are overdue
│
├── If a credential matches MULTIPLE policies → PolicyConflictError (refuses to act)
│
├── If overdue + enforce = :rotate_immediately → publish RotationScheduled event
│
├── If in warning window → publish PolicyViolation event (alert only)
│
└── If enforce = :notify_only → alert operators but don't rotate
Why compile-time validation matters:
# ❌ This fails at compile time — :rotate_immediatly is not a valid Action
enforce :rotate_immediatly
# ❌ This fails at compile time — Credential has no method 'kund'
match { |c| c.kund.aws_secretsmgr? }
# ❌ This fails at registration time — 'telegrm' is not a valid Channel
notify_via :telegrmIf it compiles, your policies are valid. Period.
When a credential needs rotating, VORTEX follows a strict four-step contract borrowed from AWS Secrets Manager's rotation Lambda pattern:
Step 1: GENERATE Step 2: APPLY
┌─────────────────────┐ ┌─────────────────────┐
│ Create new secret │ │ Make new secret │
│ Old is still active │──────────►│ usable alongside old │
│ New is "pending" │ │ (dual-version safe) │
└─────────────────────┘ └──────────┬──────────┘
│
▼
Step 4: COMMIT Step 3: VERIFY
┌─────────────────────┐ ┌─────────────────────┐
│ Promote new secret │◄──────────│ Read back new secret │
│ Revoke old secret │ │ Confirm byte-equal │
│ (only irreversible) │ │ match │
└─────────────────────┘ └─────────────────────┘
What happens when something goes wrong:
| Failure Point | What VORTEX Does | Final State |
|---|---|---|
| Generate fails | Nothing was created — safe to retry | Failed |
| Apply fails | Calls rollback_apply() to clean up the new secret |
Failed |
| Verify fails | Calls rollback_apply() — new secret didn't match |
Failed |
| Commit fails | Cannot safely undo — cloud state may be partially updated | Inconsistent |
The Inconsistent state is terminal and triggers a critical alert. It means the cloud side might have the new credential as active while the old one is partially revoked. This requires human intervention — VORTEX won't try to fix it automatically because that could make things worse.
What each rotator does at each step:
| Step | AWS Secrets Manager | HashiCorp Vault | GitHub PATs | Local .env |
|---|---|---|---|---|
| Generate | PutSecretValue with AWSPENDING label |
read_dynamic(role_path) — Vault issues new creds |
create_pat(name, scopes) |
Generate random Base64 |
| Apply | No-op (AWS already staged) | No-op (Vault already issued) | No-op (GitHub already created) | Write .env.pending file |
| Verify | GetSecretValue by version ID |
Renew lease as liveness check | Authenticate with new token (GET /user) |
Read back .env.pending |
| Commit | UpdateSecretVersionStage — move labels |
Revoke old lease | Delete old PAT | Atomic File.rename with flock |
VORTEX uses envelope encryption — every secret gets its own unique encryption key (DEK), and that key is itself encrypted by a master key (KEK):
Your Secret ("db-password-2026")
│
│ AES-256-GCM encrypt with random DEK + AAD binding
▼
┌─────────────────────────┐
│ Encrypted Secret │ ──→ stored in credential_versions.ciphertext
│ (nonce ‖ tag ‖ body) │
└─────────────────────────┘
DEK (32 random bytes)
│
│ AES-256-GCM wrap with KEK + AAD binding
▼
┌─────────────────────────┐
│ Wrapped DEK │ ──→ stored in credential_versions.dek_wrapped
│ (nonce ‖ tag ‖ body) │
└─────────────────────────┘
Why this design?
- Per-row DEKs — Each secret uses a unique key, so there's zero risk of nonce reuse (the #1 way to break AES-GCM)
- AAD binding — The encrypted data is bound to
credential_id + kind + version. You can't swap one row's ciphertext into another row — decryption fails - Crypto agility — An
algorithm_idbyte is reserved:0x01= AES-256-GCM (current),0x02= XChaCha20-Poly1305,0x03= ML-KEM hybrid (post-quantum)
This is where VORTEX gets paranoid — in a good way. Every action is recorded in an audit log protected by three independent integrity layers:
┌─────────────────────────────────────────────────────────────┐
│ LAYER 3: Ed25519 Merkle Batches │
│ │
│ Every 5 minutes, VORTEX collects recent audit entries, │
│ builds a Merkle tree from their hashes, and signs the root │
│ with an Ed25519 private key. Your auditor can verify with │
│ the public key alone — no database access needed. │
├─────────────────────────────────────────────────────────────┤
│ LAYER 2: HMAC Ratchet │
│ │
│ Each entry is signed with HMAC-SHA256. Every 1,024 entries, │
│ the signing key rotates: new_key = SHA256(old_key ‖ salt). │
│ The old key is securely zeroed from memory. Even if an │
│ attacker recomputes the hash chain, they can't forge the │
│ HMACs without the original seed key. │
├─────────────────────────────────────────────────────────────┤
│ LAYER 1: SHA-256 Hash Chain │
│ │
│ Each entry's hash = SHA256(previous_hash ‖ payload). │
│ Change any single row and every hash after it breaks. │
│ This is the same principle Bitcoin uses. │
├─────────────────────────────────────────────────────────────┤
│ DATABASE: Append-Only Protection │
│ │
│ PostgreSQL triggers REFUSE all UPDATE, DELETE, and TRUNCATE │
│ operations on the audit table. Even SQL injection can't │
│ erase history. │
└─────────────────────────────────────────────────────────────┘
Verify the entire chain with one command:
vortex audit verify --db=sqlite:vortex.db
# ✓ chain valid: 14,892 entriesRun vortex watch to get a live terminal dashboard:
┌─ VORTEX ───────────────────────────────────────────────────────┐
│ PID 4242 │ uptime 2h 14m │ tick 60s │
├─ Status ───────────────────────────────────────────────────────┤
│ credentials: 24 │ due: 3 │ overdue: 1 │ rotated-24h: 7 │
├─ Active Rotations ────────────────────────────────────────────┤
│ db-prod-rw aws_secretsmgr ▰▰▰▱ verify [3/4] │
│ deploy-bot github_pat ▰▱▱▱ generate [1/4] │
├─ Recent Events ───────────────────────────────────────────────┤
│ 14:32:01 ✅ rotation.completed db-staging-ro │
│ 14:31:58 🔄 rotation.step db-prod-rw → verify │
│ 14:31:55 ⚠️ policy.violation api-key-prod (47d overdue) │
│ 14:30:00 🕐 scheduler.tick │
├────────────────────────────────────────────────────────────────┤
│ Press Ctrl+C to exit │
└────────────────────────────────────────────────────────────────┘
The TUI is hand-rolled using nothing but ANSI escape sequences — no ncurses, no external TUI framework. It subscribes to the event bus with Drop overflow (a stale UI is better than a blocked engine) and coalesces repaints to 200ms intervals.
Adding support for a new secret backend is a single file. Here's the pattern:
# src/vortex/rotators/my_backend.cr
module Vortex::Rotators
class MyBackendRotator < Rotator
register_as :my_backend # ← This one line wires it in
def kind : Symbol = :my_backend
def can_rotate?(c) = c.kind.my_backend?
def generate(c) # Create the new credential
def apply(c, s) # Make it usable
def verify(c, s) # Read it back and confirm
def commit(c, s) # Promote new, revoke old
def rollback_apply(c, s) # Undo apply() on failure
end
endThat's it. The register_as :my_backend macro adds your class to the compile-time registry. Zero changes to the orchestrator, scheduler, event bus, audit log, policy evaluator, or any other file.
| Rotator | Backend | Authentication | Source |
|---|---|---|---|
| AWS Secrets Manager | secretsmanager.<region>.amazonaws.com |
SigV4 (hand-rolled) | aws_secrets.cr |
| HashiCorp Vault | Dynamic database secrets engine | X-Vault-Token |
vault_dynamic.cr |
| GitHub PATs | Fine-grained personal access tokens | Bearer ghp_... |
github_pat.cr |
Local .env files |
Atomic temp+rename with file locking | n/a | env_file.cr |
| Command | Purpose |
|---|---|
vortex run --db=<url> |
Start the headless daemon |
vortex watch --db=<url> |
Start the daemon with live TUI |
vortex check --db=<url> --output=json |
One-shot policy check for CI (exit 1 = violations) |
vortex rotate <credential-id> |
Manually rotate a single credential |
vortex policy list |
Show all compiled policies |
vortex policy show <name> |
Detailed inspection of one policy |
vortex audit verify |
Verify all three integrity layers |
vortex export --framework=soc2 --out=evidence.zip |
Generate signed compliance bundle |
vortex verify-bundle evidence.zip |
Offline verification of a compliance bundle |
vortex demo |
Zero-deps demo (< 1 second) |
vortex tui-demo --seconds=8 |
TUI preview with synthetic events |
vortex run and vortex watch require two 32-byte hex secrets. Generate them with one command each:
export VORTEX_HMAC_KEY_HEX=$(openssl rand -hex 32) # Audit log HMAC seed
export VORTEX_KEK_HEX=$(openssl rand -hex 32) # Master encryption key
export VORTEX_SIGNING_KEY_HEX=$(openssl rand -hex 32) # Optional: Merkle batch signing
vortex run --db=sqlite:vortex.db # headless
vortex watch --db=sqlite:vortex.db # with TUIImportant
Generate these once and store them somewhere durable. These are the cryptographic roots of your entire deployment.
| Situation | Recommendation |
|---|---|
| Playing around / demo | Paste in terminal — they live until you close the window |
| Dev machine | Put them in a .env file (add to .gitignore — never commit secrets) |
| Production | Store in AWS KMS, HashiCorp Vault, or your org's secrets manager. Inject at boot via systemd EnvironmentFile=/etc/vortex/vortex.env (mode 0600) |
| Never, ever | In a Git repo. Even private ones. Even "just for a second." |
| Key | Impact of Loss |
|---|---|
VORTEX_KEK_HEX |
⛔ All stored secrets become permanently unreadable. This is the master encryption key. Lose it = lose access to every encrypted credential. Treat it like a root password. |
VORTEX_HMAC_KEY_HEX |
|
VORTEX_SIGNING_KEY_HEX |
🔸 Can't sign new Merkle batches. Previously signed batches still verify with the public key. Least critical of the three. |
┌──────────────────────────────────────────────────────────────────┐
│ VORTEX (single Crystal binary) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Event Bus │ │
│ │ (typed Crystal channels) │ │
│ └───┬──────┬──────┬──────┬──────┬──────┬──────────────────┘ │
│ │ │ │ │ │ │ │
│ ┌───▼──┐ ┌─▼───┐ ┌▼────┐ ┌▼───┐ ┌▼────┐ ┌▼─────┐ │
│ │Sched.│ │Rot. │ │Pol. │ │TUI │ │Tele.│ │Audit │ │
│ │ │ │Wrkr │ │Eval │ │ │ │Bot │ │Sub. │ │
│ └──────┘ └──┬──┘ └─────┘ └────┘ └─────┘ └──┬───┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Persistence (PostgreSQL / SQLite) │ │
│ │ + Three-Layer Audit Integrity │ │
│ │ + AEAD Envelope Encryption │ │
│ └───────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
All components run as fibers in a single OS process. The event bus uses Crystal channels (nanosecond-scale), so the architectural overhead is essentially zero.
vortex export --framework=soc2 --out=evidence.zip generates a self-verifying ZIP bundle:
| Framework | Controls Covered |
|---|---|
| SOC 2 | CC6.1, CC6.6, CC6.7, CC4.1, CC7.1, CC7.2 |
| PCI-DSS v4.0.1 | 8.3.9, 8.6.3, 10.2.1, 10.5.2, 10.5.3, 3.7.4, 11.5.2 |
| ISO 27001:2022 | A.5.16–A.5.18, A.8.5, A.8.15–A.8.16, A.8.24 |
| HIPAA | §164.308(a)(5)(ii)(D), §164.312(b) |
The bundle includes: audit_log.ndjson, audit_batches.json, control_mapping.json, public_key.pem, manifest.json (SHA-256 checksums), manifest.sig (Ed25519 signature), and a self-documenting README.md.
| Layer | Technology |
|---|---|
| Language | Crystal 1.20+ — compiled, type-safe, fiber-based concurrency |
| Database | SQLite3 (dev/demo) or PostgreSQL 16+ (production) |
| Crypto | Direct LibCrypto FFI — AES-256-GCM AEAD + Ed25519 signing |
| HTTP | Crystal stdlib with custom retry (exponential backoff + jitter) |
| Testing | Crystal Spec — 179+ unit tests + integration tests against real PostgreSQL |
| CI | GitHub Actions — format, unit (matrix), integration (PG service), release build |
| Linting | Ameba 1.6.4 |
| Task Runner | just |
| Shard | Purpose |
|---|---|
crystal-db |
Database abstraction |
crystal-pg |
PostgreSQL driver |
crystal-sqlite3 |
SQLite3 driver |
tourmaline |
Telegram bot framework (used minimally) |
webmock.cr |
HTTP mocking for tests |
VORTEX is configured entirely via environment variables — no config files needed. Every integration is opt-in: set the env vars for what you want, leave the rest unset.
See CONFIGURATION.md for the full operator guide:
- Required env vars (
VORTEX_KEK_HEX,VORTEX_HMAC_KEY_HEX,DATABASE_URL) - Per-rotator setup (AWS IAM policy, Vault token policy, GitHub admin PAT)
- Telegram bot creation and chat-ID discovery
- systemd service unit with hardening directives
- Production security checklist
| Document | Audience | What's Inside |
|---|---|---|
| WALKTHROUGH.md | Beginners | Step-by-step from zero to running — install Crystal, build, generate keys, run demos, go to production. No prior experience needed. |
| CONFIGURATION.md | Operators | Full env-var reference, per-rotator setup, systemd, security checklist |
| 00 — Overview | Everyone | Prerequisites, quick start, three-tier demo path |
| 01 — Concepts | Learners | Rotation theory, real breaches, NIST/SOC2/PCI/ISO/HIPAA controls |
| 02 — Architecture | Developers | Bus + plugin design, persistence layers, audit integrity, encryption |
| 03 — Implementation | Developers | Code-level walkthrough of every major subsystem |
| 04 — Challenges | Contributors | 10 extension challenges: PG ALTER USER, Slack, ML-KEM, SPIFFE, JIT broker |
src/vortex/
├── audit/ # 7 files — Hash chain, HMAC ratchet, Merkle tree, Ed25519 signing
├── aws/ # 2 files — SigV4 signer + Secrets Manager client
├── cli/ # 16 files — CLI dispatcher, bootstrap, 11 subcommands
├── compliance/ # 2 files — Evidence bundle generator + control mappings
├── crypto/ # 4 files — AEAD, envelope encryption, KEK, secure random
├── demo/ # 2 files — Tier 1 demo + TUI preview
├── domain/ # 3 files — Credential, CredentialVersion, NewSecret
├── engine/ # 6 files — Event bus, orchestrator, rotation worker, scheduler
├── events/ # 3 files — Event base class + credential/system events
├── github/ # 1 file — GitHub PAT management client
├── http/ # 1 file — HTTP retry with exponential backoff + jitter
├── notifiers/ # 4 files — Log notifier, Telegram client/bot/subscriber
├── persistence/ # 15 files — Abstract interface + SQLite + PostgreSQL backends
├── policy/ # 4 files — DSL, builder, policy model, evaluator
├── rotators/ # 5 files — Abstract base + 4 concrete rotators
├── tui/ # 5 files — TUI controller, ANSI helpers, renderer, state
└── vault/ # 1 file — Vault HTTP client