Post-Tyranny-Tech-Infrastru.../keys/ssh/README.md
Pieter 071ed083f7 feat: Implement per-client SSH key isolation
Resolves #14

Each client now gets a dedicated SSH key pair, ensuring that compromise
of one client server does not grant access to other client servers.

## Changes

### Infrastructure (OpenTofu)
- Replace shared `hcloud_ssh_key.default` with per-client `hcloud_ssh_key.client`
- Each client key read from `keys/ssh/<client_name>.pub`
- Server recreated with new key (dev server only, acceptable downtime)

### Key Management
- Created `keys/ssh/` directory for SSH keys
- Added `.gitignore` to protect private keys from git
- Generated ED25519 key pair for dev client
- Private key gitignored, public key committed

### Scripts
- **`scripts/generate-client-keys.sh`** - Generate SSH key pairs for clients
- Updated `scripts/deploy-client.sh` to check for client SSH key

### Documentation
- **`docs/ssh-key-management.md`** - Complete SSH key management guide
- **`keys/ssh/README.md`** - Quick reference for SSH keys directory

### Configuration
- Removed `ssh_public_key` variable from `variables.tf`
- Updated `terraform.tfvars` to remove shared SSH key reference
- Updated `terraform.tfvars.example` with new key generation instructions

## Security Improvements

 Client isolation: Each client has dedicated SSH key
 Granular rotation: Rotate keys per-client without affecting others
 Defense in depth: Minimize blast radius of key compromise
 Proper key storage: Private keys gitignored, backups documented

## Testing

-  Generated new SSH key for dev client
-  Applied OpenTofu changes (server recreated)
-  Tested SSH access: `ssh -i keys/ssh/dev root@78.47.191.38`
-  Verified key isolation: Old shared key removed from Hetzner

## Migration Notes

For existing clients:
1. Generate key: `./scripts/generate-client-keys.sh <client>`
2. Apply OpenTofu: `cd tofu && tofu apply` (will recreate server)
3. Deploy: `./scripts/deploy-client.sh <client>`

For new clients:
1. Generate key first
2. Deploy as normal

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-17 19:50:30 +01:00

196 lines
4 KiB
Markdown

# SSH Keys Directory
This directory contains **per-client SSH key pairs** for server access.
## Purpose
Each client gets a dedicated SSH key pair to ensure:
- **Isolation**: Compromise of one client ≠ access to others
- **Granular control**: Rotate or revoke keys per-client
- **Security**: Defense in depth, minimize blast radius
## Files
```
keys/ssh/
├── .gitignore # Protects private keys from git
├── README.md # This file
├── dev # Private key for dev server (gitignored)
├── dev.pub # Public key for dev server (committed)
├── client1 # Private key for client1 (gitignored)
└── client1.pub # Public key for client1 (committed)
```
## Generating Keys
Use the helper script:
```bash
./scripts/generate-client-keys.sh <client_name>
```
Or manually:
```bash
ssh-keygen -t ed25519 -f keys/ssh/<client_name> -C "client-<client_name>-deploy-key" -N ""
```
## Security
### What Gets Committed
-**Public keys** (`*.pub`) - Safe to commit
-**README.md** - Documentation
-**`.gitignore`** - Protection rules
### What NEVER Gets Committed
-**Private keys** (no `.pub` extension) - Gitignored
-**Temporary files** - Gitignored
-**Backup keys** - Gitignored
The `.gitignore` file in this directory ensures private keys are never committed:
```gitignore
# NEVER commit SSH private keys
*
# Allow README and public keys only
!.gitignore
!README.md
!*.pub
```
## Backup Strategy
**⚠️ IMPORTANT: Backup private keys securely!**
Private keys must be backed up to prevent lockout:
1. **Password Manager** (Recommended):
- Store in 1Password, Bitwarden, etc.
- Tag with client name and server IP
2. **Encrypted Archive**:
```bash
tar czf - keys/ssh/ | gpg -c > ssh-keys-backup.tar.gz.gpg
```
3. **Team Vault**:
- Share securely with team members who need access
- Document key ownership
## Usage
### SSH Connection
```bash
# Connect to client server
ssh -i keys/ssh/dev root@<server_ip>
# Run command
ssh -i keys/ssh/dev root@<server_ip> "docker ps"
```
### Ansible
Ansible automatically uses the correct key (via dynamic inventory and OpenTofu):
```bash
ansible-playbook -i hcloud.yml playbooks/deploy.yml --limit dev
```
### SSH Config
Add to `~/.ssh/config` for convenience:
```
Host dev.vrije.cloud
User root
IdentityFile ~/path/to/infrastructure/keys/ssh/dev
```
Then: `ssh dev.vrije.cloud`
## Key Rotation
Rotate keys annually or on security events:
```bash
# Generate new key (backs up old automatically)
./scripts/generate-client-keys.sh dev
# Apply to server (recreates server with new key)
cd tofu && tofu apply
# Test new key
ssh -i keys/ssh/dev root@<new_ip> hostname
```
## Verification
### Check Key Fingerprint
```bash
# Show fingerprint of private key
ssh-keygen -lf keys/ssh/dev
# Show fingerprint of public key
ssh-keygen -lf keys/ssh/dev.pub
# Should match!
```
### Check What's in Git
```bash
# Verify no private keys committed
git ls-files keys/ssh/
# Should only show:
# keys/ssh/.gitignore
# keys/ssh/README.md
# keys/ssh/*.pub
```
### Check Permissions
```bash
# Private keys must be 600
ls -la keys/ssh/dev
# Should show: -rw------- (600)
# Fix if needed:
chmod 600 keys/ssh/*
chmod 644 keys/ssh/*.pub
```
## Troubleshooting
### "Permission denied (publickey)"
1. Check you're using the correct private key for the client
2. Verify public key is on server (check OpenTofu state)
3. Ensure private key has correct permissions (600)
### "No such file or directory"
Generate the key first:
```bash
./scripts/generate-client-keys.sh <client_name>
```
### "Bad permissions"
Fix key permissions:
```bash
chmod 600 keys/ssh/<client_name>
chmod 644 keys/ssh/<client_name>.pub
```
## See Also
- [../docs/ssh-key-management.md](../../docs/ssh-key-management.md) - Complete SSH key management guide
- [../../scripts/generate-client-keys.sh](../../scripts/generate-client-keys.sh) - Key generation script
- [../../tofu/main.tf](../../tofu/main.tf) - OpenTofu SSH key resources