A centralized SSH public key registry. One binary, one command to enroll devices, full-mesh SSH connectivity.
You have servers, LXC containers, phones, laptops. Each needs SSH keys to talk to the others. You're either:
- Copying the same private key everywhere (insecure)
- Running
ssh-copy-idto every server for every device (tedious) - Manually managing
authorized_keysacross an ever-changing fleet (painful)
KeyForge is a single binary that runs as a server and a CLI client. Every device registers its public key once. Every server fetches all keys automatically.
ββββββββββββββββββββ
β KeyForge Server β
β (REST API + β
β Web UI + β
β SQLite) β
ββββββββββ¬ββββββββββ
β
βββββββββββββββββββΌββββββββββββββββββ
β β β
βββββββΌββββββ βββββββΌββββββ βββββββΌββββββ
β Laptop β β Server β β Phone β
β registers β β registers β β registers β
β its key β β + fetches β β its key β
β β β all keys β β β
βββββββββββββ βββββββββββββ βββββββββββββ
Result: any enrolled device can SSH into any enrolled server. Servers can SSH to each other. No manual key distribution.
# Build
go build -o keyforge ./cmd/keyforge
# Start (generates an API key on first run β save it!)
./keyforge serve --port 9315 --data ./keyforge-dataOutput on first run:
=== Generated API Key (save this!) ===
a1b2c3d4e5f6...
=======================================
KeyForge server listening on :9315
Open https://keyforge.yurii.live to access the Web UI (login with the API key).
The easiest way to enroll a device:
- Open the Web UI β Tokens β Quick Enroll
- Fill in: device name, toggle "Accepts SSH", set sync interval, set expiry
- Click Generate Enrollment Link
- Copy the one-liner and run it on the target device:
curl -sSL https://keyforge.yurii.live/e/48291037 | sh
That's it β the device generates an SSH key, registers with KeyForge, and sets up cron sync if configured.
On the machine you want to enroll:
# First, create an enrollment token (on any machine with the API key)
./keyforge token create --label "for-my-laptop" --expires 1h \
--server https://keyforge.yurii.live --api-key YOUR_API_KEY
# Output: Token: xyz789...
# Then, on the device to enroll:
./keyforge enroll \
--name "my-laptop" \
--server https://keyforge.yurii.live \
--token xyz789This generates an SSH key (if needed) and registers the public key with KeyForge.
Servers use the same flow but add --accept-ssh so they receive all keys:
./keyforge enroll \
--name "web-server" \
--server https://keyforge.yurii.live \
--token abc123 \
--accept-ssh# One-time install
./keyforge keys --install --server https://keyforge.yurii.live
# Set up periodic sync (every 15 minutes)
./keyforge keys --install --cron 15m --server https://keyforge.yurii.liveThat's it. Your laptop can now SSH into the web server, and any future enrolled device will be able to as well.
Tip: Quick Enroll generates this command for you β no need to assemble flags manually.
Don't have the keyforge binary on the new device? Use the built-in enrollment script:
curl -sSL https://keyforge.yurii.live/enroll.sh | sh -s -- \
--name "lxc-nginx" \
--token abc123 \
--server https://keyforge.yurii.live \
--accept-ssh \
--sync-interval 15mThis downloads a shell script that generates keys, registers with the server, installs authorized_keys, and sets up cron β all in one command.
When creating a new LXC in Proxmox:
- Open the KeyForge Web UI at
/authorized-keys - Click Copy All
- Paste into the Proxmox SSH key field during LXC creation
- After the LXC boots, enroll it:
curl -sSL https://keyforge.yurii.live/enroll.sh | sh -s -- \ --name "lxc-$(hostname)" \ --token TOKEN \ --server https://keyforge.yurii.live \ --accept-ssh \ --sync-interval 15m
keyforge serve [--port 9315] [--data ./keyforge-data] [--url https://keyforge.example.com]# Create a token (single-use, time-limited)
keyforge token create --label "for-pixel-8" --expires 1h \
--server URL --api-key KEY
# List all tokens
keyforge token list --server URL --api-key KEY
# Delete a token
keyforge token delete TOKEN_ID --server URL --api-key KEY# Enroll this device (generates key if needed)
keyforge enroll --name "device-name" --token TOKEN \
--server URL [--accept-ssh] [--key ~/.ssh/id_ed25519]# List devices
keyforge device list --server URL --api-key KEY
# Manually add a device (paste an existing public key)
keyforge device add --name "old-laptop" --key "ssh-ed25519 AAAA..." \
--server URL --api-key KEY [--accept-ssh] [--tags "linux,home"]
# Revoke a device (excluded from future syncs)
keyforge device revoke --name "lost-phone" --server URL --api-key KEY
# Reactivate a revoked device
keyforge device reactivate --name "found-phone" --server URL --api-key KEY
# Permanently delete
keyforge device delete --name "old-device" --server URL --api-key KEY# Print all active public keys to stdout
keyforge keys --server URL
# Install to ~/.ssh/authorized_keys (managed section only)
keyforge keys --install --server URL
# Install + set up cron sync
keyforge keys --install --cron 15m --server URL# Push keys to a specific server via SSH
keyforge push --target root@192.168.1.50 --server URL| Method | Path | Description |
|---|---|---|
GET |
/api/v1/authorized_keys |
All active public keys as plain text |
GET |
/api/v1/health |
Health check |
GET |
/enroll.sh |
Enrollment shell script |
GET |
/install.sh |
Install shell script |
GET |
/download |
Download page |
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/devices |
List all devices |
GET |
/api/v1/devices/:id |
Get a device |
POST |
/api/v1/devices |
Register device (API key or enrollment token) |
PATCH |
/api/v1/devices/:id |
Update device |
DELETE |
/api/v1/devices/:id |
Delete device |
POST |
/api/v1/devices/:id/revoke |
Revoke device |
POST |
/api/v1/devices/:id/reactivate |
Reactivate device |
POST |
/api/v1/tokens |
Create enrollment token |
GET |
/api/v1/tokens |
List tokens |
DELETE |
/api/v1/tokens/:id |
Delete token |
The /api/v1/authorized_keys endpoint returns plain text β pipe it directly into authorized_keys:
curl -s https://keyforge.yurii.live/api/v1/authorized_keys >> ~/.ssh/authorized_keysKeyForge uses managed section markers in authorized_keys:
# manually added key stays untouched
ssh-rsa AAAA... admin@jumpbox
# --- KeyForge Managed Keys (DO NOT EDIT) ---
ssh-ed25519 AAAA... my-laptop
ssh-ed25519 AAAA... web-server
ssh-ed25519 AAAA... pixel-8
# --- End KeyForge Managed Keys ---
Only the section between the markers is updated during sync. Your manually-added keys are never touched.
Instead of syncing keys via cron, you can configure sshd to query KeyForge on every login attempt:
-
Copy the
keyforgebinary to a system-wide location:sudo cp keyforge /usr/local/bin/keyforge
-
Edit
/etc/ssh/sshd_config:AuthorizedKeysCommand /usr/local/bin/keyforge keys --server https://keyforge.yurii.live AuthorizedKeysCommandUser nobody -
Restart sshd:
sudo systemctl restart sshd
On every SSH login attempt, sshd runs the keyforge keys command. It fetches all active public keys from the KeyForge server and returns them to sshd for authentication.
If the KeyForge server is unreachable, the command automatically returns the last successfully fetched keys from a local cache file (~/.cache/keyforge/authorized_keys.cache or /var/cache/keyforge/authorized_keys.cache for root). This prevents SSH lockout during server outages.
To disable caching: keyforge keys --server URL --no-cache
| Cron Sync | AuthorizedKeysCommand | |
|---|---|---|
| Key freshness | Delay (cron interval) | Real-time |
| Revocation speed | Minutes | Instant |
| Server dependency | Only during sync | Every SSH login |
| Offline behavior | Stale file works | Cache fallback |
| Setup complexity | Simple | Requires sshd_config |
- Private keys never leave devices. KeyForge only stores and distributes public keys.
- Enrollment tokens are single-use and time-limited (default: 1 hour). They prevent unauthorized device registration.
- API key is auto-generated on first run and stored in the SQLite database. Used for management operations.
- Web UI requires login with the API key (session cookie, 24h expiry).
- The
/authorized_keysendpoint is unauthenticated by design β public keys are inherently public. They reveal identity but grant no access. - For production: put KeyForge behind a reverse proxy with TLS, or access it over Tailscale.
# Build for current platform
make build
# Cross-compile (example)
GOOS=linux GOARCH=amd64 go build -o keyforge-linux-amd64 ./cmd/keyforge
GOOS=linux GOARCH=arm64 go build -o keyforge-linux-arm64 ./cmd/keyforge
# Run tests
make test- Go β single static binary, zero runtime dependencies
- SQLite (modernc.org/sqlite) β embedded, pure Go, no CGo
- Cobra β CLI framework
- htmx β dynamic Web UI without JavaScript build pipelines
- golang.org/x/crypto/ssh β key generation and fingerprinting