Synchronize SSH authorized_keys for multiple users from various sources
⭐ If you like this project, star it on GitHub — it helps a lot!
Features • Getting Started • Configuration • Usage • Examples • Automation • Testing
A robust and secure Bash script for automating SSH authorized_keys synchronization across multiple users from various sources. Perfect for managing SSH access in development environments, CI/CD pipelines, and production systems with enterprise-grade reliability.
- Multi-Source Support - Fetch SSH keys from public URLs, private GitHub repositories, or GitHub user profiles
 - Enterprise-Grade Reliability - Built-in retry mechanism with configurable delays for network resilience
 - Atomic Operations - Safe file updates with comparison checks to prevent unnecessary changes
 - Comprehensive Audit Trail - Detailed timestamped logs for monitoring, debugging, and compliance
 - Self-Maintenance - Automatic updates to the latest version from GitHub repository
 - Configuration-as-Code - External configuration file for version control and team collaboration
 - Defensive Programming - Robust error handling with graceful fallbacks and validation
 - Multi-User Architecture - Concurrent SSH key management for multiple system users
 - Security-First Design - Proper file permissions, user validation, and secure temporary file handling
 
- Bash 4.0+ - Required for associative arrays support
 - curl - For HTTP operations and API communication
 - getent - User information retrieval (standard on most Linux distributions)
 - GitHub Token - Only required for accessing private repositories
 
Tip
You can test the script locally without any external dependencies by using the raw method with publicly accessible SSH key files.
- 
Download the script and configuration:
# Get the latest release curl -fsSL https://raw.githubusercontent.com/locus313/ssh-key-sync/main/sync-ssh-keys.sh -o sync-ssh-keys.sh curl -fsSL https://raw.githubusercontent.com/locus313/ssh-key-sync/main/users.conf -o users.conf chmod +x sync-ssh-keys.sh - 
Configure your users and key sources:
# Edit the configuration file nano users.conf # Example configuration declare -A USER_KEYS=( ["alice"]="ghuser:alice-github" ["bob"]="raw:https://example.com/bob.keys" )
 - 
Run the synchronization:
# Test the configuration first sudo ./sync-ssh-keys.sh # Check the logs for successful synchronization
 - 
Verify the setup:
# Check that keys were properly synchronized sudo cat /home/alice/.ssh/authorized_keys 
Configuration is managed through the users.conf file, which defines users and their SSH key sources.
# Optional: GitHub token for private repository access
CONF_GITHUB_TOKEN="your_github_token_here"
# User key mapping
declare -A USER_KEYS=(
  ["username"]="method:target"
)| Method | Description | Use Case | Authentication | 
|---|---|---|---|
raw | 
Direct HTTP(S) URL | Public key repositories, CDNs | None | 
api | 
GitHub API endpoint | Private repositories, enterprise | GitHub Token | 
ghuser | 
GitHub user profile | Individual developer keys | None | 
Note
The ghuser method fetches public keys from https://github.com/username.keys, which is a built-in GitHub feature for accessing any user's public SSH keys.
#!/bin/bash
# GitHub token for API access (optional)
CONF_GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# User key definitions
declare -A USER_KEYS=(
  # Fetch from public URL
  ["ubuntu"]="raw:https://example.com/ssh-keys/ubuntu.authorized_keys"
  
  # Fetch from private GitHub repository
  ["devuser"]="api:https://api.github.com/repos/yourorg/ssh-keys/contents/keys/devuser.authorized_keys?ref=main"
  
  # Fetch public keys from GitHub user
  ["alice"]="ghuser:alice-github-username"
  ["bob"]="ghuser:bob-github-username"
)./sync-ssh-keys.sh [OPTIONS]
OPTIONS:
  --self-update    Update the script to the latest version from GitHub
  --help, -h       Show help message
  --version, -v    Show version informationGITHUB_TOKEN: GitHub personal access token (overridesCONF_GITHUB_TOKEN)
