Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork0
Robust Bash script for automating SSH authorized_keys synchronization from multiple sources with enterprise-grade reliability
License
locus313/ssh-key-sync
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
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 SSHauthorized_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 theraw method with publicly accessible SSH key files.
Download the script and configuration:
# Get the latest releasecurl -fsSL https://raw.githubusercontent.com/locus313/ssh-key-sync/main/sync-ssh-keys.sh -o sync-ssh-keys.shcurl -fsSL https://raw.githubusercontent.com/locus313/ssh-key-sync/main/users.conf -o users.confchmod +x sync-ssh-keys.shConfigure your users and key sources:
# Edit the configuration filenano users.conf# Example configurationdeclare -A USER_KEYS=( ["alice"]="ghuser:alice-github" ["bob"]="raw:https://example.com/bob.keys")
Run the synchronization:
# Test the configuration firstsudo ./sync-ssh-keys.sh# Check the logs for successful synchronization
Verify the setup:
# Check that keys were properly synchronizedsudo cat /home/alice/.ssh/authorized_keys
Configuration is managed through theusers.conf file, which defines users and their SSH key sources.
# Optional: GitHub token for private repository accessCONF_GITHUB_TOKEN="your_github_token_here"# User key mappingdeclare -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
Theghuser method fetches public keys fromhttps://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 definitionsdeclare -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 Showhelp 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 --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 developmentdeclare -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 onlydeclare -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 fileCONF_GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"# Option 2: Environment variableexport GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"sudo -E ./sync-ssh-keys.sh
Secure storage:
# Restrict configuration file permissionschmod 600 users.conf# Or use a secrets management systemGITHUB_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 crontabsudo crontab -e# Sync every 15 minutes with logging*/15**** /opt/ssh-key-sync/sync-ssh-keys.sh>> /var/log/ssh-key-sync.log2>&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" admin@company.com
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 ServiceDocumentation=https://github.com/locus313/ssh-key-syncAfter=network-online.targetWants=network-online.target[Service]Type=oneshotExecStart=/opt/ssh-key-sync/sync-ssh-keys.shUser=rootGroup=rootStandardOutput=journalStandardError=journal# Security settingsNoNewPrivileges=trueProtectSystem=strictProtectHome=trueReadWritePaths=/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 minutesDocumentation=https://github.com/locus313/ssh-key-syncRequires=ssh-key-sync.service[Timer]OnBootSec=5minOnUnitActiveSec=10minRandomizedDelaySec=2minPersistent=true[Install]WantedBy=timers.target
Deploy and monitor:
# Install and startsudo systemctl daemon-reloadsudo systemctlenable ssh-key-sync.timersudo systemctl start ssh-key-sync.timer# Monitor statussudo systemctl status ssh-key-sync.timersudo journalctl -u ssh-key-sync.service -f
Integrate with popular CI/CD platforms for automated deployment:
name:Deploy and Sync SSH Keyson:push:branches:[main]jobs:deploy:runs-on:ubuntu-lateststeps: -uses:actions/checkout@v4 -name:Deploy applicationrun:./deploy.sh -name:Sync SSH keys on target serversrun:| # 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-deploysync-ssh-keys:stage:post-deployscript: -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.shvariables:GITHUB_TOKEN:$CI_SSH_SYNC_TOKENonly: -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 executablechmod +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 permissionsls -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 limitcurl -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 connectivitycurl -I https://github.comcurl -I https://api.github.com# Check DNS resolutionnslookup github.com# Test with verbose curl outputcurl -v https://github.com/username.keys
User validation failures
# Check if user existsid username# Create user if neededsudo useradd -m username# Verify user home directorygetent passwd username
Configuration syntax errors
# Validate Bash syntaxbash -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 modebash -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 logs2025-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 process2025-09-17 12:00:02: Fetching key filefor alice from https://github.com/alice.keys (method: ghuser)2025-09-17 12:00:03: Updated authorized_keysfor user'alice' at /home/alice/.ssh/authorized_keys2025-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 forERROR: User'nonexistent' does not exist. Skipping.ERROR: GITHUB_TOKEN is requiredfor API accessERROR: Failed to fetch key filefor user'alice' from https://invalid-url after multiple attemptsWARNING: No changes detectedin authorized_keysfor user'bob'
The project includes comprehensive testing infrastructure to ensure reliability and prevent regressions:
The project uses acentralized 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 validationbash -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 environmentdocker run -it --rm ubuntu:22.04 bash# Install dependencies and testapt update&& apt install -y curl bashcurl -fsSL https://raw.githubusercontent.com/locus313/ssh-key-sync/main/test.sh| bash
Note
For detailed testing procedures and guidelines, seeTESTING.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 tokensexport GITHUB_TOKEN="$(vault kv get -field=token secret/github/ssh-sync)"# Restrict configuration file permissionschmod 600 users.confchown 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 mirrorsdeclare -A USER_KEYS=( ["user"]="raw:https://internal-mirror.company.com/keys/user.authorized_keys")# Monitor network traffic if requiredtcpdump -i any host github.com
The 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 compliancesudo ./sync-ssh-keys.sh2>&1| tee -a /var/log/ssh-key-sync-audit.log# Log rotation for long-term storagecat> /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
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.shreadonly DEFAULT_RETRIES=3readonly 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.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.
About
Robust Bash script for automating SSH authorized_keys synchronization from multiple sources with enterprise-grade reliability
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
Contributors2
Uh oh!
There was an error while loading.Please reload this page.