firstboot: Add shell-ssh.sh module and restore shell-system.sh

- Create new shell-ssh.sh module for SSH key installation and password setup
  * Install SSH public keys to authorized_keys (root + clawdie)
  * Configure sshd: disable password auth if key provided, enable if not
  * Set system user passwords (auto-generate or use provided)
  * Save emergency root password to root/.firstboot-emergency-password
- Restore shell-system.sh (was accidentally overwritten during rename)

Enables secure SSH-key-first access with password fallback.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Sam & Claude 2026-03-24 07:45:50 +00:00
parent 640d44b5fe
commit ae11ae03ff
2 changed files with 460 additions and 199 deletions

260
firstboot/shell-ssh.sh Normal file
View file

@ -0,0 +1,260 @@
#!/bin/sh
# Clawdie Shell — SSH & System User Password Module
# Purpose: Configure SSH keys and system user passwords
# Author: Clawdie Project
# POSIX-compliant (no bash-isms)
set -eu
# Configuration (can be overridden for testing)
LOG_FILE="${LOG_FILE:-/var/log/clawdie-firstboot.log}"
PROGRESS_FILE="${PROGRESS_FILE:-/var/log/clawdie-firstboot.progress}"
EMERGENCY_PASSWORD_FILE="${EMERGENCY_PASSWORD_FILE:-/root/.firstboot-emergency-password}"
# Derived from wizard/build inputs (caller sets these)
# SSH_PUBLIC_KEY - Optional SSH public key (ssh-ed25519 or ssh-rsa)
# ROOT_PASSWORD - Optional root password (if empty, auto-generate)
# CLAWDIE_USER_PASSWORD - Optional clawdie user password (if empty, auto-generate)
# ============================================================================
# MAIN ENTRY POINT
# ============================================================================
clawdie_shell_ssh_setup() {
# Main orchestrator
# 1. Configure SSH keys (if provided)
# 2. Set system passwords (if provided or auto-generate)
# 3. Configure SSH auth methods (key-only or key+password)
log_msg "[ssh] Starting SSH and password setup"
# Generate emergency root password if not provided
if [ -z "${ROOT_PASSWORD:-}" ]; then
ROOT_PASSWORD=$(gen_secret)
fi
# Generate clawdie user password if not provided
if [ -z "${CLAWDIE_USER_PASSWORD:-}" ]; then
CLAWDIE_USER_PASSWORD=$(gen_secret)
fi
# Setup SSH keys if provided
if [ -n "${SSH_PUBLIC_KEY:-}" ]; then
clawdie_shell_ssh_install_pubkey
clawdie_shell_ssh_disable_password_auth
log_msg "[ssh] SSH public key installed, password auth disabled"
else
clawdie_shell_ssh_enable_password_auth
log_msg "[ssh] No SSH key provided, password auth enabled (less secure)"
fi
# Set system user passwords
clawdie_shell_ssh_set_passwords "$ROOT_PASSWORD" "$CLAWDIE_USER_PASSWORD"
log_msg "[ssh] System user passwords set"
# Save emergency root password to file
clawdie_shell_ssh_save_emergency_password "$ROOT_PASSWORD"
log_msg "[ssh] Emergency root password saved"
echo "[SSH] COMPLETE" >> "$PROGRESS_FILE"
log_msg "[ssh] SSH setup complete"
}
# ============================================================================
# SSH PUBLIC KEY INSTALLATION
# ============================================================================
clawdie_shell_ssh_install_pubkey() {
# Install SSH_PUBLIC_KEY to authorized_keys for both root and clawdie user
local root_ssh_dir="/root/.ssh"
local clawdie_ssh_dir="/home/clawdie/.ssh"
# Setup root authorized_keys
if [ ! -d "$root_ssh_dir" ]; then
mkdir -p "$root_ssh_dir"
chmod 700 "$root_ssh_dir"
fi
echo "$SSH_PUBLIC_KEY" >> "$root_ssh_dir/authorized_keys" 2>/dev/null || {
log_msg "[ssh] ERROR: Failed to install SSH key for root"
return 1
}
chmod 600 "$root_ssh_dir/authorized_keys"
log_msg "[ssh] Installed SSH key for root user"
# Setup clawdie authorized_keys
if [ ! -d "$clawdie_ssh_dir" ]; then
mkdir -p "$clawdie_ssh_dir"
chmod 700 "$clawdie_ssh_dir"
fi
echo "$SSH_PUBLIC_KEY" >> "$clawdie_ssh_dir/authorized_keys" 2>/dev/null || {
log_msg "[ssh] ERROR: Failed to install SSH key for clawdie"
return 1
}
chmod 600 "$clawdie_ssh_dir/authorized_keys"
chown -R clawdie:clawdie "$clawdie_ssh_dir"
log_msg "[ssh] Installed SSH key for clawdie user"
return 0
}
# ============================================================================
# SSH AUTH METHOD CONFIGURATION
# ============================================================================
clawdie_shell_ssh_disable_password_auth() {
# Disable password authentication in sshd_config
# Keep PubkeyAuthentication enabled
local sshd_config="/etc/ssh/sshd_config"
if [ ! -f "$sshd_config" ]; then
log_msg "[ssh] WARNING: sshd_config not found"
return 1
fi
# Ensure PubkeyAuthentication is enabled
if grep -q "^#*PubkeyAuthentication" "$sshd_config"; then
sed -i '' 's/^#*PubkeyAuthentication.*/PubkeyAuthentication yes/' "$sshd_config"
else
echo "PubkeyAuthentication yes" >> "$sshd_config"
fi
# Disable PasswordAuthentication
if grep -q "^#*PasswordAuthentication" "$sshd_config"; then
sed -i '' 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' "$sshd_config"
else
echo "PasswordAuthentication no" >> "$sshd_config"
fi
# Disable PermitRootLogin with password (allow pubkey only)
if grep -q "^#*PermitRootLogin" "$sshd_config"; then
sed -i '' 's/^#*PermitRootLogin.*/PermitRootLogin prohibit-password/' "$sshd_config"
else
echo "PermitRootLogin prohibit-password" >> "$sshd_config"
fi
log_msg "[ssh] Disabled password auth in sshd_config"
return 0
}
clawdie_shell_ssh_enable_password_auth() {
# Enable password authentication in sshd_config (fallback)
# Less secure but prevents lockout if user loses SSH key
local sshd_config="/etc/ssh/sshd_config"
if [ ! -f "$sshd_config" ]; then
log_msg "[ssh] WARNING: sshd_config not found"
return 1
fi
# Enable PasswordAuthentication
if grep -q "^#*PasswordAuthentication" "$sshd_config"; then
sed -i '' 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' "$sshd_config"
else
echo "PasswordAuthentication yes" >> "$sshd_config"
fi
# Allow root login with password
if grep -q "^#*PermitRootLogin" "$sshd_config"; then
sed -i '' 's/^#*PermitRootLogin.*/PermitRootLogin yes/' "$sshd_config"
else
echo "PermitRootLogin yes" >> "$sshd_config"
fi
log_msg "[ssh] WARNING: Password auth enabled (less secure)"
return 0
}
# ============================================================================
# PASSWORD MANAGEMENT
# ============================================================================
clawdie_shell_ssh_set_passwords() {
local root_pwd="$1"
local clawdie_pwd="$2"
# Set root password using echo | pw usermod
echo "$root_pwd" | pw usermod root -h 0 2>/dev/null || {
log_msg "[ssh] ERROR: Failed to set root password"
return 1
}
log_msg "[ssh] Root password set"
# Set clawdie user password
# Create user if it doesn't exist
if ! id clawdie >/dev/null 2>&1; then
pw useradd -n clawdie -u 1000 -g wheel -m -s /bin/sh 2>/dev/null || {
log_msg "[ssh] ERROR: Failed to create clawdie user"
return 1
}
log_msg "[ssh] Created clawdie user"
fi
echo "$clawdie_pwd" | pw usermod clawdie -h 0 2>/dev/null || {
log_msg "[ssh] ERROR: Failed to set clawdie password"
return 1
}
log_msg "[ssh] Clawdie user password set"
return 0
}
clawdie_shell_ssh_save_emergency_password() {
local root_pwd="$1"
# Save emergency password to file (root-only, readable only by root)
{
echo "EMERGENCY ROOT PASSWORD"
echo "=============================="
echo ""
echo "Generated at: $(date)"
echo "Password: $root_pwd"
echo ""
echo "Access:"
echo " ssh root@<domain>"
echo ""
echo "WARNING: Store this securely."
echo "This file should be moved to secure storage and deleted from this system."
} > "$EMERGENCY_PASSWORD_FILE" 2>/dev/null || {
log_msg "[ssh] WARNING: Could not save emergency password file"
return 1
}
chmod 600 "$EMERGENCY_PASSWORD_FILE"
log_msg "[ssh] Emergency password saved to $EMERGENCY_PASSWORD_FILE"
return 0
}
# ============================================================================
# UTILITY: Logging & Secret Generation
# ============================================================================
log_msg() {
local msg="$1"
echo "$msg" >> "$LOG_FILE" 2>/dev/null || true
}
gen_secret() {
openssl rand -base64 32 | tr -d '\n/+=' | head -c 32
}
# ============================================================================
# Export for use by firstboot.sh
# ============================================================================
case "${0##*/}" in
shell-ssh.sh)
# Direct execution (for testing)
clawdie_shell_ssh_setup
;;
*)
# Sourced from another script — functions available
;;
esac

