Projects / Keyway

Keyway

Self-hosted secrets vault with PBKDF2 key derivation and Redis-backed, session-only key handling.

Demo

A real recording of Keyway running locally: sign up, add three secrets with expiry dates and health-check URLs, run the checks (one valid, one rejected, one expired), reveal a value that re-masks itself, and open the dashboard.

Overview

Keyway is a self-hosted vault for the API keys, tokens and credentials a developer collects. Unlike a generic password manager it tracks each key's expiry date and can test whether the key still works, so a revoked or expired key is found before it breaks something in production.

It is built so that a stolen database is useless. Each user gets a random salt, and the encryption key is derived from their password at login with PBKDF2, held only in the server-side session, and never written to disk or sent to the browser.

The demo below is a recording of Keyway running locally with throwaway test data: three clearly fake secrets, checked against a small mock API that accepts exactly one of the keys.

What it does

Screenshots

The Add New Secret form with service name, masked secret, tags, expiry date and a health check toggle
Adding a secret: name, masked value, tags, an expiry date and an optional health-check URL.
The vault after running health checks: one Valid, one Invalid and one Expired key
After Check All Health: Stripe is Valid, SendGrid is Invalid, and the legacy token is Expired.
A revealed secret with a countdown badge before it masks itself again
Revealing a value shows a countdown, then it masks itself again. Reveals are limited to 10 a minute.
The vault health dashboard with counts of expired, expiring, invalid and unchecked keys
The dashboard: counts of expired, expiring, invalid and unchecked keys, with the ones needing action listed.

How it works

Interactive diagram · 9 steps
login / save password key key encrypt decrypt request status shown A Vue 3 single-page app: login, a vault list with masked values, an add and edit form, health and expiry badges, and a dashboard of expired, expiring and invalid keys.Vue 3 appvault, badges, dashboard1 A Flask REST API with signed server-side sessions. Routes cover sign-up and login, secrets, health checks and the dashboard. The browser only ever holds a session cookie.Flask APIREST + sessions2 At login, PBKDF2-HMAC-SHA256 with 480,000 iterations turns your password and your own random 16-byte salt into a 32-byte key. The same password and salt always give the same key, so nothing has to be stored.ƒPBKDF2 keySHA-256, 480k rounds3 The derived key lives only in the server-side session, held in Redis or on the server's disk when running locally. It is never written to the database and never sent to the browser. Logging out destroys it.Session storememory only (Redis / disk)4 Every secret value is encrypted with Fernet (authenticated encryption) using that session key before it is stored, and decrypted only when you reveal it or run a health check.Fernetencrypt / decrypt5 SQLite in development and PostgreSQL in production. A copy of the database alone is not enough to read your secrets: it holds ciphertext, salts and metadata, but not the keys.Databaseciphertext + salts only6 Health checks only allow GET, POST and HEAD, never follow redirects (so a key cannot be sent to a surprise host), time out after five seconds and discard the response body so APIs that echo keys cannot leak them into logs.Check guardno redirects, 5 s7 The service the key belongs to. Keyway calls the URL you configure with the key attached (as a bearer token, a header or a query parameter) and looks only at the status code.Third-party APIStripe, SendGrid, ...8 Only the status code is kept. The vault and dashboard show it as Valid or Invalid, next to an expiry badge, so you can see which keys need renewing before something breaks.Status badgesvalid, invalid, expired9
Pick a stepHover, focus or tap any shape to see what it does.
How a key travels through Keyway. The dashed arrow is the outbound health-check request. Editable source (.excalidraw)
  1. You sign up and log in through the Vue app. The Flask API receives your password over the session.
  2. PBKDF2 combines your password with your own random salt into a 32-byte key, which is stored only in the server-side session.
  3. When you save a secret, Fernet encrypts it with that key and only the ciphertext reaches the database.
  4. For a health check, the value is decrypted in memory and sent to the API you chose, with redirects blocked, a five-second timeout and the response body discarded.
  5. Only the status code is kept, and the vault and dashboard show it as a Valid or Invalid badge next to the expiry badge.

Install and try it

Keyway needs Python 3 and Node 18+. This is the local setup, with SQLite and filesystem sessions and no Docker.

  1. Get the code and the backend running

    On macOS port 5000 is taken by AirPlay Receiver, so run the backend on another port such as 5055.

    git clone https://github.com/BriskAM/keyway.git
    cd keyway
    python3 -m venv venv
    source venv/bin/activate
    pip install -r backend/requirements.txt
    FLASK_RUN_PORT=5055 python backend/run.py
  2. Start the frontend

    Then open http://localhost:5173.

    cd frontend
    npm install
    VITE_API_BASE_URL=http://localhost:5055 npm run dev
  3. Or use Docker Compose

    This starts PostgreSQL, Redis, Gunicorn and Nginx, and serves the app at http://localhost:8080.

    cp .env.example .env
    docker-compose up --build
  4. Run the tests
    PYTHONPATH=backend python -m unittest discover -s backend/tests/

Notes

All projects · Home