# Run synchronization
./sync-ssh-keys.sh
# Update script to latest version
./sync-ssh-keys.sh --self-update
# Show help
./sync-ssh-keys.sh --helpConfigure SSH access for a development team with mixed requirements:
declare -A USER_KEYS=(
  # DevOps team with enterprise private keys
  ["devops-lead"]="api:https://api.github.com/repos/company/ssh-keys/contents/team/devops-lead.keys?ref=main"
  ["sre-admin"]="api:https://api.github.com/repos/company/ssh-keys/contents/team/sre-admin.keys?ref=main"
  
  # Developers using personal GitHub keys
  ["alice"]="ghuser:alice-dev"
  ["bob"]="ghuser:bob-coder"
  ["charlie"]="ghuser:charlie-ops"
  
  # Service accounts and automation
  ["ci-deploy"]="raw:https://cdn.company.com/ci-keys/deploy-bot.keys"
  ["backup-service"]="raw:https://secure.company.com/service-keys/backup.authorized_keys"
)Different configurations for different environments:
# staging.conf - More permissive for development
declare -A USER_KEYS=(
  ["dev-alice"]="ghuser:alice-personal"
  ["dev-bob"]="ghuser:bob-personal"
  ["staging-deploy"]="raw:https://staging-keys.company.com/deploy.keys"
)
# production.conf - Strict enterprise keys only
declare -A USER_KEYS=(
  ["prod-alice"]="api:https://api.github.com/repos/company/prod-keys/contents/alice.keys?ref=main"
  ["prod-deploy"]="api:https://api.github.com/repos/company/prod-keys/contents/deploy.keys?ref=main"
)Sync keys across multiple regions with different sources:
declare -A USER_KEYS=(
  # Global admin access
  ["global-admin"]="api:https://api.github.com/repos/company/global-keys/contents/admin.keys?ref=main"
  
  # Region-specific access
  ["us-east-ops"]="raw:https://us-east.company.com/ops-keys/authorized_keys"
  ["eu-west-ops"]="raw:https://eu-west.company.com/ops-keys/authorized_keys"
  ["asia-ops"]="raw:https://asia.company.com/ops-keys/authorized_keys"
)For accessing private repositories, you'll need a GitHub Personal Access Token:
- 
Generate a token:
- Go to GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
 - Click "Generate new token (classic)"
 - Select scopes: 
repo(for private repository access) - Set an appropriate expiration date
 
 - 
Configure the token:
# Option 1: In configuration file CONF_GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # Option 2: Environment variable export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" sudo -E ./sync-ssh-keys.sh
 - 
Secure storage:
# Restrict configuration file permissions chmod 600 users.conf # Or use a secrets management system GITHUB_TOKEN=$(vault kv get -field=token secret/github/ssh-sync)
 
Important
Security Best Practice: Use tokens with minimal required permissions and rotate them regularly. For organizations, consider using GitHub Apps instead of personal access tokens.
Set up automated synchronization for production environments:
# Edit root crontab
sudo crontab -e
# Sync every 15 minutes with logging
*/15 * * * * /opt/ssh-key-sync/sync-ssh-keys.sh >> /var/log/ssh-key-sync.log 2>&1
# Daily summary report (optional)
0 9 * * * grep "$(date +%Y-%m-%d)" /var/log/ssh-key-sync.log | mail -s "SSH Key Sync Daily Report" [email protected]Create a robust systemd service with automatic restart and monitoring:
- 
Create the service
/etc/systemd/system/ssh-key-sync.service:[Unit] Description=SSH Key Synchronization Service Documentation=https://github.com/locus313/ssh-key-sync After=network-online.target Wants=network-online.target [Service] Type=oneshot ExecStart=/opt/ssh-key-sync/sync-ssh-keys.sh User=root Group=root StandardOutput=journal StandardError=journal # Security settings NoNewPrivileges=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/home /root [Install] WantedBy=multi-user.target
 - 
Create the timer
/etc/systemd/system/ssh-key-sync.timer:[Unit] Description=Run SSH Key Sync every 10 minutes Documentation=https://github.com/locus313/ssh-key-sync Requires=ssh-key-sync.service [Timer] OnBootSec=5min OnUnitActiveSec=10min RandomizedDelaySec=2min Persistent=true [Install] WantedBy=timers.target
 - 
Deploy and monitor:
# Install and start sudo systemctl daemon-reload sudo systemctl enable ssh-key-sync.timer sudo systemctl start ssh-key-sync.timer # Monitor status sudo systemctl status ssh-key-sync.timer sudo journalctl -u ssh-key-sync.service -f
 
Integrate with popular CI/CD platforms for automated deployment:
name: Deploy and Sync SSH Keys
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy application
        run: ./deploy.sh
        
      - name: Sync SSH keys on target servers
        run: |
          # Download and run ssh-key-sync
          curl -fsSL https://raw.githubusercontent.com/locus313/ssh-key-sync/main/sync-ssh-keys.sh | \
          ssh -o StrictHostKeyChecking=no deploy@${{ secrets.SERVER_HOST }} 'cat > sync-ssh-keys.sh && chmod +x sync-ssh-keys.sh && sudo ./sync-ssh-keys.sh'
        env:
          GITHUB_TOKEN: ${{ secrets.SSH_SYNC_TOKEN }}stages:
  - deploy
  - post-deploy
sync-ssh-keys:
  stage: post-deploy
  script:
    - apt-get update && apt-get install -y curl
    - curl -fsSL https://raw.githubusercontent.com/locus313/ssh-key-sync/main/sync-ssh-keys.sh -o sync-ssh-keys.sh
    - chmod +x sync-ssh-keys.sh
    - ./sync-ssh-keys.sh
  variables:
    GITHUB_TOKEN: $CI_SSH_SYNC_TOKEN
  only:
    - mainpipeline {
    agent any
    
    environment {
        GITHUB_TOKEN = credentials('ssh-sync-github-token')
    }
    
    stages {
        stage('Deploy') {
            steps {
                sh './deploy.sh'
            }
        }
        
        stage('Sync SSH Keys') {
            steps {
                sh '''
                    curl -fsSL https://raw.githubusercontent.com/locus313/ssh-key-sync/main/sync-ssh-keys.sh -o sync-ssh-keys.sh
                    chmod +x sync-ssh-keys.sh
                    sudo ./sync-ssh-keys.sh
                '''
            }
        }
    }
    
    post {
        always {
            sh 'rm -f sync-ssh-keys.sh'
        }
    }
}Permission denied errors
# Ensure script is executable
chmod +x sync-ssh-keys.sh
# Run with appropriate privileges (required for managing other users' SSH keys)
sudo ./sync-ssh-keys.sh
# Check file ownership and permissions
ls -la sync-ssh-keys.sh users.confGitHub API rate limits
# Use authenticated requests (increases rate limit from 60 to 5000 per hour)
export GITHUB_TOKEN="your_token_here"
# Monitor your rate limit
curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/rate_limit
# Consider reducing sync frequency for high-volume usageNetwork connectivity issues
The script includes automatic retry logic (3 attempts by default), but you can troubleshoot:
# Test direct connectivity
curl -I https://github.com
curl -I https://api.github.com
# Check DNS resolution
nslookup github.com
# Test with verbose curl output
curl -v https://github.com/username.keysUser validation failures
# Check if user exists
id username
# Create user if needed
sudo useradd -m username
# Verify user home directory
getent passwd usernameConfiguration syntax errors
# Validate Bash syntax
bash -n users.conf
# Check for common issues
# - Missing quotes around array values
# - Incorrect associative array syntax
# - Typos in method names (raw, api, ghuser)Enable detailed debugging information:
# Run with bash debug mode
bash -x sync-ssh-keys.sh
# Or modify the script temporarily
# Add 'set -x' at the top of sync-ssh-keys.shUnderstanding log output:
# Successful execution logs
2025-09-17 12:00:00: Starting SSH key synchronization (version 0.1.5)
2025-09-17 12:00:01: Loading configuration...
2025-09-17 12:00:01: Found 3 user(s) to process
2025-09-17 12:00:02: Fetching key file for alice from https://github.com/alice.keys (method: ghuser)
2025-09-17 12:00:03: Updated authorized_keys for user 'alice' at /home/alice/.ssh/authorized_keys
2025-09-17 12:00:03: Successfully processed user 'alice'
2025-09-17 12:00:04: Synchronization complete. Processed: 3, Failed: 0
# Error patterns to watch for
ERROR: User 'nonexistent' does not exist. Skipping.
ERROR: GITHUB_TOKEN is required for API access
ERROR: Failed to fetch key file for user 'alice' from https://invalid-url after multiple attempts
WARNING: No changes detected in authorized_keys for user 'bob'The project includes comprehensive testing infrastructure to ensure reliability and prevent regressions:
The project uses a centralized CI workflow that orchestrates all testing and validation:
- Lint Check - ShellCheck static analysis for code quality and best practices
 - Version Check - Ensures version bumps in pull requests for proper release management
 - Integration Tests - Real user creation, SSH key synchronization, and error handling validation
 - Multi-Environment Testing - Validation across different Linux distributions
 - Security Focus - Proper permissions, file handling, and authentication validation
 
Note
Workflow Architecture: The CI workflow calls individual test workflows (lint.yml, test.yml, check-version.yml) as reusable workflows, preventing duplicate runs while maintaining organized test separation.
# Quick validation suite
./test.sh
# Manual syntax validation
bash -n sync-ssh-keys.sh
# With ShellCheck (recommended)
shellcheck sync-ssh-keys.sh
# Test with dry-run mode (if implemented)
./sync-ssh-keys.sh --dry-runThe test suite validates:
- ✅ Configuration file parsing and validation
 - ✅ User existence and permission checks
 - ✅ Network connectivity and retry logic
 - ✅ File operations and atomic updates
 - ✅ Error handling and edge cases
 - ✅ GitHub API integration
 - ✅ Security permissions and ownership
 
