Implement dual-key git access for dev users

- Generate server-side SSH keypair for git access from server
- Upload both laptop key and server key to Forgejo
- Add mandatory key revocation in dev-remove.sh
- Fix: use forgejo@ instead of git@ for SSH URLs
- Keys named username-laptop and username-devserver
- Key comment includes DO-NOT-REUSE warning

Closes ops-jrz1-rfx
This commit is contained in:
Dan 2026-01-09 19:35:59 -08:00
parent 99b187fa5a
commit d9c1848e88
2 changed files with 140 additions and 25 deletions

View file

@ -76,7 +76,7 @@ create_user() {
log_info "Added to devs group" log_info "Added to devs group"
fi fi
# Set up SSH key # Set up SSH directory and login key (authorized_keys)
local ssh_dir="/home/$username/.ssh" local ssh_dir="/home/$username/.ssh"
mkdir -p "$ssh_dir" mkdir -p "$ssh_dir"
echo "$ssh_key" > "$ssh_dir/authorized_keys" echo "$ssh_key" > "$ssh_dir/authorized_keys"
@ -84,9 +84,28 @@ create_user() {
chmod 600 "$ssh_dir/authorized_keys" chmod 600 "$ssh_dir/authorized_keys"
chown -R "$username:users" "$ssh_dir" chown -R "$username:users" "$ssh_dir"
if [[ "$user_exists" == true ]]; then if [[ "$user_exists" == true ]]; then
log_info "Updated SSH key" log_info "Updated SSH login key"
fi fi
# Generate server-side keypair for git access (if not exists)
local server_key="$ssh_dir/id_ed25519"
if [[ ! -f "$server_key" ]]; then
log_info "Generating server-side SSH key for git access..."
sudo -u "$username" ssh-keygen -t ed25519 \
-f "$server_key" \
-N '' \
-C "$username@jrz1-server-DO-NOT-REUSE" \
>/dev/null 2>&1
log_info "Server-side SSH key generated"
else
log_info "Server-side SSH key already exists"
fi
# Ensure strict permissions
chmod 700 "$ssh_dir"
chmod 600 "$server_key"
chmod 644 "$server_key.pub"
chown -R "$username:users" "$ssh_dir"
# Set up user's shell config (may not exist on NixOS) # Set up user's shell config (may not exist on NixOS)
# .profile = login shells (SSH), .bashrc = interactive non-login # .profile = login shells (SSH), .bashrc = interactive non-login
local profile="/home/$username/.profile" local profile="/home/$username/.profile"
@ -183,9 +202,37 @@ EOF
fi fi
} }
# Helper function to upload a single SSH key to Forgejo
upload_forgejo_key() {
local username="$1"
local pubkey="$2"
local title="$3"
local token="$4"
local forgejo_url="http://localhost:3000"
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$forgejo_url/api/v1/admin/users/$username/keys" \
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"$title\",
\"key\": \"$pubkey\",
\"read_only\": false
}" 2>/dev/null) || true
if [[ "$http_code" == "201" ]]; then
log_info "Forgejo key '$title' added"
elif [[ "$http_code" == "422" ]]; then
log_info "Forgejo key '$title' already exists"
else
log_warn "Forgejo key '$title' upload returned HTTP $http_code"
fi
}
provision_forgejo() { provision_forgejo() {
local username="$1" local username="$1"
local ssh_key="$2" local login_key="$2"
local token_file="/run/secrets/forgejo-api-token" local token_file="/run/secrets/forgejo-api-token"
local forgejo_url="http://localhost:3000" local forgejo_url="http://localhost:3000"
@ -244,23 +291,18 @@ EOF
log_info "Credentials written to ~/.forgejo-credentials" log_info "Credentials written to ~/.forgejo-credentials"
fi fi
# Upload SSH key to Forgejo (ignore if already exists) # Upload BOTH keys to Forgejo:
http_code=$(curl -s -o /dev/null -w "%{http_code}" \ # 1. Login key (user's laptop key) - for direct laptop→git access
-X POST "$forgejo_url/api/v1/admin/users/$username/keys" \ upload_forgejo_key "$username" "$login_key" "$username-laptop" "$token"
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"dev-server-key\",
\"key\": \"$ssh_key\",
\"read_only\": false
}" 2>/dev/null) || true
if [[ "$http_code" == "201" ]]; then # 2. Server-side key - for git access from within the server
log_info "SSH key added to Forgejo" local server_pubkey_file="/home/$username/.ssh/id_ed25519.pub"
elif [[ "$http_code" == "422" ]]; then if [[ -r "$server_pubkey_file" ]]; then
log_info "SSH key already in Forgejo" local server_pubkey
server_pubkey=$(cat "$server_pubkey_file")
upload_forgejo_key "$username" "$server_pubkey" "$username-devserver" "$token"
else else
log_warn "Forgejo key upload returned HTTP $http_code (may already exist)" log_warn "Server-side key not found at $server_pubkey_file"
fi fi
} }
@ -297,7 +339,7 @@ print_onboarding() {
echo "" echo ""
echo "## Git Access" echo "## Git Access"
echo "" echo ""
echo " Clone repos: git clone git@git.clarun.xyz:org/repo.git" echo " Clone repos: git clone forgejo@git.clarun.xyz:org/repo.git"
echo "" echo ""
echo " Forgejo account: $username" echo " Forgejo account: $username"
echo " Credentials file: ~/.forgejo-credentials" echo " Credentials file: ~/.forgejo-credentials"

View file

@ -7,8 +7,9 @@
# - Home directory (or archives if --archive flag) # - Home directory (or archives if --archive flag)
# - Maubot plugin symlinks # - Maubot plugin symlinks
# #
# NOTE: Does NOT delete Forgejo account automatically (too dangerous). # NOTE: Revokes Forgejo SSH keys automatically (prevents zombie access).
# Manual suspension required via Forgejo admin UI. # Does NOT delete Forgejo account (too dangerous - may delete repos).
# Manual account suspension still recommended via Forgejo admin UI.
set -euo pipefail set -euo pipefail
@ -28,6 +29,75 @@ log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
log_dry() { echo -e "${CYAN}[DRY-RUN]${NC} $1"; } log_dry() { echo -e "${CYAN}[DRY-RUN]${NC} $1"; }
revoke_forgejo_keys() {
local username="$1"
local token_file="/run/secrets/forgejo-api-token"
local forgejo_url="http://localhost:3000"
if [[ "$DRY_RUN" == true ]]; then
log_dry "Would revoke Forgejo SSH keys for '$username'"
return 0
fi
# Check if token file exists
if [[ ! -r "$token_file" ]]; then
log_warn "Forgejo API token not found - cannot revoke keys automatically"
log_warn "MANUAL ACTION REQUIRED: Delete SSH keys for '$username' in Forgejo"
return 1
fi
local token
token=$(cat "$token_file")
log_info "Revoking Forgejo SSH keys for '$username'..."
# Get list of user's keys
local keys_json
keys_json=$(curl -s \
-H "Authorization: token $token" \
"$forgejo_url/api/v1/admin/users/$username/keys" 2>/dev/null) || true
if [[ -z "$keys_json" || "$keys_json" == "null" ]]; then
log_info "No Forgejo keys found for '$username'"
return 0
fi
# Parse key IDs and delete each one
# Using grep/sed since jq may not be available
local key_ids
key_ids=$(echo "$keys_json" | grep -o '"id":[0-9]*' | sed 's/"id"://' || true)
if [[ -z "$key_ids" ]]; then
log_info "No Forgejo keys found for '$username'"
return 0
fi
local revoked=0
local failed=0
for key_id in $key_ids; do
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
-X DELETE "$forgejo_url/api/v1/admin/users/$username/keys/$key_id" \
-H "Authorization: token $token" 2>/dev/null) || true
if [[ "$http_code" == "204" ]]; then
((revoked++))
else
log_warn "Failed to revoke key $key_id (HTTP $http_code)"
((failed++))
fi
done
if [[ $revoked -gt 0 ]]; then
log_info "Revoked $revoked Forgejo SSH key(s)"
fi
if [[ $failed -gt 0 ]]; then
log_warn "$failed key(s) failed to revoke - manual cleanup may be needed"
return 1
fi
return 0
}
usage() { usage() {
echo "Usage: $0 <username> [--archive] [--dry-run]" echo "Usage: $0 <username> [--archive] [--dry-run]"
echo "" echo ""
@ -223,6 +293,9 @@ main() {
fi fi
fi fi
# Revoke Forgejo SSH keys BEFORE removing user (prevents zombie access)
revoke_forgejo_keys "$username"
remove_maubot_symlinks "$username" remove_maubot_symlinks "$username"
if [[ "$archive" == true ]]; then if [[ "$archive" == true ]]; then
@ -233,14 +306,14 @@ main() {
if [[ "$DRY_RUN" == true ]]; then if [[ "$DRY_RUN" == true ]]; then
log_dry "Dry run complete - no changes made" log_dry "Dry run complete - no changes made"
log_dry "Would print Forgejo suspension reminder"
else else
log_info "Dev '$username' removed successfully" log_info "Dev '$username' removed successfully"
# Remind admin to handle Forgejo account # Remind admin to handle Forgejo account (keys already revoked)
echo "" echo ""
log_warn "ACTION REQUIRED: Suspend Forgejo account for '$username'" log_warn "RECOMMENDED: Suspend Forgejo account for '$username'"
echo " Visit: https://git.clarun.xyz/admin/users" echo " SSH keys have been revoked automatically."
echo " To fully disable the account, visit: https://git.clarun.xyz/admin/users"
echo " Find user '$username' and either:" echo " Find user '$username' and either:"
echo " - Prohibit login (keeps repos/PRs intact)" echo " - Prohibit login (keeps repos/PRs intact)"
echo " - Delete user (WARNING: may delete their repos)" echo " - Delete user (WARNING: may delete their repos)"