- Remove stderr suppression from ssh-keygen (show errors) - Add curl timeouts (--connect-timeout 5 --max-time 30) - Add || true to arithmetic increments for set -e safety Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
391 lines
12 KiB
Bash
Executable file
391 lines
12 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# dev-add.sh - Add or update a dev user account
|
|
# Usage: dev-add.sh <username> <ssh-pubkey>
|
|
#
|
|
# Idempotent: safe to re-run. Updates SSH key and config if user exists.
|
|
#
|
|
# Creates/updates:
|
|
# - Unix user account with SSH key
|
|
# - Adds to devs group (Slack token access)
|
|
# - Git config (user.name, user.email)
|
|
# - Forgejo account for git.clarun.xyz access
|
|
# - Outputs onboarding instructions
|
|
|
|
set -euo pipefail
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m' # No Color
|
|
|
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
|
|
|
usage() {
|
|
echo "Usage: $0 <username> <ssh-pubkey>"
|
|
echo ""
|
|
echo "Arguments:"
|
|
echo " username - Dev's username (alphanumeric, 3-16 chars)"
|
|
echo " ssh-pubkey - SSH public key (starts with ssh-ed25519, ssh-rsa, etc.)"
|
|
echo ""
|
|
echo "Example:"
|
|
echo " $0 alice 'ssh-ed25519 AAAA... alice@laptop'"
|
|
exit 1
|
|
}
|
|
|
|
validate_username() {
|
|
local username="$1"
|
|
if [[ ! "$username" =~ ^[a-z][a-z0-9_-]{2,15}$ ]]; then
|
|
log_error "Invalid username: must be 3-16 chars, start with letter, alphanumeric/underscore/dash only"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
validate_ssh_key() {
|
|
local key="$1"
|
|
if [[ ! "$key" =~ ^ssh-(ed25519|rsa|ecdsa) ]]; then
|
|
log_error "Invalid SSH key: must start with ssh-ed25519, ssh-rsa, or ssh-ecdsa"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
create_user() {
|
|
local username="$1"
|
|
local ssh_key="$2"
|
|
local user_exists=false
|
|
|
|
# Check if user already exists
|
|
if id "$username" &>/dev/null; then
|
|
user_exists=true
|
|
log_info "Updating existing user '$username'..."
|
|
else
|
|
log_info "Creating user '$username'..."
|
|
# Create user with home directory
|
|
# NixOS: don't specify shell (uses default), group is 'users'
|
|
useradd -m -g users "$username"
|
|
fi
|
|
|
|
# Make home directory private (not world-readable)
|
|
chmod 700 "/home/$username"
|
|
|
|
# Add to devs group for Slack token access
|
|
if ! groups "$username" | grep -q '\bdevs\b'; then
|
|
usermod -aG devs "$username"
|
|
log_info "Added to devs group"
|
|
fi
|
|
|
|
# Set up SSH directory and login key (authorized_keys)
|
|
local ssh_dir="/home/$username/.ssh"
|
|
mkdir -p "$ssh_dir"
|
|
echo "$ssh_key" > "$ssh_dir/authorized_keys"
|
|
chmod 700 "$ssh_dir"
|
|
chmod 600 "$ssh_dir/authorized_keys"
|
|
chown -R "$username:users" "$ssh_dir"
|
|
if [[ "$user_exists" == true ]]; then
|
|
log_info "Updated SSH login key"
|
|
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
|
|
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)
|
|
# .profile = login shells (SSH), .bashrc = interactive non-login
|
|
local profile="/home/$username/.profile"
|
|
{
|
|
echo '# bun global packages (preferred - faster installs)'
|
|
echo "export PATH=\"\$HOME/.bun/bin:\$PATH\""
|
|
echo ''
|
|
echo '# npm global packages (fallback)'
|
|
echo "export PATH=\"\$HOME/.npm-global/bin:\$PATH\""
|
|
echo ''
|
|
echo '# Slack bot development tokens (if readable)'
|
|
echo '[ -r /etc/slack-dev.env ] && source /etc/slack-dev.env'
|
|
} > "$profile"
|
|
chown "$username:users" "$profile"
|
|
|
|
# Pre-create npm global directory and configure npm prefix
|
|
local npm_global="/home/$username/.npm-global"
|
|
local npmrc="/home/$username/.npmrc"
|
|
mkdir -p "$npm_global"
|
|
echo "prefix=$npm_global" > "$npmrc"
|
|
chown "$username:users" "$npm_global" "$npmrc"
|
|
|
|
# Create AGENTS.md for AI coding assistants
|
|
cat > "/home/$username/AGENTS.md" << 'AGENTS_EOF'
|
|
# AGENTS.md - Dev Server Guide
|
|
|
|
Shared NixOS dev server. This guide helps AI coding agents work effectively here.
|
|
|
|
## Possible Right Now
|
|
|
|
**Install packages:**
|
|
```bash
|
|
bun install -g @google/gemini-cli # JS tools (fast)
|
|
nix profile install nixpkgs#go # System tools (go, rust, etc.)
|
|
uv venv && uv pip install <pkg> # Python packages
|
|
```
|
|
|
|
**Run services:**
|
|
```bash
|
|
python -m http.server 8080 # Dev servers on high ports
|
|
bun run app.js # Run JS directly
|
|
tmux / screen # Persistent sessions
|
|
```
|
|
|
|
**Available tools:** python3, uv, bun, node, npm, zig, git, vim, curl, tmux, opencode, bd
|
|
|
|
## Not Possible Right Now
|
|
|
|
| Want | Why | Workaround |
|
|
|------|-----|------------|
|
|
| sudo / root | Shared server security | Use nix profile or npm install -g |
|
|
| apt / yum | NixOS uses nix | `nix profile install nixpkgs#<pkg>` |
|
|
| Port 80/443 | Needs root | Use high port + SSH tunnel |
|
|
| Docker | Security isolation | Use nix for dependencies |
|
|
| systemd system services | Needs root | Use pm2, screen, or tmux |
|
|
|
|
## Resource Limits
|
|
|
|
Per-user limits to keep the server stable:
|
|
- **Memory**: ~1GB (50% of system)
|
|
- **Processes**: 200 max
|
|
- **Network**: 30 new connections/min
|
|
|
|
Heavy processes may be killed automatically.
|
|
|
|
## Environment
|
|
|
|
- **OS**: NixOS (not Debian/Ubuntu)
|
|
- **Shell**: bash
|
|
- **Home**: ~/ (private, 700)
|
|
- **Temp**: /tmp (fast, cleared on reboot)
|
|
AGENTS_EOF
|
|
chown "$username:users" "/home/$username/AGENTS.md"
|
|
|
|
# Set up git config for proper commit attribution
|
|
local gitconfig="/home/$username/.gitconfig"
|
|
cat > "$gitconfig" << EOF
|
|
[user]
|
|
name = $username
|
|
email = $username@clarun.xyz
|
|
[init]
|
|
defaultBranch = main
|
|
EOF
|
|
chown "$username:users" "$gitconfig"
|
|
log_info "Git config created"
|
|
|
|
# Provision Forgejo account for git.clarun.xyz access
|
|
provision_forgejo "$username" "$ssh_key"
|
|
|
|
if [[ "$user_exists" == true ]]; then
|
|
log_info "User updated"
|
|
else
|
|
log_info "User created with SSH access"
|
|
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}" \
|
|
--connect-timeout 5 --max-time 30 \
|
|
-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() {
|
|
local username="$1"
|
|
local login_key="$2"
|
|
local token_file="/run/secrets/forgejo-api-token"
|
|
local forgejo_url="http://localhost:3000"
|
|
|
|
# Check if token file exists
|
|
if [[ ! -r "$token_file" ]]; then
|
|
log_warn "Forgejo API token not found at $token_file"
|
|
log_warn "Skipping Forgejo provisioning - user can self-register at https://git.clarun.xyz"
|
|
return 0
|
|
fi
|
|
|
|
local token
|
|
token=$(cat "$token_file")
|
|
|
|
# Try to create Forgejo user (ignore if already exists)
|
|
local random_pass
|
|
random_pass=$(head -c 16 /dev/urandom | base64 | tr -d '/+=' | head -c 16)
|
|
|
|
local http_code
|
|
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
--connect-timeout 5 --max-time 30 \
|
|
-X POST "$forgejo_url/api/v1/admin/users" \
|
|
-H "Authorization: token $token" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"username\": \"$username\",
|
|
\"email\": \"$username@clarun.xyz\",
|
|
\"password\": \"$random_pass\",
|
|
\"must_change_password\": true,
|
|
\"send_notify\": false
|
|
}" 2>/dev/null) || true
|
|
|
|
local user_created=false
|
|
if [[ "$http_code" == "201" ]]; then
|
|
log_info "Forgejo user created"
|
|
user_created=true
|
|
elif [[ "$http_code" == "422" ]]; then
|
|
log_info "Forgejo user already exists"
|
|
else
|
|
log_warn "Forgejo user creation returned HTTP $http_code (may already exist)"
|
|
fi
|
|
|
|
# Write credentials file (only if we created the user and know the password)
|
|
if [[ "$user_created" == true ]]; then
|
|
local creds_file="/home/$username/.forgejo-credentials"
|
|
cat > "$creds_file" << EOF
|
|
{
|
|
"service": "forgejo",
|
|
"url": "https://git.clarun.xyz",
|
|
"username": "$username",
|
|
"initial_password": "$random_pass",
|
|
"must_change_password": true,
|
|
"note": "Delete this file after first login: rm ~/.forgejo-credentials"
|
|
}
|
|
EOF
|
|
chmod 600 "$creds_file"
|
|
chown "$username:users" "$creds_file"
|
|
log_info "Credentials written to ~/.forgejo-credentials"
|
|
fi
|
|
|
|
# Upload BOTH keys to Forgejo:
|
|
# 1. Login key (user's laptop key) - for direct laptop→git access
|
|
upload_forgejo_key "$username" "$login_key" "$username-laptop" "$token"
|
|
|
|
# 2. Server-side key - for git access from within the server
|
|
local server_pubkey_file="/home/$username/.ssh/id_ed25519.pub"
|
|
if [[ -r "$server_pubkey_file" ]]; then
|
|
local server_pubkey
|
|
server_pubkey=$(cat "$server_pubkey_file")
|
|
upload_forgejo_key "$username" "$server_pubkey" "$username-devserver" "$token"
|
|
else
|
|
log_warn "Server-side key not found at $server_pubkey_file"
|
|
fi
|
|
}
|
|
|
|
print_onboarding() {
|
|
local username="$1"
|
|
local server_ip
|
|
# NixOS: use ip command instead of hostname -I
|
|
server_ip=$(ip -4 addr show scope global | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo " Dev Environment Ready: $username"
|
|
echo "=========================================="
|
|
echo ""
|
|
echo "## SSH Config (~/.ssh/config on your laptop)"
|
|
echo ""
|
|
echo " Host dev-server"
|
|
echo " HostName ${server_ip:-<server-ip>}"
|
|
echo " User $username"
|
|
echo " LocalForward 8080 127.0.0.1:8080"
|
|
echo ""
|
|
echo "## Quick Start"
|
|
echo ""
|
|
echo "1. SSH in:"
|
|
echo " ssh dev-server"
|
|
echo ""
|
|
echo "2. Install AI tools (pick one or more):"
|
|
echo " bun install -g @anthropic-ai/claude-code # Claude"
|
|
echo " bun install -g @google/gemini-cli # Gemini"
|
|
echo ""
|
|
echo "3. Authenticate and start coding:"
|
|
echo " claude # or: gemini"
|
|
echo " # Follow prompts to authenticate"
|
|
echo ""
|
|
echo "## Git Access"
|
|
echo ""
|
|
echo " Clone repos: git clone forgejo@git.clarun.xyz:org/repo.git"
|
|
echo ""
|
|
echo " Forgejo account: $username"
|
|
echo " Credentials file: ~/.forgejo-credentials"
|
|
echo " Web UI: https://git.clarun.xyz"
|
|
echo ""
|
|
echo " Note: Change password on first login, then delete credentials file"
|
|
echo ""
|
|
echo "## Tools Available"
|
|
echo " System: python3, uv, git, bun, node, opencode, bd"
|
|
echo " Install more: bun install -g <package>"
|
|
echo " nix profile install nixpkgs#<package>"
|
|
echo ""
|
|
echo "=========================================="
|
|
}
|
|
|
|
main() {
|
|
if [[ $# -lt 2 ]]; then
|
|
usage
|
|
fi
|
|
|
|
local username="$1"
|
|
local ssh_key="$2"
|
|
|
|
# Must run as root
|
|
if [[ $EUID -ne 0 ]]; then
|
|
log_error "This script must be run as root"
|
|
exit 1
|
|
fi
|
|
|
|
validate_username "$username"
|
|
validate_ssh_key "$ssh_key"
|
|
|
|
# Check devs group exists (created by NixOS config)
|
|
if ! getent group devs >/dev/null; then
|
|
log_error "Required group 'devs' does not exist"
|
|
log_error "Ensure users.groups.devs = {} is in NixOS config and deployed"
|
|
exit 1
|
|
fi
|
|
|
|
create_user "$username" "$ssh_key"
|
|
print_onboarding "$username"
|
|
|
|
log_info "Dev user '$username' setup complete!"
|
|
}
|
|
|
|
main "$@"
|