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>
301 lines
7.2 KiB
Markdown
301 lines
7.2 KiB
Markdown
# SSH Key Management
|
|
|
|
Per-client SSH key isolation ensures that compromise of one client server does not grant access to other client servers.
|
|
|
|
## Architecture
|
|
|
|
Each client gets a **dedicated SSH key pair**:
|
|
- **Private key**: `keys/ssh/<client_name>` (gitignored, never committed)
|
|
- **Public key**: `keys/ssh/<client_name>.pub` (committed to repository)
|
|
|
|
## Security Benefits
|
|
|
|
| Benefit | Description |
|
|
|---------|-------------|
|
|
| **Isolation** | Compromising one client ≠ compromising others |
|
|
| **Granular Rotation** | Rotate keys per-client without affecting others |
|
|
| **Access Control** | Different teams can have access to different client keys |
|
|
| **Auditability** | Track which key accessed which server |
|
|
|
|
## Generating Keys for New Clients
|
|
|
|
### Automated (Recommended)
|
|
|
|
```bash
|
|
# Generate key pair for new client
|
|
./scripts/generate-client-keys.sh newclient
|
|
|
|
# Output:
|
|
# ✓ SSH key pair generated successfully
|
|
# Private key: keys/ssh/newclient
|
|
# Public key: keys/ssh/newclient.pub
|
|
```
|
|
|
|
### Manual
|
|
|
|
```bash
|
|
# Create keys directory
|
|
mkdir -p keys/ssh
|
|
|
|
# Generate ED25519 key pair
|
|
ssh-keygen -t ed25519 \
|
|
-f keys/ssh/newclient \
|
|
-C "client-newclient-deploy-key" \
|
|
-N ""
|
|
|
|
# Verify generation
|
|
ls -la keys/ssh/newclient*
|
|
```
|
|
|
|
## Using Client SSH Keys
|
|
|
|
### With SSH Command
|
|
|
|
```bash
|
|
# Connect to client server
|
|
ssh -i keys/ssh/dev root@78.47.191.38
|
|
|
|
# Run command on client server
|
|
ssh -i keys/ssh/dev root@78.47.191.38 "docker ps"
|
|
```
|
|
|
|
### With Ansible
|
|
|
|
Ansible automatically uses the correct key per client:
|
|
|
|
```bash
|
|
# Deploy to specific client (uses client-specific key)
|
|
ansible-playbook -i hcloud.yml playbooks/deploy.yml --limit dev
|
|
```
|
|
|
|
The dynamic inventory provides the correct host, and OpenTofu ensures the server has the matching public key.
|
|
|
|
### Adding to SSH Config
|
|
|
|
```bash
|
|
# ~/.ssh/config
|
|
Host dev.vrije.cloud
|
|
User root
|
|
IdentityFile ~/path/to/infrastructure/keys/ssh/dev
|
|
StrictHostKeyChecking no
|
|
|
|
Host newclient.vrije.cloud
|
|
User root
|
|
IdentityFile ~/path/to/infrastructure/keys/ssh/newclient
|
|
StrictHostKeyChecking no
|
|
```
|
|
|
|
Then simply: `ssh dev.vrije.cloud`
|
|
|
|
## Key Rotation
|
|
|
|
### When to Rotate
|
|
|
|
- **Annually**: Routine rotation (best practice)
|
|
- **On Compromise**: Immediately if key suspected compromised
|
|
- **On Departure**: When team member with key access leaves
|
|
- **On Audit**: During security audits
|
|
|
|
### Rotation Procedure
|
|
|
|
1. **Generate new key**:
|
|
```bash
|
|
# Backup old key
|
|
cp keys/ssh/dev keys/ssh/dev.old
|
|
cp keys/ssh/dev.pub keys/ssh/dev.pub.old
|
|
|
|
# Generate new key (overwrites old)
|
|
./scripts/generate-client-keys.sh dev
|
|
```
|
|
|
|
2. **Update OpenTofu** (will recreate server):
|
|
```bash
|
|
cd tofu
|
|
tofu apply
|
|
# Server will be recreated with new key
|
|
```
|
|
|
|
3. **Test new key**:
|
|
```bash
|
|
ssh -i keys/ssh/dev root@<new_ip> hostname
|
|
```
|
|
|
|
4. **Remove old key backup**:
|
|
```bash
|
|
rm keys/ssh/dev.old keys/ssh/dev.pub.old
|
|
```
|
|
|
|
### Zero-Downtime Rotation (Advanced)
|
|
|
|
For production clients where downtime is unacceptable:
|
|
|
|
1. Generate new key with temporary name
|
|
2. Add both keys to server via OpenTofu
|
|
3. Test new key works
|
|
4. Remove old key from OpenTofu
|
|
5. Update local key file
|
|
|
|
## Key Storage & Backup
|
|
|
|
### Local Storage
|
|
|
|
```
|
|
keys/ssh/
|
|
├── .gitignore # Protects private keys from git
|
|
├── dev # Private key (gitignored)
|
|
├── dev.pub # Public key (committed)
|
|
├── client1 # Private key (gitignored)
|
|
├── client1.pub # Public key (committed)
|
|
└── README.md # Documentation (to be created)
|
|
```
|
|
|
|
### Backup Strategy
|
|
|
|
**Private keys must be backed up securely:**
|
|
|
|
1. **Password Manager** (Recommended):
|
|
- Store in 1Password, Bitwarden, or similar
|
|
- Tag with "ssh-key" and client name
|
|
- Include server IP and hostname
|
|
|
|
2. **Encrypted Backup**:
|
|
```bash
|
|
# Create encrypted archive
|
|
tar czf - keys/ssh/ | gpg -c > ssh-keys-backup.tar.gz.gpg
|
|
|
|
# Store backup in secure location (NOT in git)
|
|
```
|
|
|
|
3. **Team Shared Vault**:
|
|
- Use team password manager
|
|
- Ensure key escrow for bus factor
|
|
- Document who has access
|
|
|
|
**⚠️ NEVER commit private keys to git!**
|
|
|
|
The `.gitignore` file protects you, but double-check:
|
|
```bash
|
|
# Verify no private keys in git
|
|
git ls-files keys/ssh/
|
|
|
|
# Should only show:
|
|
# keys/ssh/.gitignore
|
|
# keys/ssh/README.md
|
|
# keys/ssh/*.pub (public keys only)
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### "Permission denied (publickey)"
|
|
|
|
**Cause**: Server doesn't have the public key or wrong private key used.
|
|
|
|
**Solution**:
|
|
```bash
|
|
# 1. Verify public key is in OpenTofu state
|
|
cd tofu
|
|
tofu state show 'hcloud_ssh_key.client["dev"]'
|
|
|
|
# 2. Verify server has the key
|
|
ssh-keygen -lf keys/ssh/dev.pub # Get fingerprint
|
|
# Compare with Hetzner Cloud Console → Server → SSH Keys
|
|
|
|
# 3. Use correct private key
|
|
ssh -i keys/ssh/dev root@<server_ip>
|
|
```
|
|
|
|
### "No such file or directory: keys/ssh/dev"
|
|
|
|
**Cause**: SSH key not generated yet.
|
|
|
|
**Solution**:
|
|
```bash
|
|
./scripts/generate-client-keys.sh dev
|
|
```
|
|
|
|
### "Connection refused"
|
|
|
|
**Cause**: Server not yet booted or firewall blocking SSH.
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Wait for server to boot (check Hetzner Console)
|
|
# Check firewall rules allow your IP
|
|
cd tofu
|
|
tofu state show 'hcloud_firewall.client_firewall'
|
|
```
|
|
|
|
### Key Permissions Wrong
|
|
|
|
**Cause**: Private key has incorrect permissions.
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Private keys must be 600
|
|
chmod 600 keys/ssh/dev
|
|
|
|
# Public keys should be 644
|
|
chmod 644 keys/ssh/dev.pub
|
|
```
|
|
|
|
## Migration from Shared Key
|
|
|
|
If migrating from a shared SSH key setup:
|
|
|
|
1. **Generate per-client keys**:
|
|
```bash
|
|
for client in dev client1 client2; do
|
|
./scripts/generate-client-keys.sh $client
|
|
done
|
|
```
|
|
|
|
2. **Update OpenTofu**:
|
|
- Remove `hcloud_ssh_key.default` resource
|
|
- Update `hcloud_server.client` to use `hcloud_ssh_key.client[each.key].id`
|
|
|
|
3. **Apply changes** (will recreate servers):
|
|
```bash
|
|
cd tofu
|
|
tofu apply
|
|
```
|
|
|
|
4. **Update Ansible/scripts** to use new keys
|
|
|
|
5. **Remove old shared key** from Hetzner Cloud Console
|
|
|
|
## Best Practices
|
|
|
|
✅ **DO**:
|
|
- Generate unique keys per client
|
|
- Use ED25519 algorithm (modern, secure, fast)
|
|
- Backup private keys securely
|
|
- Rotate keys annually
|
|
- Document key ownership
|
|
- Use descriptive comments in keys
|
|
|
|
❌ **DON'T**:
|
|
- Reuse keys between clients
|
|
- Share private keys via email/Slack
|
|
- Commit private keys to git
|
|
- Use weak SSH algorithms (RSA < 4096, DSA)
|
|
- Store keys in unencrypted cloud storage
|
|
- Forget to backup keys
|
|
|
|
## Key Specifications
|
|
|
|
| Property | Value | Rationale |
|
|
|----------|-------|-----------|
|
|
| Algorithm | ED25519 | Modern, secure, fast, small keys |
|
|
| Key Size | 256 bits | Standard for ED25519 |
|
|
| Comment | `client-<name>-deploy-key` | Identifies key purpose |
|
|
| Passphrase | None (empty) | Automation requires no passphrase |
|
|
| Permissions | 600 (private), 644 (public) | Standard SSH security |
|
|
|
|
**Note on Passphrases**: Automation keys typically have no passphrase. If adding a passphrase, use `ssh-agent` to avoid prompts during deployment.
|
|
|
|
## See Also
|
|
|
|
- [OpenTofu Configuration](../tofu/main.tf) - SSH key resources
|
|
- [Deployment Scripts](../scripts/deploy-client.sh) - Uses client keys
|
|
- [Issue #14](https://github.com/Post-X-Society/post-tyranny-tech-infrastructure/issues/14) - Original requirement
|
|
- [Architecture Decisions](./architecture-decisions.md) - Security baseline
|