No description
  • Go 89%
  • Shell 11%
Find a file
Arne Skaar Fismen 87c2cd1689 Tag v0.1.3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 08:06:23 +01:00
internal tui: fix selector border color — rebuild delegate styles from session renderer 2026-03-15 08:05:07 +01:00
.gitignore Auto-bootstrap first key via SSH, remove manual add/list/remove commands 2026-03-14 20:18:02 +01:00
CLAUDE.md Add CLAUDE.md and log.md 2026-03-14 23:09:50 +01:00
deploy.md Fix service config, default to port 22, add tests and deploy script 2026-03-14 21:06:12 +01:00
deploy.sh deploy.sh: --dev implies --update 2026-03-15 08:01:31 +01:00
go.mod Fix go.mod version, fix deploy.sh for Alpine Go version and ssh-keygen package 2026-03-14 21:14:59 +01:00
go.sum Fix go.mod version, fix deploy.sh for Alpine Go version and ssh-keygen package 2026-03-14 21:14:59 +01:00
keys.openrc Fix service config, default to port 22, add tests and deploy script 2026-03-14 21:06:12 +01:00
log.md Tag v0.1.3 2026-03-15 08:06:23 +01:00
main.go Refactor into internal packages 2026-03-15 07:04:07 +01:00
README.md README rewrite, TUI colors, tag v0.1.0 2026-03-15 07:10:14 +01:00

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 an authorized_keys file; point AuthorizedKeysURL at 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

See deploy.md for the full setup guide: Incus container on Alpine Linux with its own Tailscale node, Caddy reverse proxy for HTTPS.

Internet
└── keys.example.com (Caddy, TLS termination)
    └── Tailscale → keys container :8080

Tailscale network
└── ssh keys → TUI