From 817d223918672c43fb20b9db270ad7dbd32431e6 Mon Sep 17 00:00:00 2001 From: LukhsaanE Date: Wed, 12 Nov 2025 12:52:15 -0500 Subject: [PATCH 1/4] Initial self hosted runner provision script, to be tested --- scripts/provision/provision_runner.sh | 348 ++++++++++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 scripts/provision/provision_runner.sh diff --git a/scripts/provision/provision_runner.sh b/scripts/provision/provision_runner.sh new file mode 100644 index 000000000..011a22063 --- /dev/null +++ b/scripts/provision/provision_runner.sh @@ -0,0 +1,348 @@ +#!/usr/bin/env bash +# ---------------------------------------------------------------------- +# Provisioning script for a self-hosted GitHub Actions runner (Ubuntu) +# +# Purpose: +# Install and configure a hardened, idempotent GitHub Actions runner +# intended for SIL testing with optional SocketCAN support (virtual +# vcan0 or physical CAN adapters). +# +# Key features: +# - Installs runner binaries and registers the runner against a +# repository using GH_RUNNER_TOKEN. +# - Creates a least-privileged user `github-runner` (no sudo). +# - Installs security tooling: ufw, fail2ban, apparmor, auditd, +# unattended-upgrades, and systemd service hardening. +# - Optional SocketCAN support: +# CAN_TYPE=virtual -> creates vcan0 +# CAN_TYPE=physical -> installs CAN utilities and serial CAN tools +# - Installs Python tools useful for SIL: python-can, cantools. +# - Applies AppArmor profile and systemd limits to mitigate malicious +# code (fork bombs, disk flooding, privilege escalation). +# - Idempotent: safe to run multiple times; performs checks before +# making changes. +# +# Usage: +# sudo GH_REPO_URL="https://github.com/OWNER/REPO" \ +# GH_RUNNER_TOKEN="REPLACE_ME" \ +# GH_RUNNER_NAME="sil-runner-01" \ +# GH_RUNNER_LABELS="self-hosted,linux,sil,socketcan" \ +# RUNNER_ARCH="x64" \ +# CAN_TYPE="virtual" \ +# bash provision_runner.sh +# +# RUNNER_ARCH options: x64, arm64, arm +# CAN_TYPE options: virtual, physical, none +# +# Required: +# - Internet access to download packages and runner archive +# - GH_RUNNER_TOKEN (from repository Settings → Actions → Runners) +# ---------------------------------------------------------------------- + +set -euo pipefail + +# ---- Config from env ---- +: "${GH_REPO_URL:?Set GH_REPO_URL to https://github.com/OWNER/REPO}" +: "${GH_RUNNER_TOKEN:?Set GH_RUNNER_TOKEN from GitHub Runners page}" +GH_RUNNER_NAME="${GH_RUNNER_NAME:-sil-runner-01}" +GH_RUNNER_LABELS="${GH_RUNNER_LABELS:-self-hosted,linux,sil}" +RUNNER_ARCH="${RUNNER_ARCH:-x64}" +CAN_TYPE="${CAN_TYPE:-none}" +RUNNER_VER="${RUNNER_VER:-2.329.0}" + +# Validate architecture +case "$RUNNER_ARCH" in + x64|arm64|arm) ;; + *) + echo "Invalid RUNNER_ARCH: $RUNNER_ARCH (must be x64, arm64, or arm)" + exit 1 + ;; +esac + +# Validate CAN type +case "$CAN_TYPE" in + virtual|physical|none) ;; + *) + echo "Invalid CAN_TYPE: $CAN_TYPE (must be virtual, physical, or none)" + exit 1 + ;; +esac + +# ---- Base packages + security tools ---- +export DEBIAN_FRONTEND=noninteractive +apt-get update +apt-get install -y curl tar jq git ca-certificates ufw fail2ban unattended-upgrades \ + apparmor apparmor-utils auditd + +# CAN tooling (install if any CAN is needed) +if [ "$CAN_TYPE" != "none" ]; then + apt-get install -y iproute2 can-utils || true + + # Python and CAN libraries for SIL + apt-get install -y python3 python3-pip python3-venv || true + pip3 install --break-system-packages python-can cantools || true + + # Verify SocketCAN kernel modules + if ! lsmod | grep -q "^can"; then + modprobe can || echo "Warning: CAN kernel module not available" + fi + if ! lsmod | grep -q "^can_raw"; then + modprobe can_raw || echo "Warning: CAN_RAW kernel module not available" + fi + if ! lsmod | grep -q "^vcan"; then + modprobe vcan || echo "Warning: VCAN kernel module not available" + fi + + # Load CAN modules on boot + cat >/etc/modules-load.d/can.conf <<'EOF' +can +can_raw +vcan +can_dev +EOF + + # Physical CAN additional tools + if [ "$CAN_TYPE" = "physical" ]; then + # Install slcand for serial CAN adapters + apt-get install -y can-utils slcan-utils || true + fi +fi + +# ---- Enable automatic security updates ---- +cat >/etc/apt/apt.conf.d/50unattended-upgrades <<'EOF' +Unattended-Upgrade::Allowed-Origins { + "${distro_id}:${distro_codename}-security"; +}; +Unattended-Upgrade::AutoFixInterruptedDpkg "true"; +Unattended-Upgrade::MinimalSteps "true"; +Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; +Unattended-Upgrade::Remove-Unused-Dependencies "true"; +Unattended-Upgrade::Automatic-Reboot "false"; +EOF + +cat >/etc/apt/apt.conf.d/20auto-upgrades <<'EOF' +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Unattended-Upgrade "1"; +APT::Periodic::AutocleanInterval "7"; +EOF + +systemctl enable unattended-upgrades +systemctl restart unattended-upgrades + +# ---- Least-privileged runner user (NO SUDO) ---- +if ! id -u github-runner >/dev/null 2>&1; then + adduser --disabled-password --gecos "" github-runner +fi + +# Ensure runner has NO sudo access +rm -f /etc/sudoers.d/github-runner + +# Strict user limits to prevent fork bombs and resource exhaustion +cat >/etc/security/limits.d/github-runner.conf <<'EOF' +github-runner soft nproc 256 +github-runner hard nproc 512 +github-runner soft nofile 4096 +github-runner hard nofile 8192 +github-runner soft fsize 2097152 +github-runner hard fsize 2097152 +github-runner soft cpu 60 +github-runner hard cpu 120 +EOF + +# Create runner directory with restricted permissions +install -d -o github-runner -g github-runner -m 0750 /opt/actions-runner + +# ---- AppArmor profile for runner ---- +if [ ! -f /etc/apparmor.d/opt.actions-runner.Runner.Listener ]; then +cat >/etc/apparmor.d/opt.actions-runner.Runner.Listener <<'EOF' +#include + +/opt/actions-runner/bin/Runner.Listener { + #include + #include + + # Allow runner to execute + /opt/actions-runner/** r, + /opt/actions-runner/bin/* ix, + + # Allow work directory + /opt/actions-runner/_work/** rw, + + # Network access for GitHub API + network inet stream, + network inet6 stream, + + # SocketCAN access + network can raw, + /sys/class/net/vcan0/** r, + /sys/class/net/can0/** r, + /dev/ttyUSB* rw, + + # Required system access + /proc/sys/kernel/random/uuid r, + /sys/devices/virtual/net/vcan0/** r, + + # Deny dangerous paths + deny /etc/shadow r, + deny /etc/sudoers* r, + deny /root/** rw, + deny /home/*/.ssh/** rw, + + # Deny writing to system locations + deny /boot/** w, + deny /etc/** w, + deny /sys/** w, +} +EOF +fi + +# Load AppArmor profile +apparmor_parser -r /etc/apparmor.d/opt.actions-runner.Runner.Listener || true + +# ---- Restrictive firewall ---- +ufw --force reset >/dev/null 2>&1 || true +ufw default deny incoming +ufw default allow outgoing +ufw limit OpenSSH +echo "y" | ufw enable >/dev/null 2>&1 || true + +# ---- SSH hardening with fail2ban ---- +if [ ! -f /etc/fail2ban/jail.local ]; then +cat >/etc/fail2ban/jail.local <<'EOF' +[DEFAULT] +bantime = 3600 +findtime = 600 +maxretry = 3 + +[sshd] +enabled = true +port = ssh +logpath = %(sshd_log)s +backend = %(sshd_backend)s +EOF +fi + +systemctl enable fail2ban +systemctl restart fail2ban + +# ---- Optional: SocketCAN vcan0 ---- +if [ "$CAN_TYPE" = "virtual" ]; then + modprobe vcan || true + cat >/etc/systemd/system/vcan0.service <<'EOF' +[Unit] +Description=Bring up vcan0 (virtual CAN) +After=network.target +[Service] +Type=oneshot +ExecStart=/sbin/ip link add dev vcan0 type vcan || true +ExecStart=/sbin/ip link set up vcan0 +RemainAfterExit=yes +[Install] +WantedBy=multi-user.target +EOF + systemctl daemon-reload + systemctl enable --now vcan0.service || true +fi + +# ---- Audit logging ---- +systemctl enable auditd +systemctl restart auditd + +# Add rules to monitor runner activity (idempotent check) +if ! grep -q "runner_changes" /etc/audit/rules.d/runner.rules 2>/dev/null; then +cat >>/etc/audit/rules.d/runner.rules <<'EOF' +# Monitor runner directory changes +-w /opt/actions-runner/ -p wa -k runner_changes + +# Monitor dangerous syscalls +-a always,exit -F arch=b64 -S execve -F euid=github-runner -k runner_exec +EOF +fi + +augenrules --load || true + +# ---- Download runner ---- +if [ ! -f /opt/actions-runner/bin/Runner.Listener ]; then +su - github-runner -c "cd /opt/actions-runner && \ + curl -fsSL -o actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VER}.tar.gz \ + https://github.com/actions/runner/releases/download/v${RUNNER_VER}/actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VER}.tar.gz && \ + tar xzf actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VER}.tar.gz" +fi + +# ---- Register runner with labels ---- +if [ ! -f /opt/actions-runner/.runner ]; then +su - github-runner -c "cd /opt/actions-runner && \ + ./config.sh --url '${GH_REPO_URL}' \ + --token '${GH_RUNNER_TOKEN}' \ + --name '${GH_RUNNER_NAME}' \ + --labels '${GH_RUNNER_LABELS}' \ + --work _work \ + --unattended" +fi + +# ---- Install as a service with resource limits ---- +if ! systemctl is-active --quiet actions.runner.*; then + /opt/actions-runner/svc.sh install github-runner || true +fi + +# Apply strict systemd limits (idempotent) +mkdir -p /etc/systemd/system/actions.runner.*.service.d/ +cat >/etc/systemd/system/actions.runner.*.service.d/limits.conf <<'EOF' +[Service] +# Security hardening +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=yes +ReadWritePaths=/opt/actions-runner/_work + +# Resource limits (prevent fork bombs, disk flooding, CPU exhaustion) +LimitCORE=0 +LimitNOFILE=4096 +LimitNPROC=512 +LimitFSIZE=2147483648 +CPUQuota=150% +MemoryMax=2G +TasksMax=256 + +# Prevent runaway processes +TimeoutStopSec=30 + +# Network isolation (allow only outbound) +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_CAN + +# Prevent privilege escalation +CapabilityBoundingSet= +AmbientCapabilities= + +# Additional protections against malicious code +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectControlGroups=yes +ReadOnlyPaths=/opt/actions-runner/bin +SystemCallFilter=@system-service +SystemCallFilter=~@privileged @resources + +# Prevent access to sensitive files +InaccessiblePaths=/root /home /etc/shadow /etc/sudoers /boot + +# Disk quota for work directory (prevent disk flooding) +# Note: Requires quotas enabled on filesystem +EOF + +systemctl daemon-reload + +# Start or restart service +if systemctl is-active --quiet actions.runner.*; then + systemctl restart actions.runner.* || true +else + /opt/actions-runner/svc.sh start || true +fi + +# ---- Cleanup job for old work directories ---- +if ! sudo -u github-runner crontab -l 2>/dev/null | grep -q "_work"; then + (sudo -u github-runner crontab -l 2>/dev/null; echo '15 3 * * 0 find /opt/actions-runner/_work -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \;') | sudo -u github-runner crontab - +fi + +echo "Runner '${GH_RUNNER_NAME}' provisioned with security hardening." +echo "Verify in GitHub → Settings → Actions → Runners." \ No newline at end of file From a8e6022dfe45dfd1957242800929d1ac40b62ee6 Mon Sep 17 00:00:00 2001 From: LukhsaanE Date: Wed, 12 Nov 2025 13:14:30 -0500 Subject: [PATCH 2/4] Rename file --- scripts/provision/{provision_runner.sh => github_runner.sh} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename scripts/provision/{provision_runner.sh => github_runner.sh} (100%) diff --git a/scripts/provision/provision_runner.sh b/scripts/provision/github_runner.sh similarity index 100% rename from scripts/provision/provision_runner.sh rename to scripts/provision/github_runner.sh From fd83b9dfb89a969ebc0d35e8b757efcc164076ee Mon Sep 17 00:00:00 2001 From: LukhsaanE Date: Fri, 14 Nov 2025 20:31:42 -0500 Subject: [PATCH 3/4] Fix runner script, works on Linux VM, idempotent, add comments --- scripts/provision/github_runner.sh | 441 +++++++++++++++++++---------- 1 file changed, 289 insertions(+), 152 deletions(-) diff --git a/scripts/provision/github_runner.sh b/scripts/provision/github_runner.sh index 011a22063..af2d41481 100644 --- a/scripts/provision/github_runner.sh +++ b/scripts/provision/github_runner.sh @@ -1,56 +1,74 @@ #!/usr/bin/env bash -# ---------------------------------------------------------------------- -# Provisioning script for a self-hosted GitHub Actions runner (Ubuntu) +# ============================================================================== +# GitHub Actions Self-Hosted Runner Provisioning Script (Ubuntu) +# ============================================================================== # -# Purpose: -# Install and configure a hardened, idempotent GitHub Actions runner -# intended for SIL testing with optional SocketCAN support (virtual -# vcan0 or physical CAN adapters). +# PURPOSE: +# Provisions a hardened, security-focused GitHub Actions self-hosted runner +# for Software-in-the-Loop (SIL) testing with optional SocketCAN support. # -# Key features: -# - Installs runner binaries and registers the runner against a -# repository using GH_RUNNER_TOKEN. -# - Creates a least-privileged user `github-runner` (no sudo). -# - Installs security tooling: ufw, fail2ban, apparmor, auditd, -# unattended-upgrades, and systemd service hardening. -# - Optional SocketCAN support: -# CAN_TYPE=virtual -> creates vcan0 -# CAN_TYPE=physical -> installs CAN utilities and serial CAN tools -# - Installs Python tools useful for SIL: python-can, cantools. -# - Applies AppArmor profile and systemd limits to mitigate malicious -# code (fork bombs, disk flooding, privilege escalation). -# - Idempotent: safe to run multiple times; performs checks before -# making changes. +# SECURITY FEATURES: +# 1. Least-Privilege User: Runner executes as 'github-runner' (NO sudo access) +# 2. Resource Limits: CPU, memory, process, and disk quotas prevent DoS attacks +# 3. Network Isolation: Firewall (ufw) restricts all inbound except SSH +# 4. Intrusion Detection: fail2ban protects SSH; auditd monitors runner activity +# 5. Mandatory Access Control: AppArmor profile restricts file/network access +# 6. Automatic Updates: unattended-upgrades applies security patches +# 7. Audit Logging: Tracks all runner executions and file modifications +# 8. Process Isolation: systemd sandboxing (PrivateTmp, ProtectSystem, etc.) # -# Usage: +# DEFENDS AGAINST: +# - Privilege escalation (no sudo, capability dropping, NoNewPrivileges) +# - Fork bombs (nproc limits, TasksMax in systemd) +# - Disk flooding (FSIZE limits, work directory cleanup cron) +# - CPU exhaustion (CPUQuota, cpu time limits) +# - Memory exhaustion (MemoryMax in systemd) +# - Malicious code execution (AppArmor profile, filesystem restrictions) +# +# USAGE: # sudo GH_REPO_URL="https://github.com/OWNER/REPO" \ -# GH_RUNNER_TOKEN="REPLACE_ME" \ +# GH_RUNNER_TOKEN="YOUR_TOKEN_HERE" \ # GH_RUNNER_NAME="sil-runner-01" \ # GH_RUNNER_LABELS="self-hosted,linux,sil,socketcan" \ # RUNNER_ARCH="x64" \ # CAN_TYPE="virtual" \ # bash provision_runner.sh # -# RUNNER_ARCH options: x64, arm64, arm -# CAN_TYPE options: virtual, physical, none +# PARAMETERS: +# GH_REPO_URL - GitHub repository URL (required) +# GH_RUNNER_TOKEN - Runner registration token from GitHub (required) +# GH_RUNNER_NAME - Runner display name (default: sil-runner-01) +# GH_RUNNER_LABELS - Comma-separated labels (default: self-hosted,linux,sil) +# RUNNER_ARCH - Architecture: x64, arm64, or arm (default: x64) +# CAN_TYPE - SocketCAN type: virtual, physical, or none (default: none) +# RUNNER_VER - GitHub runner version (default: 2.329.0) +# +# IDEMPOTENCY: +# Safe to run multiple times. All operations check for existing state before +# making changes. Re-running will update configurations without breaking the +# existing runner installation. # -# Required: -# - Internet access to download packages and runner archive -# - GH_RUNNER_TOKEN (from repository Settings → Actions → Runners) -# ---------------------------------------------------------------------- +# ============================================================================== +# Exit immediately on error, treat unset variables as errors, fail on pipe errors set -euo pipefail -# ---- Config from env ---- +# ============================================================================== +# CONFIGURATION: Load and validate environment variables +# ============================================================================== + +# Required parameters (script exits if not set) : "${GH_REPO_URL:?Set GH_REPO_URL to https://github.com/OWNER/REPO}" : "${GH_RUNNER_TOKEN:?Set GH_RUNNER_TOKEN from GitHub Runners page}" + +# Optional parameters with sensible defaults GH_RUNNER_NAME="${GH_RUNNER_NAME:-sil-runner-01}" GH_RUNNER_LABELS="${GH_RUNNER_LABELS:-self-hosted,linux,sil}" RUNNER_ARCH="${RUNNER_ARCH:-x64}" CAN_TYPE="${CAN_TYPE:-none}" RUNNER_VER="${RUNNER_VER:-2.329.0}" -# Validate architecture +# Validate architecture matches GitHub Actions runner releases case "$RUNNER_ARCH" in x64|arm64|arm) ;; *) @@ -59,7 +77,7 @@ case "$RUNNER_ARCH" in ;; esac -# Validate CAN type +# Validate CAN interface type case "$CAN_TYPE" in virtual|physical|none) ;; *) @@ -68,21 +86,40 @@ case "$CAN_TYPE" in ;; esac -# ---- Base packages + security tools ---- +echo "[INFO] Provisioning GitHub runner: $GH_RUNNER_NAME" +echo "[INFO] Repository: $GH_REPO_URL" +echo "[INFO] Architecture: $RUNNER_ARCH | CAN Type: $CAN_TYPE | Runner Version: $RUNNER_VER" + +# ============================================================================== +# STEP 1: Install base packages and security tools +# ============================================================================== +# SECURITY: Install essential security packages to harden the system +# - ufw: Uncomplicated Firewall for network isolation +# - fail2ban: Intrusion prevention (blocks brute-force SSH attacks) +# - apparmor/apparmor-utils: Mandatory Access Control (MAC) system +# - auditd: Audit daemon for tracking system calls and file access +# - unattended-upgrades: Automatic security patch installation + export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y curl tar jq git ca-certificates ufw fail2ban unattended-upgrades \ apparmor apparmor-utils auditd -# CAN tooling (install if any CAN is needed) +# ============================================================================== +# STEP 2: Install SocketCAN tooling (if CAN support is requested) +# ============================================================================== +# Install CAN bus utilities for Software-in-the-Loop (SIL) testing +# Supports both virtual CAN (vcan) and physical CAN adapters + if [ "$CAN_TYPE" != "none" ]; then + # Install CAN utilities: ip (for CAN interface mgmt), candump, cansend, etc. apt-get install -y iproute2 can-utils || true - - # Python and CAN libraries for SIL + + # Install Python CAN libraries for SIL test scripts apt-get install -y python3 python3-pip python3-venv || true pip3 install --break-system-packages python-can cantools || true - - # Verify SocketCAN kernel modules + + # Load SocketCAN kernel modules (required for CAN communication) if ! lsmod | grep -q "^can"; then modprobe can || echo "Warning: CAN kernel module not available" fi @@ -92,8 +129,8 @@ if [ "$CAN_TYPE" != "none" ]; then if ! lsmod | grep -q "^vcan"; then modprobe vcan || echo "Warning: VCAN kernel module not available" fi - - # Load CAN modules on boot + + # Persist CAN modules across reboots cat >/etc/modules-load.d/can.conf <<'EOF' can can_raw @@ -101,14 +138,18 @@ vcan can_dev EOF - # Physical CAN additional tools + # For physical CAN: install serial CAN adapter tools (slcand) if [ "$CAN_TYPE" = "physical" ]; then - # Install slcand for serial CAN adapters apt-get install -y can-utils slcan-utils || true fi fi -# ---- Enable automatic security updates ---- +# ============================================================================== +# STEP 3: Configure automatic security updates +# ============================================================================== +# SECURITY: Automatically apply security patches to prevent exploitation of +# known vulnerabilities. System will not auto-reboot to avoid runner downtime. + cat >/etc/apt/apt.conf.d/50unattended-upgrades <<'EOF' Unattended-Upgrade::Allowed-Origins { "${distro_id}:${distro_codename}-security"; @@ -129,91 +170,62 @@ EOF systemctl enable unattended-upgrades systemctl restart unattended-upgrades -# ---- Least-privileged runner user (NO SUDO) ---- +# ============================================================================== +# STEP 4: Create least-privileged runner user +# ============================================================================== +# SECURITY: Runner executes as 'github-runner' user with NO sudo privileges +# This prevents malicious workflows from escalating privileges or modifying +# system files. Even if a workflow is compromised, damage is contained. + if ! id -u github-runner >/dev/null 2>&1; then adduser --disabled-password --gecos "" github-runner fi -# Ensure runner has NO sudo access +# SECURITY: Explicitly remove any sudo access (defense in depth) rm -f /etc/sudoers.d/github-runner -# Strict user limits to prevent fork bombs and resource exhaustion +# SECURITY: Apply resource limits to prevent DoS attacks +# nproc: limits process count (prevents fork bombs) +# nofile: limits open file descriptors (prevents fd exhaustion) +# cpu: limits CPU time per process (prevents CPU exhaustion) cat >/etc/security/limits.d/github-runner.conf <<'EOF' github-runner soft nproc 256 github-runner hard nproc 512 github-runner soft nofile 4096 github-runner hard nofile 8192 -github-runner soft fsize 2097152 -github-runner hard fsize 2097152 github-runner soft cpu 60 github-runner hard cpu 120 EOF -# Create runner directory with restricted permissions -install -d -o github-runner -g github-runner -m 0750 /opt/actions-runner - -# ---- AppArmor profile for runner ---- -if [ ! -f /etc/apparmor.d/opt.actions-runner.Runner.Listener ]; then -cat >/etc/apparmor.d/opt.actions-runner.Runner.Listener <<'EOF' -#include - -/opt/actions-runner/bin/Runner.Listener { - #include - #include - - # Allow runner to execute - /opt/actions-runner/** r, - /opt/actions-runner/bin/* ix, - - # Allow work directory - /opt/actions-runner/_work/** rw, - - # Network access for GitHub API - network inet stream, - network inet6 stream, - - # SocketCAN access - network can raw, - /sys/class/net/vcan0/** r, - /sys/class/net/can0/** r, - /dev/ttyUSB* rw, - - # Required system access - /proc/sys/kernel/random/uuid r, - /sys/devices/virtual/net/vcan0/** r, - - # Deny dangerous paths - deny /etc/shadow r, - deny /etc/sudoers* r, - deny /root/** rw, - deny /home/*/.ssh/** rw, - - # Deny writing to system locations - deny /boot/** w, - deny /etc/** w, - deny /sys/** w, -} -EOF -fi +# Create runner installation directory with restricted permissions +install -d -o github-runner -g github-runner -m 0755 /opt/actions-runner -# Load AppArmor profile -apparmor_parser -r /etc/apparmor.d/opt.actions-runner.Runner.Listener || true +# ============================================================================== +# STEP 5: Configure restrictive firewall +# ============================================================================== +# SECURITY: Network isolation - deny all inbound except SSH +# This prevents external attacks and limits the attack surface. +# Runner only needs outbound access to GitHub API and package repos. -# ---- Restrictive firewall ---- ufw --force reset >/dev/null 2>&1 || true ufw default deny incoming ufw default allow outgoing -ufw limit OpenSSH +# Allow SSH with rate limiting (prevents brute-force) +ufw limit OpenSSH 2>/dev/null || ufw limit ssh 2>/dev/null || ufw limit 22/tcp echo "y" | ufw enable >/dev/null 2>&1 || true -# ---- SSH hardening with fail2ban ---- +# ============================================================================== +# STEP 6: Configure SSH intrusion prevention (fail2ban) +# ============================================================================== +# SECURITY: Protects SSH from brute-force attacks by banning IPs after +# 3 failed login attempts within 10 minutes. Ban lasts 1 hour. + if [ ! -f /etc/fail2ban/jail.local ]; then cat >/etc/fail2ban/jail.local <<'EOF' [DEFAULT] bantime = 3600 findtime = 600 maxretry = 3 - [sshd] enabled = true port = ssh @@ -225,78 +237,145 @@ fi systemctl enable fail2ban systemctl restart fail2ban -# ---- Optional: SocketCAN vcan0 ---- +# ============================================================================== +# STEP 7: Configure SocketCAN interface (if requested) +# ============================================================================== +# Creates a persistent CAN interface for SIL testing +# - virtual: vcan0 (software-only, no hardware needed) +# - physical: can0 (requires hardware CAN adapter) + if [ "$CAN_TYPE" = "virtual" ]; then - modprobe vcan || true + # Virtual CAN interface for testing without hardware + modprobe vcan 2>/dev/null || true cat >/etc/systemd/system/vcan0.service <<'EOF' [Unit] Description=Bring up vcan0 (virtual CAN) After=network.target [Service] Type=oneshot -ExecStart=/sbin/ip link add dev vcan0 type vcan || true +ExecStart=/bin/bash -c '/sbin/ip link add dev vcan0 type vcan 2>/dev/null || true' ExecStart=/sbin/ip link set up vcan0 RemainAfterExit=yes [Install] WantedBy=multi-user.target EOF systemctl daemon-reload - systemctl enable --now vcan0.service || true + systemctl enable vcan0.service + systemctl restart vcan0.service || true + +elif [ "$CAN_TYPE" = "physical" ]; then + # Physical CAN interface (requires hardware adapter like MCP2515 or USB-CAN) + cat >/etc/systemd/system/can0.service <<'EOF' +[Unit] +Description=Bring up can0 (physical CAN interface) +After=network.target +[Service] +Type=oneshot +# For native SocketCAN devices (e.g., MCP2515) +ExecStart=/sbin/ip link set can0 type can bitrate 500000 +ExecStart=/sbin/ip link set can0 up +ExecStop=/sbin/ip link set can0 down +RemainAfterExit=yes +[Install] +WantedBy=multi-user.target +EOF + systemctl daemon-reload + systemctl enable can0.service + systemctl restart can0.service || true + + # NOTE: For serial CAN adapters (slcan), run slcand before enabling service: + # slcand -o -c -s6 /dev/ttyUSB0 can0 fi -# ---- Audit logging ---- +# ============================================================================== +# STEP 8: Configure audit logging +# ============================================================================== +# SECURITY: Track all runner activity for forensic analysis and intrusion detection +# Logs all file modifications in runner directory and all processes executed by runner + systemctl enable auditd systemctl restart auditd -# Add rules to monitor runner activity (idempotent check) +# Create audit rules to monitor runner activity (idempotent) if ! grep -q "runner_changes" /etc/audit/rules.d/runner.rules 2>/dev/null; then cat >>/etc/audit/rules.d/runner.rules <<'EOF' -# Monitor runner directory changes +# Log all file writes/attribute changes in runner directory -w /opt/actions-runner/ -p wa -k runner_changes - -# Monitor dangerous syscalls +# Log all program executions by runner user (track what workflows run) -a always,exit -F arch=b64 -S execve -F euid=github-runner -k runner_exec EOF fi augenrules --load || true -# ---- Download runner ---- +# ============================================================================== +# STEP 9: Download and extract GitHub Actions runner binaries +# ============================================================================== +# Downloads official runner release from GitHub. Runs as 'github-runner' user +# to ensure all files are owned by the unprivileged account. + if [ ! -f /opt/actions-runner/bin/Runner.Listener ]; then -su - github-runner -c "cd /opt/actions-runner && \ - curl -fsSL -o actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VER}.tar.gz \ - https://github.com/actions/runner/releases/download/v${RUNNER_VER}/actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VER}.tar.gz && \ - tar xzf actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VER}.tar.gz" + echo "[INFO] Downloading GitHub Actions runner version $RUNNER_VER" + ARCHIVE="actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VER}.tar.gz" + su - github-runner -c "cd /opt/actions-runner && \ + curl -fsSL -o '$ARCHIVE' \ + 'https://github.com/actions/runner/releases/download/v${RUNNER_VER}/'$ARCHIVE && \ + tar xzf '$ARCHIVE'" + echo "[INFO] Runner binaries extracted successfully" fi -# ---- Register runner with labels ---- +# Pre-create directories that runner needs (prevents permission errors during first run) +mkdir -p /opt/actions-runner/_diag /opt/actions-runner/_work +chown -R github-runner:github-runner /opt/actions-runner +chmod -R u+w /opt/actions-runner + +# ============================================================================== +# STEP 10: Register runner with GitHub repository +# ============================================================================== +# Registers the runner with GitHub using a registration token. The .runner file +# indicates successful registration. This step is idempotent. +# SECURITY: Token is single-use and short-lived (keep it secret) + if [ ! -f /opt/actions-runner/.runner ]; then -su - github-runner -c "cd /opt/actions-runner && \ - ./config.sh --url '${GH_REPO_URL}' \ - --token '${GH_RUNNER_TOKEN}' \ - --name '${GH_RUNNER_NAME}' \ - --labels '${GH_RUNNER_LABELS}' \ - --work _work \ - --unattended" + echo "[INFO] Registering runner with GitHub" + cd /opt/actions-runner + su github-runner -c "./config.sh --url '${GH_REPO_URL}' \ + --token '${GH_RUNNER_TOKEN}' \ + --name '${GH_RUNNER_NAME}' \ + --labels '${GH_RUNNER_LABELS}' \ + --work _work \ + --unattended" + echo "[INFO] Runner registration completed" fi -# ---- Install as a service with resource limits ---- +# ============================================================================== +# STEP 11: Install runner as a systemd service with strict resource limits +# ============================================================================== +# SECURITY: Systemd sandboxing and resource limits defend against malicious code +# - NoNewPrivileges: prevents setuid/setgid privilege escalation +# - PrivateTmp: isolates /tmp from other processes +# - ProtectSystem: makes system directories read-only +# - ProtectHome: blocks access to user home directories +# - LimitNPROC/TasksMax: prevents fork bombs +# - LimitFSIZE: prevents disk flooding attacks +# - CPUQuota/MemoryMax: prevents resource exhaustion +# - CapabilityBoundingSet: drops all capabilities (no raw sockets, etc.) if ! systemctl is-active --quiet actions.runner.*; then /opt/actions-runner/svc.sh install github-runner || true fi -# Apply strict systemd limits (idempotent) +# Create systemd drop-in directory and apply security limits mkdir -p /etc/systemd/system/actions.runner.*.service.d/ cat >/etc/systemd/system/actions.runner.*.service.d/limits.conf <<'EOF' [Service] -# Security hardening +# Process isolation and sandboxing NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict ProtectHome=yes -ReadWritePaths=/opt/actions-runner/_work +ReadWritePaths=/opt/actions-runner/_work /opt/actions-runner/_diag -# Resource limits (prevent fork bombs, disk flooding, CPU exhaustion) +# Resource limits to prevent DoS attacks LimitCORE=0 LimitNOFILE=4096 LimitNPROC=512 @@ -305,44 +384,102 @@ CPUQuota=150% MemoryMax=2G TasksMax=256 -# Prevent runaway processes +# Execution limits TimeoutStopSec=30 -# Network isolation (allow only outbound) +# Network restrictions (outbound only, plus CAN for SIL) RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_CAN -# Prevent privilege escalation +# Remove all capabilities (prevents raw socket access, etc.) CapabilityBoundingSet= AmbientCapabilities= +EOF -# Additional protections against malicious code -ProtectKernelModules=yes -ProtectKernelTunables=yes -ProtectControlGroups=yes -ReadOnlyPaths=/opt/actions-runner/bin -SystemCallFilter=@system-service -SystemCallFilter=~@privileged @resources +systemctl daemon-reload -# Prevent access to sensitive files -InaccessiblePaths=/root /home /etc/shadow /etc/sudoers /boot +# Ensure service is running +SERVICE_NAME=$(systemctl list-units --full --all --no-legend 'actions.runner.*' | awk '{print $1}' | head -n1) +if [ -n "$SERVICE_NAME" ]; then + if ! systemctl is-active --quiet "$SERVICE_NAME"; then + echo "[*] Starting runner service: $SERVICE_NAME" + systemctl start "$SERVICE_NAME" || true + else + echo "[*] Runner service already active: $SERVICE_NAME" + fi +fi + +# ============================================================================== +# STEP 12: Apply AppArmor Mandatory Access Control profile +# ============================================================================== +# SECURITY: AppArmor provides an additional layer of defense by restricting +# what the runner process can access, even if it's compromised. +# - Allows: runner directory, work directory, network access, CAN interfaces +# - Denies: sensitive system files, other users' data, system directories -# Disk quota for work directory (prevent disk flooding) -# Note: Requires quotas enabled on filesystem +if [ ! -f /etc/apparmor.d/opt.actions-runner.Runner.Listener ]; then +cat >/etc/apparmor.d/opt.actions-runner.Runner.Listener <<'EOF' +#include +/opt/actions-runner/bin/Runner.Listener { + #include + #include + + # Allow runner to execute its own binaries + /opt/actions-runner/ r, + /opt/actions-runner/** r, + /opt/actions-runner/bin/* ix, + + # Allow work and diagnostic directories (where jobs execute) + /opt/actions-runner/_work/** rw, + /opt/actions-runner/_diag/** rw, + /opt/actions-runner/.credentials rw, + /opt/actions-runner/.runner rw, + /opt/actions-runner/.env rw, + /opt/actions-runner/.path rw, + + # Network access for GitHub API communication + network inet stream, + network inet6 stream, + + # SocketCAN access for SIL testing + network can raw, + /sys/class/net/vcan0/** r, + /sys/class/net/can0/** r, + /dev/ttyUSB* rw, + + # Required system access for runner operation + /proc/sys/kernel/random/uuid r, + /sys/devices/virtual/net/vcan0/** r, + + # Explicitly deny access to sensitive files + deny /etc/shadow r, + deny /etc/sudoers* r, + deny /root/** rw, + deny /home/*/.ssh/** rw, + + # Prevent writes to critical system directories + deny /boot/** w, + deny /etc/** w, + deny /sys/** w, +} EOF +fi -systemctl daemon-reload +# Load the AppArmor profile into the kernel +apparmor_parser -r /etc/apparmor.d/opt.actions-runner.Runner.Listener || true -# Start or restart service -if systemctl is-active --quiet actions.runner.*; then - systemctl restart actions.runner.* || true -else - /opt/actions-runner/svc.sh start || true -fi +# ============================================================================== +# STEP 13: Configure automatic cleanup of old work directories +# ============================================================================== +# SECURITY: Prevents disk space exhaustion from accumulated build artifacts +# Weekly cron job (Sundays at 03:15) removes work directories older than 7 days -# ---- Cleanup job for old work directories ---- -if ! sudo -u github-runner crontab -l 2>/dev/null | grep -q "_work"; then - (sudo -u github-runner crontab -l 2>/dev/null; echo '15 3 * * 0 find /opt/actions-runner/_work -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \;') | sudo -u github-runner crontab - +if ! crontab -l 2>/dev/null | grep -q "_work"; then + (crontab -l 2>/dev/null; echo '15 3 * * 0 find /opt/actions-runner/_work -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \;') | crontab - fi -echo "Runner '${GH_RUNNER_NAME}' provisioned with security hardening." -echo "Verify in GitHub → Settings → Actions → Runners." \ No newline at end of file +# ============================================================================== +# PROVISIONING COMPLETE +# ============================================================================== +echo "[SUCCESS] Runner '$GH_RUNNER_NAME' provisioned with security hardening" +echo "[INFO] CAN Type: $CAN_TYPE" +echo "[INFO] Verify runner status in GitHub: Settings -> Actions -> Runners" From f93f34fdea5c736e44b7151f8819e3714f8d2270 Mon Sep 17 00:00:00 2001 From: LukhsaanE Date: Sun, 16 Nov 2025 16:42:38 -0500 Subject: [PATCH 4/4] Add current workflow and dummy script for SIL --- .github/workflows/sil-check.yml | 40 ++++++++++++++++++++++++++++++ scripts/provision/github_runner.sh | 2 +- scripts/provision/run_sil_dummy.sh | 9 +++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sil-check.yml create mode 100644 scripts/provision/run_sil_dummy.sh diff --git a/.github/workflows/sil-check.yml b/.github/workflows/sil-check.yml new file mode 100644 index 000000000..44666ca1b --- /dev/null +++ b/.github/workflows/sil-check.yml @@ -0,0 +1,40 @@ +name: SIL check + +on: + pull_request: + types: [opened, synchronize, reopened, labeled] + workflow_dispatch: + +jobs: + sil: + # Only run when: + # - Manually triggered (workflow_dispatch), OR + # - PR has the label "ci:run-sil" + if: ${{ + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && + contains(github.event.pull_request.labels.*.name, 'ci:run-sil')) + }} + + + # Target only your hardened runner + runs-on: [self-hosted, linux, sil, socketcan] + + # Minimal required permissions + permissions: + contents: read + + # Prevent runaway SIL jobs + timeout-minutes: 2 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run dummy SIL (0 = pass, 1 = fail) # Update to actual SIL check once ready + run: | + # Per-step resource guardrails + ulimit -c 0 # disable core dumps + ulimit -n 65536 # bump file descriptor limit if needed + + ./scripts/run_sil_dummy.sh diff --git a/scripts/provision/github_runner.sh b/scripts/provision/github_runner.sh index af2d41481..e8f1736f8 100644 --- a/scripts/provision/github_runner.sh +++ b/scripts/provision/github_runner.sh @@ -482,4 +482,4 @@ fi # ============================================================================== echo "[SUCCESS] Runner '$GH_RUNNER_NAME' provisioned with security hardening" echo "[INFO] CAN Type: $CAN_TYPE" -echo "[INFO] Verify runner status in GitHub: Settings -> Actions -> Runners" +echo "[INFO] Verify runner status in GitHub: Settings -> Actions -> Runners" \ No newline at end of file diff --git a/scripts/provision/run_sil_dummy.sh b/scripts/provision/run_sil_dummy.sh new file mode 100644 index 000000000..04eebe11c --- /dev/null +++ b/scripts/provision/run_sil_dummy.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +# For demo: pass +echo "[dummy-sil] OK" + +# Simulate Failing SIL: exit 1 +# Simulate Passing SIL: exit 0 +exit 0 \ No newline at end of file