- Go 88.8%
- Shell 11.2%
Basefile install task now downloads the arch-matched binary from the Gitea release instead of compiling from source — avoids the multi-minute Go toolchain download and build inside the container. release.sh cross-builds linux/amd64 and linux/arm64, tags the version from Basefile, and uploads assets via tea. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| docs/superpowers | ||
| internal | ||
| .gitignore | ||
| Basefile | ||
| CLAUDE.md | ||
| deploy.md | ||
| deploy.sh | ||
| go.mod | ||
| go.sum | ||
| keys.openrc | ||
| log.md | ||
| main.go | ||
| README.md | ||
| release.sh | ||
| tsnet.go | ||
keys
Managing SSH keys across multiple servers is tedious. You either paste the same key into every authorized_keys file by hand, or set up something complex like LDAP. keys is the simple middle ground: one place to manage your SSH public keys, served over HTTP so any server can consume them with a single config line.
It also solves the new-device problem: when you get a new laptop and SSH in with an unknown key, instead of getting "Permission denied", the server records the key as pending and notifies you next time you log in from an approved device. One keypress to approve it.
How it works
- HTTP server on
:8080— serves your keys as anauthorized_keysfile; pointAuthorizedKeysURLat it on any sshd - SSH server on
:22— an interactive TUI for managing keys, itself protected by the managed keys - Auto-bootstrap — first ever connection is automatically approved, so setup requires no CLI work
- Pending keys — unknown SSH keys are saved as pending; the next authorized login can approve or reject them
- Keys are stored in
~/.local/share/keys/keys.json(XDG-aware)
Quick start
go install code.bas.es/arne/keys@latest
keys serve
The first time you SSH in, your key is automatically approved:
ssh <host> # bootstraps your key — opens the TUI
On any other server, add one line to /etc/ssh/sshd_config:
AuthorizedKeysURL http://keys-host:8080/
TUI
Connect from any approved device to manage your keys:
ssh keys-host
| Key | Action |
|---|---|
a |
Add a new key manually |
d |
Delete selected key |
r |
Rename selected key |
p |
Review pending keys (shown when there are pending keys) |
q / esc |
Quit / go back |
Approving a new device
When a new key attempts to connect, they see:
Key submitted for approval.
The next time you log in from an approved device, the help bar shows [p] pending (1). Press p, select the key, press a, give it a label — done. The new device can now connect.
CLI
keys serve start HTTP + SSH servers
| Flag | Env var | Default |
|---|---|---|
-http-addr |
KEYS_HTTP_ADDR |
:8080 |
-ssh-addr |
KEYS_SSH_ADDR |
:22 |
-host-key |
KEYS_HOST_KEY |
./host_key |
-store |
KEYS_STORE |
~/.local/share/keys/keys.json |
Integration
sshd AuthorizedKeysURL
# /etc/ssh/sshd_config
AuthorizedKeysURL http://keys:8080/
Any HTTP request to the keys server returns the full authorized_keys content.
Fetch on login
# ~/.profile or equivalent
curl -sf http://keys:8080/ >> ~/.ssh/authorized_keys
Deployment
keys is deployed via bases — a self-hosted app platform built on Incus. You'll need a running bases instance.
In your bases dashboard, press Install and enter:
arne/keys
That's the shorthand for code.bas.es/arne/keys. Bases reads the included Basefile, provisions an Alpine container, builds the binary, and starts the service.
Once running, the service topology looks like this:
Internet
└── keys.example.com (Caddy, TLS termination)
└── Tailscale → keys container :8080
Tailscale network
└── ssh keys → TUI