A secure, intelligent tool for backing up SSH keys and configuration to HashiCorp Vault with client-side encryption and perfect permission preservation.
- Zero-knowledge: Vault server never sees your SSH keys in plaintext
- Strong key derivation: PBKDF2 with 100,000 iterations
- Integrity verification: MD5 checksums for all files
- Perfect permission preservation: Exact SSH file permissions maintained and verified
- Permission validation: Critical warnings for insecure SSH key permissions
- Directory security: SSH directory automatically secured to 0700 permissions
- Automatic detection: RSA, PEM, Ed25519, ECDSA, OpenSSH formats
- Service categorization: GitHub, GitLab, BitBucket, ArgoCD, AWS, GCP, etc.
- Key pair relationships: Links private/public key pairs automatically
- System file recognition: Config, known_hosts, authorized_keys
- Smart categorization: Personal, work, service, and system files
- Complete SSH Directory Backup with metadata
- Selective File Restore with permission verification
- Cross-machine/user restore - backup on one machine, restore anywhere
- Flexible storage strategies for different use cases and team environments
- Interactive Mode for file and backup selection
- Dry-run Support for safe testing
- Multiple Backup Versions with retention
- Cross-platform Support (Linux, macOS, Windows)
- Container Ready with Docker and Podman support
- CI/CD Integration friendly
- Permission Security validation and warnings
- Migration tools for upgrading from legacy storage
SSH Secret Keeper includes a modern landing page built with React and TypeScript:
- Live Site: Available via GitHub Pages (automatically deployed)
- Technology: React 18, TypeScript, Vite, Tailwind CSS
- CI/CD: Automated building and deployment pipeline
- Development: Full development environment with hot reload
# Navigate to site directory
cd site/
# Start development server
make dev
# Build for production
make build
# Run quality checks
make lint && make type-checkSee site/README.md for detailed development documentation.
# Quick installation - detects your OS and installs automatically
curl -sSL https://raw.githubusercontent.com/rafaelvzago/ssh-secret-keeper/refs/heads/main/install.sh | bashInstallation Options:
# Install to user directory (no sudo required)
curl -sSL https://raw.githubusercontent.com/rafaelvzago/ssh-secret-keeper/refs/heads/main/install.sh | bash -s -- --user
# Install specific version
curl -sSL https://raw.githubusercontent.com/rafaelvzago/ssh-secret-keeper/refs/heads/main/install.sh | bash -s -- --version 1.2.0
# Install to custom directory
curl -sSL https://raw.githubusercontent.com/rafaelvzago/ssh-secret-keeper/refs/heads/main/install.sh | bash -s -- --install-dir /opt/bin
# View all options
curl -sSL https://raw.githubusercontent.com/rafaelvzago/ssh-secret-keeper/refs/heads/main/install.sh | bash -s -- --helpThe installation script:
- ✅ Auto-detects your OS and architecture (Linux/macOS, amd64/arm64)
- ✅ Downloads the appropriate binary from GitHub releases
- ✅ Verifies checksums for security
- ✅ Installs to system (
/usr/local/bin) or user directory (~/.local/bin) - ✅ Handles existing installations and updates
- ✅ Fallback to building from source if needed
Once installed, SSH Secret Keeper can update itself to the latest version:
# Check for updates
sshsk update --check
# Update to latest version
sshsk update
# Update to specific version
sshsk update --version v1.0.5See Update Documentation for more details.
# Download latest release (replace VERSION and ARCH)
curl -L https://github.com/rafaelvzago/ssh-secret-keeper/releases/latest/download/ssh-secret-keeper-VERSION-linux-amd64.tar.gz -o sshsk.tar.gz
tar -xzf sshsk.tar.gz
chmod +x sshsk
sudo mv sshsk /usr/local/bin/git clone https://github.com/rafaelvzago/ssh-secret-keeper
cd ssh-secret-keeper
make build
sudo make install# Using Docker
docker pull rafaelvzago/ssh-secret-keeper:latest
docker run --rm -v ~/.ssh:/ssh -v ~/.ssh-secret-keeper:/config rafaelvzago/ssh-secret-keeper analyze
# Using Podman
podman pull rafaelvzago/ssh-secret-keeper:latest
podman run --rm -v ~/.ssh:/ssh -v ~/.ssh-secret-keeper:/config rafaelvzago/ssh-secret-keeper analyze- HashiCorp Vault server (local or remote)
- Valid Vault token with KV v2 permissions
- SSH directory with keys to backup (~/.ssh)
SSH Secret Keeper supports two authentication approaches:
Perfect for containers, CI/CD, and environments where no local files should be stored.
# Set required environment variables
export VAULT_ADDR="https://your-vault-server:8200" # Replace with your actual Vault server address
export VAULT_TOKEN="your-vault-token" # Your Vault authentication token
# Initialize (no config files created)
sshsk init
# Use any command - everything works with environment variables
sshsk backup my-backup
sshsk statusUses local configuration and token files for persistent setups.
# Set Vault address (required)
export VAULT_ADDR="https://your-vault-server:8200"
# Initialize with token flag (creates config and token files)
sshsk init --token "your-vault-token"Important Notes:
VAULT_ADDRenvironment variable is required for all operationsVAULT_TOKENenvironment variable takes priority over token files- When using environment variables, no local files are created or required
- The application will fail with clear error messages if authentication is missing
# See what SSH files you have
sshsk analyze --verboseExample output:
SSH Directory Analysis
======================
Summary:
Total files: 28
Key pairs: 14
Service keys: 14 (GitHub, GitLab, ArgoCD, etc.)
Personal keys: 10
Work keys: 1
System files: 3
Key Pairs Found:
- service1_rsa (Complete pair) [0600/0644]
- service2_rsa (Complete pair) [0600/0644]
- service3_rsa (Complete pair) [0600/0644]
- id_rsa (Complete pair) [0600/0644]
Permission Summary:
- 0600: 14 files (private keys)
- 0644: 14 files (public keys, config)
# Backup everything (you'll be prompted for encryption passphrase)
sshsk backup
# Or with a custom name using variables
BACKUP_NAME="laptop-$(hostname)-$(date +%Y%m%d)"
sshsk backup "${BACKUP_NAME}"# List available backups
sshsk list --detailed
# Restore the most recent backup
sshsk restore
# Or restore to a specific directory using variables
RESTORE_DIR="/tmp/restored-ssh-$(date +%Y%m%d)"
sshsk restore --target-dir "${RESTORE_DIR}"By default, SSH Secret Keeper now uses universal storage which enables cross-machine and cross-user restore. For existing users with the legacy machine-user storage:
# Check current storage strategy
sshsk migrate-status
# Migrate to universal storage (recommended)
sshsk migrate --from machine-user --to universal --dry-run # Preview first
sshsk migrate --from machine-user --to universal --cleanup # Perform migration
# Now you can restore backups on any machine!Storage Strategies:
- Universal (default): Backup on laptop, restore on desktop/server
- User-scoped: Per-user isolation with cross-machine capability
- Machine-user (legacy): Tied to specific machine-user combination
- Custom: Team/project-based organization
See Storage Strategies Guide for detailed information.
| Command | Description | Example |
|---|---|---|
init |
Initialize configuration and Vault setup | sshsk init --vault-addr "${VAULT_ADDR}" |
backup |
Backup SSH directory to Vault | sshsk backup "${BACKUP_NAME}" |
restore |
Restore SSH backup from Vault | sshsk restore --select |
list |
List available backups | sshsk list --detailed |
delete |
Delete a backup from Vault | sshsk delete "${BACKUP_NAME}" --force |
analyze |
Analyze SSH directory structure | sshsk analyze --verbose |
status |
Show configuration and connection status | sshsk status --checksums |
migrate |
NEW: Migrate between storage strategies | sshsk migrate --from machine-user --to universal |
migrate-status |
NEW: Show storage strategy information | sshsk migrate-status |
# Interactive file selection
sshsk backup --interactive
# Dry run (preview only)
sshsk backup --dry-run
# Custom SSH directory using variables
SSH_DIR="/path/to/custom/ssh"
sshsk backup --ssh-dir "${SSH_DIR}"
# Custom backup name with timestamp
BACKUP_NAME="backup-$(hostname)-$(date +%Y%m%d-%H%M%S)"
sshsk backup "${BACKUP_NAME}"# Delete specific backup with confirmation
sshsk delete "backup-20240101-120000"
# Delete without confirmation prompt
sshsk delete "old-backup" --force
# Interactive backup selection for deletion
sshsk delete "" --interactive# Interactive backup selection
sshsk restore --select
# Restore specific files only
sshsk restore --files "github*,gitlab*"
# Restore to different location using variables
TARGET_DIR="/tmp/ssh-restore-$(date +%Y%m%d)"
sshsk restore --target-dir "${TARGET_DIR}"
# Interactive file selection
sshsk restore --interactive
# Combine interactive backup and file selection
sshsk restore --select --interactive
# Overwrite existing files
sshsk restore --overwrite# Show basic status
sshsk status
# Show MD5 checksums for most recent backup
sshsk status --checksums
# Show detailed info for specific backup with checksums
sshsk status "backup-20240101-120000" --checksums
# Skip vault connection check
sshsk status --vault=false
# Skip SSH directory check
sshsk status --ssh=falseConfiguration file location: ~/.ssh-secret-keeper/config.yaml
# Vault authentication - REQUIRED for all operations
export VAULT_ADDR="https://vault.company.com:8200" # Vault server address (REQUIRED)
export VAULT_TOKEN="your-vault-token-here" # Vault authentication token (REQUIRED)
# For Kubernetes clusters, set your cluster's Vault address:
# export VAULT_ADDR="http://your-vault-server:8200"All other configuration can be overridden with environment variables:
# Vault settings (optional overrides)
export SSHSK_VAULT_ADDRESS="https://vault.company.com:8200" # Alternative to VAULT_ADDR
export SSHSK_VAULT_TOKEN_FILE="/path/to/token" # Token file path (if not using VAULT_TOKEN)
export SSHSK_VAULT_MOUNT_PATH="ssh-backups" # Vault mount path
export SSHSK_VAULT_NAMESPACE="your-namespace" # Vault namespace (Enterprise)
export SSHSK_VAULT_TLS_SKIP_VERIFY="false" # Skip TLS verification (not recommended)
# Backup settings
export SSHSK_BACKUP_SSH_DIR="/custom/ssh/path" # Custom SSH directory
export SSHSK_BACKUP_RETENTION_COUNT="20" # Number of backups to keep
export SSHSK_BACKUP_HOSTNAME_PREFIX="true" # Include hostname in paths
# Security settings
export SSHSK_SECURITY_ALGORITHM="AES-256-GCM" # Encryption algorithm
export SSHSK_SECURITY_ITERATIONS="150000" # PBKDF2 iterations
export SSHSK_SECURITY_PER_FILE_ENCRYPT="true" # Encrypt each file separately
export SSHSK_SECURITY_VERIFY_INTEGRITY="true" # Verify checksums
# Logging settings
export SSHSK_LOGGING_LEVEL="info" # Log level: debug, info, warn, error
export SSHSK_LOGGING_FORMAT="console" # Log format: console, jsonThe application uses the following priority for authentication:
- VAULT_TOKEN environment variable (highest priority)
- Token file (fallback if VAULT_TOKEN not set)
- Error (if neither is available)
When both VAULT_ADDR and VAULT_TOKEN are set as environment variables:
- ✅ No configuration files are created
- ✅ No token files are stored locally
- ✅ Perfect for containers and CI/CD environments
- ✅ Enhanced security (no secrets on disk)
version: "1.0"
vault:
address: "https://vault.company.com:8200"
token_file: "~/.sshsk/token"
mount_path: "ssh-backups"
tls_skip_verify: false
backup:
ssh_dir: "~/.ssh"
hostname_prefix: true
retention_count: 10
include_patterns:
- "*.rsa"
- "*.pem"
- "*.pub"
- "id_rsa*"
- "config"
- "known_hosts*"
- "authorized_keys"
exclude_patterns:
- "*.tmp"
- "*.bak"
security:
algorithm: "AES-256-GCM"
key_derivation: "PBKDF2"
iterations: 100000
per_file_encrypt: true
verify_integrity: true# SSH Secret Keeper policy
path "ssh-backups/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "sys/mounts" {
capabilities = ["read"]
}
path "sys/mounts/ssh-backups" {
capabilities = ["create", "update"]
}# Set token TTL (time to live)
TOKEN_TTL="8760h" # 1 year
# Create a token with the policy
vault write auth/token/create policies=sshsk ttl="${TOKEN_TTL}"- User isolation: Each user gets their own namespace
- Policy-based access: Integrate with Vault policies
- Audit logging: All operations logged in Vault
- Compliance ready: SOC2, PCI DSS compatible
# .gitlab-ci.yml
variables:
VAULT_ADDR: "https://vault.company.com:8200"
backup_ssh_keys:
image: ghcr.io/rzago/sshsk:latest
variables:
BACKUP_NAME: "ci-${CI_COMMIT_SHA}-${CI_PIPELINE_ID}"
before_script:
# VAULT_TOKEN should be set as a CI/CD variable (masked)
- echo "Using Vault at ${VAULT_ADDR}"
script:
- sshsk init # No files created, uses env vars only
- sshsk backup "${BACKUP_NAME}"
only:
- main
restore_ssh_keys:
image: ghcr.io/rzago/sshsk:latest
script:
- sshsk init
- sshsk restore --target-dir /tmp/ssh-keys
- ls -la /tmp/ssh-keys/
when: manual# .github/workflows/ssh-backup.yml
name: SSH Backup
on:
push:
branches: [ main ]
jobs:
backup:
runs-on: ubuntu-latest
steps:
- name: Backup SSH Keys
env:
VAULT_ADDR: ${{ vars.VAULT_ADDR }}
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
run: |
curl -sSL https://raw.githubusercontent.com/rafaelvzago/ssh-secret-keeper/refs/heads/main/install.sh | bash
sshsk init
sshsk backup "github-${GITHUB_SHA}-${GITHUB_RUN_ID}"
restore:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- name: Restore SSH Keys
env:
VAULT_ADDR: ${{ vars.VAULT_ADDR }}
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
run: |
curl -sSL https://raw.githubusercontent.com/rafaelvzago/ssh-secret-keeper/refs/heads/main/install.sh | bash
sshsk init
sshsk restore --target-dir /tmp/ssh-keyspipeline {
agent any
environment {
VAULT_ADDR = 'https://vault.company.com:8200'
VAULT_TOKEN = credentials('vault-token')
}
stages {
stage('Backup SSH Keys') {
steps {
sh '''
curl -sSL https://raw.githubusercontent.com/rafaelvzago/ssh-secret-keeper/refs/heads/main/install.sh | bash
sshsk init
sshsk backup "jenkins-${BUILD_NUMBER}-${GIT_COMMIT}"
'''
}
}
}
}# /etc/cron.d/ssh-backup
# Daily backup at 2 AM using environment variables
0 2 * * * root /usr/bin/env VAULT_ADDR="https://vault.company.com:8200" VAULT_TOKEN="$(cat /etc/vault/token)" sshsk backup "daily-$(date +\%Y\%m\%d)" >> /var/log/ssh-backup.log 2>&1
# Weekly cleanup - keep only 30 days
0 3 * * 0 root /usr/bin/env VAULT_ADDR="https://vault.company.com:8200" VAULT_TOKEN="$(cat /etc/vault/token)" sshsk list | grep -E "daily-[0-9]{8}" | tail -n +30 | xargs -I {} sshsk delete {} --force#!/bin/bash
# setup-new-machine.sh - Automated SSH key restoration for new machines
set -e
# Configuration variables (set these in your environment or CI/CD)
VAULT_ADDR="${VAULT_ADDR:-https://vault.company.com:8200}"
VAULT_TOKEN="${VAULT_TOKEN}"
SSH_DIR="${HOME}/.ssh"
BACKUP_NAME="${BACKUP_NAME:-latest}"
# Validation
if [ -z "$VAULT_ADDR" ]; then
echo "Error: VAULT_ADDR environment variable is required"
exit 1
fi
if [ -z "$VAULT_TOKEN" ]; then
echo "Error: VAULT_TOKEN environment variable is required"
exit 1
fi
echo "Setting up SSH keys from Vault..."
# Install sshsk if not available
if ! command -v sshsk &> /dev/null; then
echo "Installing sshsk..."
curl -sSL https://raw.githubusercontent.com/rafaelvzago/ssh-secret-keeper/refs/heads/main/install.sh | bash
SSH_SECRET_KEEPER="sshsk"
else
SSH_SECRET_KEEPER="sshsk"
fi
# Initialize (no config files created)
echo "Initializing with environment variables..."
$SSH_SECRET_KEEPER init
# Restore SSH keys
echo "Restoring SSH backup: $BACKUP_NAME"
$SSH_SECRET_KEEPER restore "$BACKUP_NAME" --target-dir "$SSH_DIR"
# Secure SSH directory
chmod 700 "$SSH_DIR"
# Add keys to SSH agent if available
if command -v ssh-add &> /dev/null && [ -n "$SSH_AUTH_SOCK" ]; then
echo "Adding keys to SSH agent..."
find "$SSH_DIR" -name "id_*" -not -name "*.pub" -exec ssh-add {} \; 2>/dev/null || true
fi
echo "✅ SSH keys restored successfully!"
echo "SSH directory: $SSH_DIR"
$SSH_SECRET_KEEPER status --ssh-only
# Cleanup temporary binary
if [ "$SSH_SECRET_KEEPER" = "/tmp/sshsk" ]; then
rm -f /tmp/sshsk
fi# docker-compose.yml
version: '3.8'
services:
ssh-backup:
image: ghcr.io/rzago/sshsk:latest
environment:
- VAULT_ADDR=${VAULT_ADDR}
- VAULT_TOKEN=${VAULT_TOKEN}
volumes:
- ~/.ssh:/ssh:ro
command: >
sh -c "
sshsk init &&
sshsk backup 'scheduled-$(date +%Y%m%d-%H%M%S)'
"
profiles:
- backup
ssh-restore:
image: ghcr.io/rzago/sshsk:latest
environment:
- VAULT_ADDR=${VAULT_ADDR}
- VAULT_TOKEN=${VAULT_TOKEN}
volumes:
- ./restored-ssh:/ssh
command: >
sh -c "
sshsk init &&
sshsk restore --target-dir /ssh
"
profiles:
- restoreUsage:
# Set environment variables
export VAULT_ADDR="https://vault.company.com:8200"
export VAULT_TOKEN="your-vault-token"
# Run backup
docker-compose --profile backup up ssh-backup
# Run restore
docker-compose --profile restore up ssh-restorePerfect for containers - no configuration files needed:
# Configuration variables
IMAGE_NAME="sshsk"
IMAGE_TAG="latest"
REGISTRY="ghcr.io/rzago"
# Set your Vault credentials
export VAULT_ADDR="https://your-vault-server:8200"
export VAULT_TOKEN="your-vault-token"
# Analyze SSH directory with Docker (environment variables only)
docker run --rm \
-v ~/.ssh:/ssh:ro \
-e VAULT_ADDR="${VAULT_ADDR}" \
-e VAULT_TOKEN="${VAULT_TOKEN}" \
"${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" analyze
# Analyze SSH directory with Podman (environment variables only)
podman run --rm \
-v ~/.ssh:/ssh:ro \
-e VAULT_ADDR="${VAULT_ADDR}" \
-e VAULT_TOKEN="${VAULT_TOKEN}" \
"${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" analyze
# Initialize and backup with Docker (no config files created)
docker run --rm \
-v ~/.ssh:/ssh:ro \
-e VAULT_ADDR="${VAULT_ADDR}" \
-e VAULT_TOKEN="${VAULT_TOKEN}" \
"${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" init
docker run --rm \
-v ~/.ssh:/ssh:ro \
-e VAULT_ADDR="${VAULT_ADDR}" \
-e VAULT_TOKEN="${VAULT_TOKEN}" \
"${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" backup "container-backup-$(date +%Y%m%d)"
# Initialize and backup with Podman (no config files created)
podman run --rm \
-v ~/.ssh:/ssh:ro \
-e VAULT_ADDR="${VAULT_ADDR}" \
-e VAULT_TOKEN="${VAULT_TOKEN}" \
"${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" init
podman run --rm \
-v ~/.ssh:/ssh:ro \
-e VAULT_ADDR="${VAULT_ADDR}" \
-e VAULT_TOKEN="${VAULT_TOKEN}" \
"${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" backup "container-backup-$(date +%Y%m%d)"
# Restore SSH keys to a new location
docker run --rm \
-v /tmp/restored-ssh:/ssh \
-e VAULT_ADDR="${VAULT_ADDR}" \
-e VAULT_TOKEN="${VAULT_TOKEN}" \
"${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" restore --target-dir /sshIf you prefer using configuration files:
# Build image with Docker
docker build -t "${IMAGE_NAME}:${IMAGE_TAG}" .
# Build image with Podman
podman build -t "${IMAGE_NAME}:${IMAGE_TAG}" .
# Use with configuration files (traditional approach)
docker run --rm \
-v ~/.ssh:/ssh:ro \
-v ~/.sshsk:/config \
-e VAULT_ADDR="${VAULT_ADDR}" \
"${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" analyzeapiVersion: batch/v1
kind: CronJob
metadata:
name: ssh-backup
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: sshsk
image: ghcr.io/rzago/sshsk:latest
command:
- /bin/sh
- -c
- |
BACKUP_NAME="k8s-daily-$(date +%Y%m%d)"
sshsk backup "${BACKUP_NAME}"
env:
- name: VAULT_ADDR
value: "http://your-vault-server:8200" # Your Kubernetes cluster Vault address
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: vault-token
key: token
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
restartPolicy: OnFailureThis project follows SOLID principles and clean architecture patterns for maintainability and extensibility:
- Single Responsibility Principle: Each service has one clear purpose
- VaultStorageService: Handles only Vault storage operations
- FileAnalysisService: Handles only SSH file analysis
- EncryptionService: Handles only encryption/decryption operations
- ValidationService: Handles only input validation
- Open/Closed Principle: New detectors can be added via registry pattern
- Liskov Substitution Principle: All implementations are substitutable via interfaces
- Interface Segregation Principle: Focused, role-specific interfaces
- Dependency Inversion Principle: All components depend on abstractions, not concretions
┌─────────────────┐ ┌─────────────────────┐
│ CLI Commands │────│ BackupOrchestrator │
└─────────────────┘ └─────────────────────┘
│
┌────────┼────────┐
│ │ │
┌────────▼──┐ ┌──▼──────┐ │
│ Vault │ │ Crypto │ │
│ Storage │ │ Service │ │
│ Service │ └─────────┘ │
└───────────┘ │
┌─────────────────▼┐
│ File Services │
│ - Analysis │
│ - Read │
│ - Restore │
│ - Validation │
└──────────────────┘
- Service Registry Pattern: Pluggable SSH key detectors
- Dependency Injection: Constructor-based service wiring
- Strategy Pattern: Multiple encryption algorithms supported
- Factory Pattern: Service creation with proper configuration
- Repository Pattern: Abstract data access layer for Vault
- Test Coverage: 40%+ with unit and integration tests (target 85%)
- Error Handling: Structured, contextual error messages
- Logging: Structured logging with zerolog
- Validation: Comprehensive input validation throughout
- Security: Fail-safe defaults, secure by design
- Go 1.21+
- Make
git clone https://github.com/rafaelvzago/ssh-secret-keeper
cd sshsk
# Build for current platform
make build
# Build for all platforms
make build-all
# Create release
make release# Run unit tests
make test- Language: Go 1.21+
- Lines of Code: ~4,300+
- Test Coverage: 40%+ (growing test suite, target 85%)
- Dependencies: Minimal, security-focused
- Performance: <100ms for typical SSH directories
- Architecture: Clean architecture with SOLID principles
- Compatibility: Linux, macOS, ARM64
- ✅ No secrets on disk: Tokens only in memory
- ✅ Container-friendly: Perfect for ephemeral environments
- ✅ CI/CD secure: Integrates with secret management systems
- ✅ Process isolation: Environment variables are process-scoped
⚠️ Process visibility: Other processes with same user may see environment variables
- ✅ Persistent: Survives process restarts
- ✅ File permissions: Protected with 0600 permissions
⚠️ Disk storage: Token stored on local filesystem⚠️ Backup exposure: May be included in system backups
# ✅ Good: Use environment variables in containers
docker run --rm \
-e VAULT_ADDR="https://vault.company.com:8200" \
-e VAULT_TOKEN="$(vault write -field=token auth/token/create)" \
sshsk backup
# ✅ Good: Use CI/CD secret management
# GitLab CI/CD Variables (masked)
# GitHub Actions Secrets
# Jenkins Credentials
# ⚠️ Avoid: Hardcoded tokens in scripts
export VAULT_TOKEN="hvs.hardcoded-token-here" # Don't do this
# ✅ Good: Read from secure location
export VAULT_TOKEN="$(cat /run/secrets/vault-token)"
# ✅ Good: Use short-lived tokens
vault write auth/token/create ttl=1h policies=sshsk- SSH files read from ~/.ssh with exact permissions captured (mode, timestamps)
- Client-side encryption using AES-256-GCM with unique salt/IV per file
- Permission metadata stored alongside encrypted content
- Encrypted data transmitted to Vault over TLS
- Vault stores encrypted data (double encryption)
- User namespace isolation: users/{hostname-username}/
- Permission restoration with validation and security warnings
- User-provided passphrase → PBKDF2(100k iterations) → Encryption key
- Each file encrypted with unique cryptographic parameters
- Vault server never sees plaintext SSH keys
- Token-based Vault authentication with minimal permissions
- Environment variable authentication takes priority over files
- Vault compromise: SSH keys remain encrypted with user passphrase
- Network interception: TLS encryption protects data in transit
- Local compromise: Keys stored encrypted in Vault, no local token files (env var mode)
- Process inspection: Environment variables only visible to same user processes
- Container escape: No persistent secrets on container filesystem
- Brute force: Strong PBKDF2 parameters make attacks infeasible
- Permission tampering: Exact file permissions verified during restore
- Directory security: SSH directories automatically secured to proper permissions
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Make your changes with tests
- Run tests (
make test) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
The architecture supports easy extension:
// Add a new key detector
type CustomKeyDetector struct{}
func (d *CustomKeyDetector) Name() string { return "custom" }
func (d *CustomKeyDetector) Detect(filename string, content []byte) (*KeyInfo, bool) {
// Implementation
}
// Register with service
analysisService := analyzer.NewService()
analysisService.RegisterDetector(&CustomKeyDetector{})// Add a new storage backend
type S3StorageService struct{}
func (s *S3StorageService) StoreBackup(ctx context.Context, name string, data map[string]interface{}) error {
// Implementation
}
// Use with orchestrator
orchestrator := orchestrator.New(s3Storage, analysis, fileRead, fileRestore, encryption, validation)This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
- HashiCorp Vault team for the excellent secrets management platform
- Go community for the robust standard library
- Contributors and early adopters
If SSH files are restored with incorrect permissions:
# Check debug logs during restore
sshsk restore --dry-run --verbose
# Look for these messages:
# ✅ "✓ Parsed permissions from json.Number" (working correctly)
# ❌ "❌ CRITICAL: Missing or invalid permissions" (issue detected)# Test Vault connectivity
sshsk status
# Check environment variables
echo "VAULT_ADDR: $VAULT_ADDR"
echo "VAULT_TOKEN: ${VAULT_TOKEN:0:10}..." # First 10 chars only
# Verify Vault token has correct permissions
vault token lookup# Analyze SSH directory structure
sshsk analyze --verbose
# Check SSH directory permissions
ls -la ~/.ssh
# SSH directory should be 0700, keys should be 0600, public keys 0644# Verify environment variables are passed correctly
docker run --rm -e VAULT_ADDR -e VAULT_TOKEN sshsk:latest status
# Check volume mounts
docker run --rm -v ~/.ssh:/ssh:ro sshsk:latest analyzeEnable detailed logging for troubleshooting:
# Set debug log level
export SSH_SECRET_LOGGING_LEVEL=debug
# Run with verbose output
sshsk [command] --verboseIf you encounter issues:
- Check logs: Run with
--verboseflag - Verify environment: Use
sshsk statuscommand - Test connectivity: Ensure Vault is accessible
- Check permissions: Verify SSH directory structure
- Review documentation: Check command examples above
- 🔧 Fixed: Critical permission parsing bug for Vault data
- ✅ Resolved: All SSH files now restore with correct original permissions
- 🔍 Enhanced: Comprehensive debug logging for troubleshooting
- 🛡️ Improved: Better error handling for data type mismatches
- Perfect permission preservation with validation
- Intelligent SSH key detection and categorization
- Triple-layer encryption (client-side + Vault + TLS)
- Container and CI/CD ready
- Cross-platform support
- Key rotation automation
- Web UI for team management
- Plugin system for custom key types
- Multi-vault redundancy
- Compliance reporting (SOC2, PCI)
- Integration with cloud HSMs
Security Notice: Always test backup and restore operations in a safe environment before using with critical SSH keys. Maintain independent backups of your SSH keys as well.
Secure SSH key management solution