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 information
GITHUB_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 --help
Configure 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:
- main
pipeline {
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.conf
GitHub 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 usage
Network 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.keys
User validation failures
# Check if user exists
id username
# Create user if needed
sudo useradd -m username
# Verify user home directory
getent passwd username
Configuration 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.sh
Understanding 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-run
The 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 | bash
Note
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 (
repo
for 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.com
The script automatically implements security best practices:
- SSH Directory:
700
permissions (owner access only) - Authorized Keys:
600
permissions (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
}
EOF
Can 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
raw
method with GitLab's raw file URLs - Bitbucket: Use the
raw
method with Bitbucket's raw file URLs - Azure DevOps: Use the
raw
method with Azure DevOps file URLs - Custom Git servers: Use the
raw
method 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 delay
Is 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.sh
for 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.