Last active
July 26, 2025 09:34
-
-
Save ephrin/b055228e9fb016d1daa60c2904cdb832 to your computer and use it in GitHub Desktop.
GitHub Deployment User Setup Script - Idempotent user and SSH key management
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# GitHub Deployment User Setup Script - Idempotent user and SSH key management | |
# | |
# This script sets up a deployment user for GitHub workflow deployments: | |
# 1. Creates/ensures a deployment user exists | |
# 2. Generates SSH key pair for GitHub authentication | |
# 3. Configures SSH for GitHub access (git clone/pull operations) | |
# 4. Sets up authorized_keys for GitHub workflow SSH access | |
# 5. Gives user sudo privileges for deployment directory management | |
# | |
# Must be run with sudo | |
set -euo pipefail | |
# Default values | |
DEFAULT_USER="deployer" | |
DEFAULT_HOST="github.com" | |
DEFAULT_DEPLOY_DIR="/var/www" | |
DEFAULT_KEY_TYPE="ed25519" | |
ENV_SUFFIX="" | |
TARGET_HOST="" | |
USERNAME="" | |
DEPLOY_DIR="" | |
KEY_TYPE="" | |
# Function to display usage | |
usage() { | |
cat << EOF | |
Usage: $0 [OPTIONS] | |
This script sets up a deployment user with SSH keys for GitHub workflow access. | |
The user will be able to: | |
- Receive SSH connections from GitHub workflows (via authorized_keys) | |
- Connect to GitHub to pull source code updates | |
- Manage deployment directories (sudo access for chown operations) | |
OPTIONS: | |
-u, --user USER Username to create/configure (default: $DEFAULT_USER) | |
-e, --env ENVIRONMENT Environment suffix for SSH key (test, staging, prod, etc.) | |
-h, --host HOST Target host for SSH config (default: $DEFAULT_HOST) | |
-d, --deploy-dir DIR Deployment directory path (default: $DEFAULT_DEPLOY_DIR) | |
-t, --key-type TYPE SSH key type: ed25519, rsa, ecdsa (default: $DEFAULT_KEY_TYPE) | |
--help Show this help message | |
Examples: | |
sudo $0 -u deployer -e prod | |
sudo $0 -u deploy-staging -e staging -d /var/www/staging | |
sudo $0 -u deploy-test -e test -h gitlab.com -d /opt/apps -t rsa | |
Key Types: | |
ed25519 - Modern, fast, short keys (recommended, default) | |
rsa - Traditional RSA keys (2048-bit minimum) | |
ecdsa - Elliptic curve keys | |
Note: This script must be run with sudo privileges. | |
EOF | |
} | |
# Parse command line arguments | |
while [[ $# -gt 0 ]]; do | |
case $1 in | |
-u|--user) | |
USERNAME="$2" | |
shift 2 | |
;; | |
-e|--env) | |
ENV_SUFFIX="$2" | |
shift 2 | |
;; | |
-h|--host) | |
TARGET_HOST="$2" | |
shift 2 | |
;; | |
-d|--deploy-dir) | |
DEPLOY_DIR="$2" | |
shift 2 | |
;; | |
-t|--key-type) | |
KEY_TYPE="$2" | |
shift 2 | |
;; | |
--help) | |
usage | |
if [[ $EUID -ne 0 ]]; then | |
echo "" | |
echo "⚠️ WARNING: This script must be run with sudo for actual execution." | |
echo " Example: sudo $0 -u deployer -e prod" | |
fi | |
exit 0 | |
;; | |
*) | |
echo "Unknown option: $1" | |
usage | |
exit 1 | |
;; | |
esac | |
done | |
# Check if running with sudo (after parsing arguments so --help works) | |
if [[ $EUID -ne 0 ]]; then | |
echo "Error: This script must be run with sudo" | |
echo "Usage: sudo $0 [OPTIONS]" | |
echo "Use --help for more information" | |
exit 1 | |
fi | |
# Set defaults if not provided | |
USERNAME="${USERNAME:-$DEFAULT_USER}" | |
TARGET_HOST="${TARGET_HOST:-$DEFAULT_HOST}" | |
DEPLOY_DIR="${DEPLOY_DIR:-$DEFAULT_DEPLOY_DIR}" | |
KEY_TYPE="${KEY_TYPE:-$DEFAULT_KEY_TYPE}" | |
# Validate key type | |
case "$KEY_TYPE" in | |
ed25519|rsa|ecdsa) | |
;; | |
*) | |
echo "Error: Invalid key type '$KEY_TYPE'. Use: ed25519, rsa, or ecdsa" | |
exit 1 | |
;; | |
esac | |
# Build key name with environment suffix and key type | |
if [[ -n "$ENV_SUFFIX" ]]; then | |
KEY_NAME="id_${KEY_TYPE}_${ENV_SUFFIX}" | |
echo "⚠️ WARNING: Using environment-specific key suffix '${ENV_SUFFIX}'" | |
echo " It's recommended to use different keys/secrets for different environments" | |
echo " (dev, test, staging, prod) for better security isolation." | |
else | |
KEY_NAME="id_${KEY_TYPE}" | |
fi | |
# User home directory | |
USER_HOME="/home/$USERNAME" | |
SSH_DIR="$USER_HOME/.ssh" | |
PRIVATE_KEY="$SSH_DIR/$KEY_NAME" | |
PUBLIC_KEY="$SSH_DIR/${KEY_NAME}.pub" | |
SSH_CONFIG="$SSH_DIR/config" | |
AUTHORIZED_KEYS="$SSH_DIR/authorized_keys" | |
KNOWN_HOSTS="$SSH_DIR/known_hosts" | |
echo "=== GitHub Deployment User Setup ===" | |
echo "User: $USERNAME" | |
echo "Environment: ${ENV_SUFFIX:-default}" | |
echo "Key type: $KEY_TYPE" | |
echo "Key name: $KEY_NAME" | |
echo "Target host: $TARGET_HOST" | |
echo "Deployment directory: $DEPLOY_DIR" | |
echo "======================================" | |
# Function to create user if it doesn't exist | |
create_user() { | |
if id "$USERNAME" &>/dev/null; then | |
echo "✓ User '$USERNAME' already exists" | |
else | |
echo "→ Creating user '$USERNAME'" | |
useradd -m -s /bin/bash "$USERNAME" | |
echo "✓ User '$USERNAME' created" | |
fi | |
# Add user to sudo group for deployment operations | |
if groups "$USERNAME" | grep -q sudo; then | |
echo "✓ User '$USERNAME' already has sudo privileges" | |
else | |
echo "→ Adding user '$USERNAME' to sudo group" | |
usermod -aG sudo "$USERNAME" | |
echo "✓ User '$USERNAME' added to sudo group" | |
fi | |
# Create sudoers rule for chown operations without password | |
SUDOERS_FILE="/etc/sudoers.d/deploy-$USERNAME" | |
if [[ ! -f "$SUDOERS_FILE" ]]; then | |
echo "→ Creating sudoers rule for deployment operations" | |
cat > "$SUDOERS_FILE" << EOF | |
# Allow $USERNAME to chown deployment directories without password | |
$USERNAME ALL=(ALL) NOPASSWD: /bin/chown, /bin/chmod | |
EOF | |
chmod 440 "$SUDOERS_FILE" | |
echo "✓ Sudoers rule created for deployment operations" | |
else | |
echo "✓ Sudoers rule already exists" | |
fi | |
} | |
# Function to setup SSH directory and permissions | |
setup_ssh_directory() { | |
echo "→ Setting up SSH directory" | |
sudo -u "$USERNAME" mkdir -p "$SSH_DIR" | |
chmod 700 "$SSH_DIR" | |
chown "$USERNAME:$USERNAME" "$SSH_DIR" | |
echo "✓ SSH directory configured" | |
} | |
# Function to generate SSH key if it doesn't exist | |
generate_ssh_key() { | |
if [[ -f "$PRIVATE_KEY" ]]; then | |
echo "✓ SSH key '$KEY_NAME' already exists" | |
else | |
echo "→ Generating SSH key '$KEY_NAME' (type: $KEY_TYPE)" | |
# Set key generation parameters based on type | |
case "$KEY_TYPE" in | |
ed25519) | |
sudo -u "$USERNAME" ssh-keygen -t ed25519 -f "$PRIVATE_KEY" -N "" -C "${USERNAME}@$(hostname)-${ENV_SUFFIX:-default}" | |
;; | |
rsa) | |
sudo -u "$USERNAME" ssh-keygen -t rsa -b 4096 -f "$PRIVATE_KEY" -N "" -C "${USERNAME}@$(hostname)-${ENV_SUFFIX:-default}" | |
;; | |
ecdsa) | |
sudo -u "$USERNAME" ssh-keygen -t ecdsa -b 521 -f "$PRIVATE_KEY" -N "" -C "${USERNAME}@$(hostname)-${ENV_SUFFIX:-default}" | |
;; | |
esac | |
chmod 600 "$PRIVATE_KEY" | |
chmod 644 "$PUBLIC_KEY" | |
echo "✓ SSH key generated ($KEY_TYPE)" | |
fi | |
} | |
# Function to add public key to authorized_keys | |
setup_authorized_keys() { | |
echo "→ Setting up authorized_keys" | |
sudo -u "$USERNAME" touch "$AUTHORIZED_KEYS" | |
chmod 600 "$AUTHORIZED_KEYS" | |
# Add public key to authorized_keys if not already present | |
if ! grep -Fxq "$(cat "$PUBLIC_KEY")" "$AUTHORIZED_KEYS" 2>/dev/null; then | |
cat "$PUBLIC_KEY" >> "$AUTHORIZED_KEYS" | |
echo "✓ Public key added to authorized_keys" | |
else | |
echo "✓ Public key already in authorized_keys" | |
fi | |
} | |
# Function to add host to known_hosts | |
setup_known_hosts() { | |
echo "→ Setting up known_hosts for $TARGET_HOST" | |
sudo -u "$USERNAME" touch "$KNOWN_HOSTS" | |
chmod 644 "$KNOWN_HOSTS" | |
# Get host key and add to known_hosts if not present | |
if ! grep -q "^$TARGET_HOST" "$KNOWN_HOSTS" 2>/dev/null; then | |
echo "→ Fetching host key for $TARGET_HOST" | |
sudo -u "$USERNAME" ssh-keyscan -H "$TARGET_HOST" >> "$KNOWN_HOSTS" 2>/dev/null || { | |
echo "⚠️ Warning: Could not fetch host key for $TARGET_HOST" | |
} | |
echo "✓ Host key added to known_hosts" | |
else | |
echo "✓ Host key already in known_hosts" | |
fi | |
} | |
# Function to setup SSH config | |
setup_ssh_config() { | |
echo "→ Setting up SSH config for GitHub deployment" | |
sudo -u "$USERNAME" touch "$SSH_CONFIG" | |
chmod 600 "$SSH_CONFIG" | |
# SSH config entry for GitHub | |
if [[ "$TARGET_HOST" == "github.com" ]]; then | |
CONFIG_ENTRY="Host github.com | |
HostName github.com | |
User git | |
IdentityFile $PRIVATE_KEY | |
IdentitiesOnly yes | |
StrictHostKeyChecking no | |
UserKnownHostsFile /dev/null" | |
else | |
CONFIG_ENTRY="Host $TARGET_HOST | |
HostName $TARGET_HOST | |
User git | |
IdentityFile $PRIVATE_KEY | |
IdentitiesOnly yes" | |
fi | |
# Add config entry if not present | |
if ! grep -q "Host $TARGET_HOST" "$SSH_CONFIG" 2>/dev/null; then | |
echo -e "\n$CONFIG_ENTRY" >> "$SSH_CONFIG" | |
echo "✓ SSH config entry added for $TARGET_HOST" | |
else | |
echo "✓ SSH config entry already exists for $TARGET_HOST" | |
fi | |
} | |
# Function to add key to SSH agent | |
setup_ssh_agent() { | |
echo "→ Setting up SSH agent" | |
# Start SSH agent if not running | |
if ! pgrep -u "$USERNAME" ssh-agent > /dev/null; then | |
sudo -u "$USERNAME" bash -c 'eval $(ssh-agent -s) && echo $SSH_AGENT_PID > ~/.ssh/agent.pid && echo $SSH_AUTH_SOCK > ~/.ssh/agent.sock' | |
fi | |
# Add key to agent | |
sudo -u "$USERNAME" bash -c " | |
export SSH_AUTH_SOCK=\$(cat ~/.ssh/agent.sock 2>/dev/null || echo '') | |
if [[ -n \"\$SSH_AUTH_SOCK\" ]] && ssh-add -l &>/dev/null; then | |
if ! ssh-add -l | grep -q '$PRIVATE_KEY'; then | |
ssh-add '$PRIVATE_KEY' 2>/dev/null || echo '⚠️ Could not add key to SSH agent' | |
else | |
echo '✓ Key already in SSH agent' | |
fi | |
else | |
echo '⚠️ SSH agent not available or not responding' | |
fi | |
" | |
} | |
# Function to print SSH keys | |
print_keys() { | |
echo "" | |
echo "=== SSH KEYS ===" | |
echo "" | |
echo "📋 PUBLIC KEY ($PUBLIC_KEY):" | |
echo "----------------------------------------" | |
cat "$PUBLIC_KEY" | |
echo "----------------------------------------" | |
echo "" | |
echo "🔒 PRIVATE KEY ($PRIVATE_KEY):" | |
echo "----------------------------------------" | |
cat "$PRIVATE_KEY" | |
echo "----------------------------------------" | |
echo "" | |
} | |
# Function to provide deployment instructions | |
show_deployment_instructions() { | |
echo "" | |
echo "=== DEPLOYMENT INSTRUCTIONS ===" | |
echo "" | |
echo "📋 Public key for GitHub Deploy Key:" | |
echo " Add this to your GitHub repository Settings > Deploy keys:" | |
echo " $(cat "$PUBLIC_KEY")" | |
echo "" | |
echo "🔧 To prepare deployment directory:" | |
echo " sudo chown -R $USERNAME:$USERNAME $DEPLOY_DIR" | |
echo " sudo chmod -R 755 $DEPLOY_DIR" | |
echo "" | |
echo "🚀 Test GitHub connection:" | |
echo " sudo -u $USERNAME ssh -T git@$TARGET_HOST" | |
echo "" | |
echo "📥 Clone repository example:" | |
echo " sudo -u $USERNAME git clone git@$TARGET_HOST:user/repo.git $DEPLOY_DIR/repo" | |
echo "" | |
echo "=================================" | |
} | |
# Main execution | |
main() { | |
create_user | |
setup_ssh_directory | |
generate_ssh_key | |
setup_authorized_keys | |
setup_known_hosts | |
setup_ssh_config | |
setup_ssh_agent | |
print_keys | |
show_deployment_instructions | |
echo "=== SETUP COMPLETE ===" | |
echo "✓ Deployment User: $USERNAME" | |
echo "✓ SSH Key Type: $KEY_TYPE" | |
echo "✓ SSH Key: $KEY_NAME" | |
echo "✓ Target Host: $TARGET_HOST" | |
echo "✓ Environment: ${ENV_SUFFIX:-default}" | |
echo "✓ Deployment Directory: $DEPLOY_DIR" | |
echo "✓ User has sudo privileges for chown/chmod operations" | |
echo "" | |
echo "🔗 Add the public key above to your GitHub repository's Deploy Keys" | |
echo "🚀 User $USERNAME is ready for GitHub workflow deployments!" | |
echo "=======================" | |
} | |
# Run main function | |
main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment