feat: add security hardening, AppArmor, and ARM64 Raspberry Pi support (Phase 6)
Some checks failed
CI / Go Tests (push) Has been cancelled
CI / Build Go Binaries (amd64, linux, linux-amd64) (push) Has been cancelled
CI / Build Go Binaries (arm64, linux, linux-arm64) (push) Has been cancelled
CI / Shellcheck (push) Has been cancelled

Security hardening: bind kubeconfig server to localhost, mount hardening
(noexec/nosuid/nodev on tmpfs), sysctl network hardening, kernel module
loading lock after boot, SHA256 checksum verification for downloads,
kernel AppArmor + Audit support, complain-mode AppArmor profiles for
containerd and kubelet, and security integration test.

ARM64 Raspberry Pi support: piCore64 base extraction, RPi kernel build
from raspberrypi/linux fork, RPi firmware fetch, SD card image with 4-
partition GPT and tryboot A/B mechanism, BootEnv Go interface abstracting
GRUB vs RPi boot environments, architecture-aware build scripts, QEMU
aarch64 dev VM and boot test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 13:08:17 -06:00
parent 7abf0e0c04
commit efc7f80b65
38 changed files with 2512 additions and 96 deletions

View File

@@ -64,6 +64,7 @@ export KUBESOLO_CLOUDINIT=""
export KUBESOLO_EXTRA_FLAGS=""
export KUBESOLO_PORTAINER_EDGE_ID=""
export KUBESOLO_PORTAINER_EDGE_KEY=""
export KUBESOLO_NOAPPARMOR=""
# --- Logging ---
log() {

View File

@@ -12,10 +12,10 @@ if ! mountpoint -q /dev 2>/dev/null; then
mount -t devtmpfs devtmpfs /dev 2>/dev/null || mount -t tmpfs tmpfs /dev
fi
if ! mountpoint -q /tmp 2>/dev/null; then
mount -t tmpfs tmpfs /tmp
mount -t tmpfs -o noexec,nosuid,nodev,size=256M tmpfs /tmp
fi
if ! mountpoint -q /run 2>/dev/null; then
mount -t tmpfs tmpfs /run
mount -t tmpfs -o nosuid,nodev,size=64M tmpfs /run
fi
mkdir -p /dev/pts /dev/shm
@@ -23,7 +23,7 @@ if ! mountpoint -q /dev/pts 2>/dev/null; then
mount -t devpts devpts /dev/pts
fi
if ! mountpoint -q /dev/shm 2>/dev/null; then
mount -t tmpfs tmpfs /dev/shm
mount -t tmpfs -o noexec,nosuid,nodev,size=64M tmpfs /dev/shm
fi
# Ensure essential device nodes exist (devtmpfs may be incomplete after switch_root)

View File

@@ -11,9 +11,14 @@ for arg in $(cat /proc/cmdline); do
kubesolo.flags=*) KUBESOLO_EXTRA_FLAGS="${arg#kubesolo.flags=}" ;;
kubesolo.edge_id=*) KUBESOLO_PORTAINER_EDGE_ID="${arg#kubesolo.edge_id=}" ;;
kubesolo.edge_key=*) KUBESOLO_PORTAINER_EDGE_KEY="${arg#kubesolo.edge_key=}" ;;
kubesolo.nomodlock) KUBESOLO_NOMODLOCK=1 ;;
kubesolo.noapparmor) KUBESOLO_NOAPPARMOR=1 ;;
esac
done
export KUBESOLO_NOMODLOCK
export KUBESOLO_NOAPPARMOR
if [ -z "$KUBESOLO_DATA_DEV" ] && [ "$KUBESOLO_NOPERSIST" != "1" ]; then
log_warn "No kubesolo.data= specified and kubesolo.nopersist not set"
log_warn "Attempting auto-detection of data partition (label: KSOLODATA)"

View File