For contributors and advanced users:
# Create isolated test environment
docker run -it --rm ubuntu:22.04 bash
# Install dependencies and test
apt update && apt install -y curl bash
curl -fsSL https://raw.githubusercontent.com/locus313/ssh-key-sync/main/test.sh | bashNote
For detailed testing procedures and guidelines, see TESTING.md.
Important
Production Security Checklist
- Store GitHub tokens securely and rotate them regularly (every 90 days recommended)
 -  Use least-privilege tokens with only required scopes (
repofor private repos) - Monitor logs for failed authentication attempts and unusual activity
 - Validate SSH key sources and ownership before adding to configuration
 - Use private repositories for sensitive key storage, never public ones
 - Implement proper backup and recovery procedures for SSH key configuration
 - Regular audit of user access and key validity
 
# Use environment variables instead of hardcoding tokens
export GITHUB_TOKEN="$(vault kv get -field=token secret/github/ssh-sync)"
# Restrict configuration file permissions
chmod 600 users.conf
chown root:root users.conf
# Consider using GitHub Apps for organization-wide deployments
# They provide better security and audit trails than personal access tokens# Validate SSL certificates (default behavior)
# The script uses curl with strict SSL validation
# For air-gapped environments, consider using local mirrors
declare -A USER_KEYS=(
  ["user"]="raw:https://internal-mirror.company.com/keys/user.authorized_keys"
)
# Monitor network traffic if required
tcpdump -i any host github.comThe script automatically implements security best practices:
- SSH Directory: 
700permissions (owner access only) - Authorized Keys: 
600permissions (owner read/write only) - Proper Ownership: All files owned by the target user
 - Atomic Operations: Temporary files with secure cleanup
 - Input Validation: Validates all user inputs and file paths
 
# Enable comprehensive logging for compliance
sudo ./sync-ssh-keys.sh 2>&1 | tee -a /var/log/ssh-key-sync-audit.log
# Log rotation for long-term storage
cat > /etc/logrotate.d/ssh-key-sync << EOF
/var/log/ssh-key-sync*.log {
    daily
    rotate 365
    compress
    delaycompress
    missingok
    notifempty
    create 640 root adm
}
EOFCan I use this script with GitLab, Bitbucket, or other Git providers?
Currently, the script has built-in support for GitHub's API and public key endpoints. For other providers:
- GitLab: Use the 
rawmethod with GitLab's raw file URLs - Bitbucket: Use the 
rawmethod with Bitbucket's raw file URLs - Azure DevOps: Use the 
rawmethod with Azure DevOps file URLs - Custom Git servers: Use the 
rawmethod with direct HTTPS URLs 
Example:
["user"]="raw:https://gitlab.com/username/ssh-keys/-/raw/main/user.keys"What happens if a user doesn't exist on the system?
The script validates user existence before processing and will:
- Log a warning message
 - Skip that user entirely
 - Continue processing other users
 - Report the failure in the final summary
 
This ensures the script doesn't fail completely due to one missing user.
How often should I run the synchronization?
Recommended frequencies based on environment:
- Development: Every 15-30 minutes for rapid iteration
 - Staging: Every 1-2 hours for testing stability
 - Production: Every 4-6 hours for security balance
 - High-security environments: Every 1 hour with audit logging
 
Consider your team's SSH key rotation frequency and security requirements.
Can I customize the retry logic and timeouts?
Yes, the script uses configurable constants that you can modify:
# Edit these variables in sync-ssh-keys.sh
readonly DEFAULT_RETRIES=3
readonly DEFAULT_RETRY_DELAY=2
# Or pass them as parameters (if you modify the script)
fetch_key_file "$method" "$target" "$temp_file" 5 3  # 5 retries, 3 second delayIs there a dry-run mode to test configuration?
While not currently implemented, you can safely test by:
- Configuration validation: 
bash -n users.conf - Syntax check: 
bash -n sync-ssh-keys.sh - Test environment: Run on a test system with test users
 - Verbose logging: Use 
bash -x sync-ssh-keys.shfor detailed output 
A dry-run mode is planned for future releases.
How do I handle SSH key rotation?
The script automatically handles key rotation:
- Update the source (GitHub keys, file URLs, etc.)
 - Run the sync - the script detects changes automatically
 - Verify the update - check the logs for confirmation
 
The script only updates files when content actually changes, making it safe to run frequently.
Can I exclude certain keys or add filtering?
Currently, the script syncs all keys from the configured source. For filtering:
- Source-level filtering: Maintain filtered key files at the source
 - Multiple sources: Create separate endpoints for different key sets
 - Custom scripts: Pipe through additional filtering if needed
 
Advanced filtering features may be added in future versions.