Vortex

██╗   ██╗ ██████╗ ██████╗ ████████╗███████╗██╗  ██╗
██║   ██║██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝╚██╗██╔╝
██║   ██║██║   ██║██████╔╝   ██║   █████╗   ╚███╔╝
╚██╗ ██╔╝██║   ██║██╔══██╗   ██║   ██╔══╝   ██╔██╗
 ╚████╔╝ ╚██████╔╝██║  ██║   ██║   ███████╗██╔╝ ██╗
  ╚═══╝   ╚═════╝ ╚═╝  ╚═╝   ╚═╝   ╚══════╝╚═╝  ╚═╝

Vault Orchestrated Rotation & Tamper-Evident eXchange
A single-binary credential rotation daemon written in Crystal.

Crystal PostgreSQL AGPL v3


What is VORTEX?

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.

Why does this exist?

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.


✨ Key Features

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

🚀 Quick Start

[!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.

Option 1: Build from source

git clone https://github.com/GENESIS-PROKEY/Vortex.git
cd Vortex
shards install && shards build vortex --release
./bin/vortex demo

Option 2: One-line installer

curl -fsSL https://raw.githubusercontent.com/GENESIS-PROKEY/Vortex/main/scripts/install.sh | bash

[!TIP] 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

Three ways to try it

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 stack

📖 How It Works — The Complete Walkthrough

This section walks you through every major system in VORTEX, from the moment you start the daemon to the moment a credential gets rotated.

1. Startup — What Happens When You Run vortex run

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.


2. The Event Bus — How Components Talk

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.


3. Policy Evaluation — How VORTEX Decides What to Rotate

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
end

The 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 :telegrm

If it compiles, your policies are valid. Period.


4. The Four-Step Rotation Contract

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

5. Encryption — How Secrets Are Stored

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_id byte is reserved: 0x01 = AES-256-GCM (current), 0x02 = XChaCha20-Poly1305, 0x03 = ML-KEM hybrid (post-quantum)

6. The Three-Layer Audit Log

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 entries

7. The TUI Dashboard

Run 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.


8. The Plugin System — Adding a New Rotator

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
end

That'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.


🔌 Supported Rotators

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

🛠️ CLI Reference

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

Running the daemon

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 TUI

[!IMPORTANT] Generate these once and store them somewhere durable. These are the cryptographic roots of your entire deployment.

Where to store your keys

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."

What happens if you lose a key

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 ⚠️ Audit log HMAC verification breaks going forward. The hash chain (Layer 1) and signed Merkle batches (Layer 3) still work. Not catastrophic, but weakens audit integrity.
VORTEX_SIGNING_KEY_HEX 🔸 Can't sign new Merkle batches. Previously signed batches still verify with the public key. Least critical of the three.

🏗️ Architecture

┌──────────────────────────────────────────────────────────────────┐
│                     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.


📋 Compliance Coverage

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.


🧰 Tech Stack

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

Dependencies

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

⚙️ Configuration

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

📚 Documentation & Learn Modules

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

📂 Project Structure

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

License

AGPL 3.0

Repository

Vortex

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 6
  • about 4 hours ago
  • August 11, 2026
License

GNU Affero General Public License v3.0

Links
Synced at

Tue, 11 Aug 2026 13:43:50 GMT

Languages