@@ -38,13 +38,13 @@ fi
# Mount data partition (format on first boot if unformatted)
mkdir -p "$DATA_MOUNT"
if ! mount -t ext4 -o noatime "$KUBESOLO_DATA_DEV" "$DATA_MOUNT" 2>/dev/null; then
if ! mount -t ext4 -o noatime,nosuid,nodev "$KUBESOLO_DATA_DEV" "$DATA_MOUNT" 2>/dev/null; then
log "Formatting $KUBESOLO_DATA_DEV as ext4 (first boot)"
mkfs.ext4 -q -L KSOLODATA "$KUBESOLO_DATA_DEV" || {
log_err "Failed to format $KUBESOLO_DATA_DEV"
return 1
}
mount -t ext4 -o noatime "$KUBESOLO_DATA_DEV" "$DATA_MOUNT" || {
mount -t ext4 -o noatime,nosuid,nodev "$KUBESOLO_DATA_DEV" "$DATA_MOUNT" || {
log_err "Failed to mount $KUBESOLO_DATA_DEV after format"
return 1
}

47
init/lib/35-apparmor.sh Normal file
View File

@@ -0,0 +1,47 @@
#!/bin/sh
# 35-apparmor.sh — Load AppArmor LSM profiles
# Check for opt-out boot parameter
if [ "$KUBESOLO_NOAPPARMOR" = "1" ]; then
log "AppArmor disabled via kubesolo.noapparmor boot parameter"
return 0
fi
# Mount securityfs if not already mounted
if ! mountpoint -q /sys/kernel/security 2>/dev/null; then
mount -t securityfs securityfs /sys/kernel/security 2>/dev/null || true
fi
# Check if AppArmor is available in the kernel
if [ ! -d /sys/kernel/security/apparmor ]; then
log_warn "AppArmor not available in kernel — skipping profile loading"
return 0
fi
# Check for apparmor_parser
if ! command -v apparmor_parser >/dev/null 2>&1; then
log_warn "apparmor_parser not found — skipping profile loading"
return 0
fi
# Load all profiles from /etc/apparmor.d/
PROFILE_DIR="/etc/apparmor.d"
if [ ! -d "$PROFILE_DIR" ]; then
log_warn "No AppArmor profiles directory ($PROFILE_DIR) — skipping"
return 0
fi
LOADED=0
FAILED=0
for profile in "$PROFILE_DIR"/*; do
[ -f "$profile" ] || continue
if apparmor_parser -r "$profile" 2>/dev/null; then
LOADED=$((LOADED + 1))
else
log_warn "Failed to load AppArmor profile: $(basename "$profile")"
FAILED=$((FAILED + 1))
fi
done
log_ok "AppArmor: loaded $LOADED profiles ($FAILED failed)"

View File

@@ -0,0 +1,20 @@
#!/bin/sh
# 85-security-lockdown.sh — Lock down kernel after all modules loaded
# Allow disabling via boot parameter for debugging
if [ "$KUBESOLO_NOMODLOCK" = "1" ]; then
log_warn "Module lock DISABLED (kubesolo.nomodlock)"
else
# Permanently prevent new kernel module loading (irreversible until reboot)
# All required modules must already be loaded by stage 30
if [ -f /proc/sys/kernel/modules_disabled ]; then
echo 1 > /proc/sys/kernel/modules_disabled 2>/dev/null && \
log_ok "Kernel module loading locked" || \
log_warn "Failed to lock kernel module loading"
fi
fi
# Safety net: enforce kernel information protection
# (also set via sysctl.d but enforce here in case sysctl.d was bypassed)
echo 2 > /proc/sys/kernel/kptr_restrict 2>/dev/null || true
echo 1 > /proc/sys/kernel/dmesg_restrict 2>/dev/null || true

View File

@@ -87,7 +87,7 @@ if [ -f "$KUBECONFIG_PATH" ]; then
# Serve kubeconfig via HTTP on port 8080 using BusyBox nc
(while true; do
printf "HTTP/1.1 200 OK\r\nContent-Type: text/yaml\r\nConnection: close\r\n\r\n" | cat - "$EXTERNAL_KC" | nc -l -p 8080 2>/dev/null
printf "HTTP/1.1 200 OK\r\nContent-Type: text/yaml\r\nConnection: close\r\n\r\n" | cat - "$EXTERNAL_KC" | nc -l -s 127.0.0.1 -p 8080 2>/dev/null
done) &
log_ok "Kubeconfig available via HTTP"