View file

@ -1,232 +1,233 @@
#!/bin/sh
# Unit tests for clawdie-shell-system.sh
# Run: sh test-clawdie-shell-system.sh
# POSIX-compliant
# Clawdie Shell — System Configuration Module
# Purpose: System-level config (rc.conf, hostname, services, environment)
# Author: Clawdie Project
# POSIX-compliant (no bash-isms)
set -u
set -eu
# FreeBSD /bin/sh doesn't support trap ERR
TESTDIR="/tmp/clawdie-test-system-$$"
mkdir -p "$TESTDIR"
cd "$TESTDIR"
# Configuration (can be overridden for testing)
RC_CONF="${RC_CONF:-/etc/rc.conf}"
HOSTNAME_FILE="${HOSTNAME_FILE:-/etc/hostname}"
PROFILE_DIR="${PROFILE_DIR:-/etc/profile.d}"
LOG_FILE="${LOG_FILE:-/var/log/clawdie-firstboot.log}"
PROGRESS_FILE="${PROGRESS_FILE:-/var/log/clawdie-firstboot.progress}"
# Setup test environment
mkdir -p "$TESTDIR/etc/profile.d"
mkdir -p "$TESTDIR/var/log"
# Derived from wizard inputs (caller sets these)
# TZ - Timezone (e.g., "Europe/Ljubljana")
# AGENT_DOMAIN - FQDN (e.g., "clawdie.local")
# DETECTED_GPU - GPU vendor from gpu module (intel, amd, nvidia, vmware, vesa)
# Create test rc.conf
touch "$TESTDIR/etc/rc.conf"
# ============================================================================
# MAIN ENTRY POINT
# ============================================================================
# Environment overrides
export RC_CONF="$TESTDIR/etc/rc.conf"
export HOSTNAME_FILE="$TESTDIR/etc/hostname"
export PROFILE_DIR="$TESTDIR/etc/profile.d"
export LOG_FILE="$TESTDIR/var/log/clawdie-system-test.log"
export PROGRESS_FILE="$TESTDIR/var/log/clawdie-system-progress-test"
clawdie_shell_system_config() {
# Main orchestrator
# Test inputs
export TZ="Europe/Ljubljana"
export AGENT_DOMAIN="clawdie.local"
export DETECTED_GPU="intel"
log_msg "[system] Starting system configuration"
# Initialize log/progress files
touch "$LOG_FILE"
touch "$PROGRESS_FILE"
# Source the module
. "$(dirname "$0")/clawdie-shell-system.sh"
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
# Test counter
TESTS_PASSED=0
TESTS_FAILED=0
# Helper: assert
assert_eq() {
local name="$1"
local expected="$2"
local actual="$3"
if [ "$expected" = "$actual" ]; then
echo "${GREEN}${NC} $name"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo "${RED}${NC} $name"
echo " Expected: $expected"
echo " Actual: $actual"
TESTS_FAILED=$((TESTS_FAILED + 1))
if [ -z "${TZ:-}" ]; then
echo "ERROR: TZ not set" >&2
exit 1
fi
if [ -z "${AGENT_DOMAIN:-}" ]; then
echo "ERROR: AGENT_DOMAIN not set" >&2
exit 1
fi
# Write rc.conf with timezone and services
clawdie_shell_system_write_rcconf
log_msg "[system] Updated rc.conf"
# Set hostname
clawdie_shell_system_set_hostname
log_msg "[system] Set hostname"
# Setup environment
clawdie_shell_system_setup_env
log_msg "[system] Setup environment"
# Enable services
clawdie_shell_system_enable_services
log_msg "[system] Enabled services"
echo "[SYSTEM] COMPLETE" >> "$PROGRESS_FILE"
log_msg "[system] System configuration complete"
}
assert_file_exists() {
local name="$1"
local file="$2"
# ============================================================================
# RC.CONF CONFIGURATION
# ============================================================================
if [ -f "$file" ]; then
echo "${GREEN}${NC} $name"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo "${RED}${NC} $name (file not found: $file)"
TESTS_FAILED=$((TESTS_FAILED + 1))
clawdie_shell_system_write_rcconf() {
# Update /etc/rc.conf with:
# - timezone
# - service configurations (dbus, hald, seatd, lightdm)
# - Lumina desktop settings
if [ ! -f "$RC_CONF" ]; then
log_msg "[system] Creating $RC_CONF"
touch "$RC_CONF"
fi
# Helper to set or update rc.conf variable
clawdie_shell_system_sysrc "timezone=$TZ"
clawdie_shell_system_sysrc "dbus_enable=YES"
clawdie_shell_system_sysrc "hald_enable=YES"
clawdie_shell_system_sysrc "seatd_enable=YES"
clawdie_shell_system_sysrc "display_manager=lightdm"
clawdie_shell_system_sysrc "lightdm_enable=YES"
log_msg "[system] Wrote rc.conf configuration"
}
assert_file_contains() {
local name="$1"
local file="$2"
local pattern="$3"
clawdie_shell_system_sysrc() {
# Add or update a variable in rc.conf
# Input: VAR=VALUE
if [ -f "$file" ] && grep -q "$pattern" "$file"; then
echo "${GREEN}${NC} $name"
TESTS_PASSED=$((TESTS_PASSED + 1))
local var_assignment="$1"
local var_name var_value
var_name=$(echo "$var_assignment" | cut -d= -f1)
var_value=$(echo "$var_assignment" | cut -d= -f2-)
# Check if var already set (idempotence)
if grep -q "^${var_name}=" "$RC_CONF" 2>/dev/null; then
# Update existing (use | as delimiter to avoid issues with / in values)
sed -i '' "s|^${var_name}=.*|${var_assignment}|" "$RC_CONF"
else
echo "${RED}${NC} $name"
echo " File: $file"
echo " Pattern: $pattern"
TESTS_FAILED=$((TESTS_FAILED + 1))
# Append new
echo "$var_assignment" >> "$RC_CONF"
fi
}
# ============================================================================
# TEST SUITE
# HOSTNAME CONFIGURATION
# ============================================================================
echo "=== Clawdie Shell System Module Tests ==="
echo ""
clawdie_shell_system_set_hostname() {
# Set /etc/hostname and apply live
# Test 1: RC.CONF Configuration
echo "Test Group: RC.CONF Configuration"
clawdie_shell_system_write_rcconf
if [ ! -f "$HOSTNAME_FILE" ]; then
touch "$HOSTNAME_FILE"
fi
assert_file_contains "rc.conf contains timezone" "$RC_CONF" "timezone=Europe/Ljubljana"
assert_file_contains "rc.conf contains dbus_enable" "$RC_CONF" "dbus_enable=YES"
assert_file_contains "rc.conf contains hald_enable" "$RC_CONF" "hald_enable=YES"
assert_file_contains "rc.conf contains seatd_enable" "$RC_CONF" "seatd_enable=YES"
assert_file_contains "rc.conf contains display_manager" "$RC_CONF" "display_manager=lightdm"
assert_file_contains "rc.conf contains lightdm_enable" "$RC_CONF" "lightdm_enable=YES"
echo ""
# Write to file
echo "$AGENT_DOMAIN" > "$HOSTNAME_FILE"
# Test 2: RC.CONF Idempotence
echo "Test Group: RC.CONF Idempotence"
rm -f "$TESTDIR/etc/rc.conf"
touch "$TESTDIR/etc/rc.conf"
# Apply live (if not in chroot)
if command -v hostname >/dev/null 2>&1; then
hostname "$AGENT_DOMAIN" 2>/dev/null || true
fi
# Write multiple times
clawdie_shell_system_write_rcconf
clawdie_shell_system_write_rcconf
# Should only have one timezone line
timezone_count=$(grep -c "^timezone=" "$RC_CONF" || true)
assert_eq "Timezone appears only once" "1" "$timezone_count"
dbus_count=$(grep -c "^dbus_enable=" "$RC_CONF" || true)
assert_eq "dbus_enable appears only once" "1" "$dbus_count"
echo ""
# Test 3: Hostname Configuration
echo "Test Group: Hostname Configuration"
clawdie_shell_system_set_hostname
assert_file_exists "Hostname file created" "$HOSTNAME_FILE"
assert_file_contains "Hostname file contains domain" "$HOSTNAME_FILE" "clawdie.local"
echo ""
# Test 4: Environment Setup
echo "Test Group: Environment Setup"
clawdie_shell_system_setup_env
assert_file_exists "clawdie.sh profile created" "$PROFILE_DIR/clawdie.sh"
assert_file_contains "Profile has npm_config_prefix" "$PROFILE_DIR/clawdie.sh" "npm_config_prefix"
assert_file_contains "Profile has PATH export" "$PROFILE_DIR/clawdie.sh" "PATH"
echo ""
# Test 5: Full Setup Flow
echo "Test Group: Full Setup Flow"
rm -f "$TESTDIR/etc/rc.conf" "$TESTDIR/etc/hostname" "$PROGRESS_FILE"
touch "$TESTDIR/etc/rc.conf"
touch "$PROGRESS_FILE"
clawdie_shell_system_config 2>/dev/null
assert_file_exists "rc.conf exists" "$RC_CONF"
assert_file_exists "hostname file exists" "$HOSTNAME_FILE"
assert_file_exists "profile exists" "$PROFILE_DIR/clawdie.sh"
if grep -q "\[SYSTEM\] COMPLETE" "$PROGRESS_FILE"; then
echo "${GREEN}${NC} Progress checkpoint logged"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo "${RED}${NC} Progress checkpoint not found"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
echo ""
# Test 6: Validation
echo "Test Group: Validation"
if clawdie_shell_system_validate 2>/dev/null; then
echo "${GREEN}${NC} Validation passes"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo "${RED}${NC} Validation failed"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
echo ""
# Test 7: SYSRC Update Behavior
echo "Test Group: SYSRC Update Behavior"
# Already tested above, but verify update specifically
rm -f "$TESTDIR/etc/rc.conf"
touch "$TESTDIR/etc/rc.conf"
clawdie_shell_system_sysrc "update_test=oldvalue"
clawdie_shell_system_sysrc "update_test=newvalue"
# Ensure only one line exists
update_count=$(grep -c "^update_test=" "$TESTDIR/etc/rc.conf" || true)
if [ "$update_count" = "1" ]; then
echo "${GREEN}${NC} SYSRC properly updates existing vars"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo "${RED}${NC} SYSRC created duplicate entries ($update_count lines)"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
echo ""
# Test 8: SYSRC Helper Function
echo "Test Group: SYSRC Helper"
rm -f "$TESTDIR/etc/rc.conf"
touch "$TESTDIR/etc/rc.conf"
clawdie_shell_system_sysrc "test_var=value1"
assert_file_contains "New var added" "$RC_CONF" "test_var=value1"
clawdie_shell_system_sysrc "test_var=value2"
test_count=$(grep -c "^test_var=" "$RC_CONF" || true)
assert_eq "SYSRC updates existing var" "1" "$test_count"
actual_value=$(grep "^test_var=" "$RC_CONF" | cut -d= -f2)
assert_eq "SYSRC sets correct value" "value2" "$actual_value"
echo ""
log_msg "[system] Set hostname to $AGENT_DOMAIN"
}
# ============================================================================
# SUMMARY
# ENVIRONMENT SETUP
# ============================================================================
echo "=== Test Results ==="
echo "${GREEN}Passed: $TESTS_PASSED${NC}"
echo "${RED}Failed: $TESTS_FAILED${NC}"
echo ""
clawdie_shell_system_setup_env() {
# Create /etc/profile.d/clawdie.sh for environment variables
# Sets up npm global paths and other Clawdie-specific variables
# Cleanup
rm -rf "$TESTDIR"
if [ ! -d "$PROFILE_DIR" ]; then
mkdir -p "$PROFILE_DIR"
fi
if [ $TESTS_FAILED -eq 0 ]; then
echo "${GREEN}✓ All tests passed!${NC}"
exit 0
else
echo "${RED}✗ Some tests failed${NC}"
exit 1
fi
local clawdie_profile="$PROFILE_DIR/clawdie.sh"
cat > "$clawdie_profile" <<'EOF'
# Clawdie-AI environment setup
# Adds npm global bin directory to PATH
export npm_config_prefix="${HOME}/.npm-global"
export PATH="${HOME}/.npm-global/bin:${PATH}"
EOF
chmod 644 "$clawdie_profile"
log_msg "[system] Created $clawdie_profile"
}
# ============================================================================
# SERVICE ENABLEMENT
# ============================================================================
clawdie_shell_system_enable_services() {
# Enable and start required services
# Safe to fail if running in chroot (first boot)
local services="dbus hald seatd lightdm"
for service in $services; do
if command -v service >/dev/null 2>&1; then
# Try to start service
service "$service" onestart 2>/dev/null || {
log_msg "[system] Could not start $service (expected in chroot)"
}
fi
done
log_msg "[system] Service enablement complete"
}
# ============================================================================
# VALIDATION
# ============================================================================
clawdie_shell_system_validate() {
# Verify system configuration completed
if [ ! -f "$RC_CONF" ]; then
echo "ERROR: rc.conf not found" >&2
return 1
fi
# Check timezone is set
if ! grep -q "^timezone=" "$RC_CONF"; then
echo "ERROR: timezone not set in rc.conf" >&2
return 1
fi
# Check hostname file exists
if [ ! -f "$HOSTNAME_FILE" ]; then
echo "ERROR: $HOSTNAME_FILE not created" >&2
return 1
fi
# Check environment profile exists
if [ ! -f "$PROFILE_DIR/clawdie.sh" ]; then
echo "ERROR: $PROFILE_DIR/clawdie.sh not created" >&2
return 1
fi
log_msg "[system] Validation passed"
return 0
}
# ============================================================================
# UTILITY: Logging
# ============================================================================
log_msg() {
local msg="$1"
echo "$msg" >> "$LOG_FILE" 2>/dev/null || true
}
# ============================================================================
# Export for use by firstboot.sh
# ============================================================================
case "${0##*/}" in
clawdie-shell-system.sh)
# Direct execution (for testing)
clawdie_shell_system_config
clawdie_shell_system_validate
;;
*)
# Sourced from another script — functions available
;;
esac