diff --git a/e2e/localdns/validate-localdns-exporter-metrics.sh b/e2e/localdns/validate-localdns-exporter-metrics.sh index 83baeb09b30..65f2cd83a53 100644 --- a/e2e/localdns/validate-localdns-exporter-metrics.sh +++ b/e2e/localdns/validate-localdns-exporter-metrics.sh @@ -316,293 +316,4 @@ else fi echo "" -# Security hardening validation -echo "=== Security Hardening Validation ===" -echo "" - -# We need a live instance to query runtime-effective security properties via systemctl show. -# localdns-exporter@.service is a template unit — systemd only loads properties for instantiated -# units. The workers are Accept=yes (per-connection) and exit immediately after responding. -# To keep an instance alive for inspection, we hold an open connection that sends no HTTP request, -# so the worker spawns and blocks waiting on stdin. - -# Step 11: Spawn a persistent worker instance by holding a connection open -echo "11. Spawning a persistent worker instance for security inspection..." -# Hold a raw TCP connection open so the socket-activated exporter worker stays alive -# for security inspection. nc is available on all AKS VHD distros (Ubuntu, AzureLinux, Flatcar). -# Note: do NOT use bash /dev/tcp here — Flatcar compiles bash without --enable-net-redirections. -HOLD_HOST="${LISTEN_ADDR%%:*}" -HOLD_PORT="${LISTEN_ADDR##*:}" -( sleep 120 | nc "${HOLD_HOST}" "${HOLD_PORT}" ) &>/dev/null & -NC_PID=$! -sleep 2 - -# Ensure cleanup of the held connection on exit (normal or error) -cleanup_nc() { - kill "$NC_PID" 2>/dev/null || true - wait "$NC_PID" 2>/dev/null || true -} -trap cleanup_nc EXIT - -echo " ✓ Connection held open (PID: $NC_PID)" -echo "" - -# Step 12: Find the active instance -echo "12. Finding active localdns-exporter instance..." -ACTIVE_INSTANCES=$(systemctl list-units --all 'localdns-exporter@*.service' --no-pager --no-legend --plain | awk '{print $1}' || true) -if [ -z "$ACTIVE_INSTANCES" ]; then - echo " ⚠️ No active instances found (socket activation may be delayed), retrying..." - sleep 3 - ACTIVE_INSTANCES=$(systemctl list-units --all 'localdns-exporter@*.service' --no-pager --no-legend --plain | awk '{print $1}' || true) -fi - -if [ -z "$ACTIVE_INSTANCES" ]; then - echo " ❌ ERROR: No localdns-exporter instances found after retry" - exit 1 -fi -INSTANCE_NAME=$(echo "$ACTIVE_INSTANCES" | head -n 1) -echo " ✓ Found instance: $INSTANCE_NAME" -echo "" - -# Step 13: Verify all 16 systemd security directives via systemctl show on the live instance. -# This checks the runtime-effective values — what systemd actually enforces — not just what's -# in the unit file. This catches drop-in overrides, syntax errors, and unsupported directives. -echo "13. Verifying all 16 systemd security directives on live instance..." -echo "" - -SECURITY_PROPS_1=$(systemctl show "$INSTANCE_NAME" \ - --property=DynamicUser,PrivateTmp,ProtectSystem,ProtectHome,ReadOnlyPaths,NoNewPrivileges \ - 2>/dev/null || true) -SECURITY_PROPS_2=$(systemctl show "$INSTANCE_NAME" \ - --property=ProtectKernelTunables,ProtectKernelModules,ProtectControlGroups,RestrictAddressFamilies \ - 2>/dev/null || true) -SECURITY_PROPS_3=$(systemctl show "$INSTANCE_NAME" \ - --property=RestrictNamespaces,LockPersonality,RestrictRealtime,RestrictSUIDSGID,RemoveIPC,PrivateMounts \ - 2>/dev/null || true) - -SECURITY_PROPS="$SECURITY_PROPS_1 -$SECURITY_PROPS_2 -$SECURITY_PROPS_3" - -echo " Retrieved security properties:" -echo "$SECURITY_PROPS" | sed 's/^/ /' -echo "" - -FAILED_CHECKS=0 - -if ! echo "$SECURITY_PROPS" | grep -q "^DynamicUser=yes$"; then - echo " ❌ ERROR: DynamicUser not enabled" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ DynamicUser=yes" -fi - -if ! echo "$SECURITY_PROPS" | grep -q "^PrivateTmp=yes$"; then - echo " ❌ ERROR: PrivateTmp not enabled" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ PrivateTmp=yes" -fi - -if ! echo "$SECURITY_PROPS" | grep -q "^ProtectSystem=strict$"; then - echo " ❌ ERROR: ProtectSystem not strict" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ ProtectSystem=strict" -fi - -if ! echo "$SECURITY_PROPS" | grep -q "^ProtectHome=yes$"; then - echo " ❌ ERROR: ProtectHome not enabled" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ ProtectHome=yes" -fi - -if ! echo "$SECURITY_PROPS" | grep -qE "^ReadOnlyPaths=/$|^ReadOnlyPaths=/ "; then - echo " ❌ ERROR: ReadOnlyPaths not set to /" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ ReadOnlyPaths=/" -fi - -if ! echo "$SECURITY_PROPS" | grep -q "^NoNewPrivileges=yes$"; then - echo " ❌ ERROR: NoNewPrivileges not enabled" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ NoNewPrivileges=yes" -fi - -if ! echo "$SECURITY_PROPS" | grep -q "^ProtectKernelTunables=yes$"; then - echo " ❌ ERROR: ProtectKernelTunables not enabled" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ ProtectKernelTunables=yes" -fi - -if ! echo "$SECURITY_PROPS" | grep -q "^ProtectKernelModules=yes$"; then - echo " ❌ ERROR: ProtectKernelModules not enabled" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ ProtectKernelModules=yes" -fi - -if ! echo "$SECURITY_PROPS" | grep -q "^ProtectControlGroups=yes$"; then - echo " ❌ ERROR: ProtectControlGroups not enabled" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ ProtectControlGroups=yes" -fi - -if ! echo "$SECURITY_PROPS" | grep -qE "RestrictAddressFamilies=.*AF_UNIX"; then - echo " ❌ ERROR: RestrictAddressFamilies does not include AF_UNIX" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - # AF_INET and AF_INET6 are expected alongside AF_UNIX for socket-activation compatibility - # with future systemd versions (seccomp filter must allow the inherited socket's address family) - if ! echo "$SECURITY_PROPS" | grep -qE "RestrictAddressFamilies=.*AF_INET"; then - echo " ❌ ERROR: RestrictAddressFamilies does not include AF_INET (required for socket activation)" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) - else - echo " ✓ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6" - fi -fi - -if ! echo "$SECURITY_PROPS" | grep -q "^RestrictNamespaces=yes$"; then - echo " ❌ ERROR: RestrictNamespaces not enabled" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ RestrictNamespaces=yes" -fi - -if ! echo "$SECURITY_PROPS" | grep -q "^LockPersonality=yes$"; then - echo " ❌ ERROR: LockPersonality not enabled" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ LockPersonality=yes" -fi - -if ! echo "$SECURITY_PROPS" | grep -q "^RestrictRealtime=yes$"; then - echo " ❌ ERROR: RestrictRealtime not enabled" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ RestrictRealtime=yes" -fi - -if ! echo "$SECURITY_PROPS" | grep -q "^RestrictSUIDSGID=yes$"; then - echo " ❌ ERROR: RestrictSUIDSGID not enabled" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ RestrictSUIDSGID=yes" -fi - -if ! echo "$SECURITY_PROPS" | grep -q "^RemoveIPC=yes$"; then - echo " ❌ ERROR: RemoveIPC not enabled" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ RemoveIPC=yes" -fi - -if ! echo "$SECURITY_PROPS" | grep -q "^PrivateMounts=yes$"; then - echo " ❌ ERROR: PrivateMounts not enabled" - FAILED_CHECKS=$((FAILED_CHECKS + 1)) -else - echo " ✓ PrivateMounts=yes" -fi - -echo "" -if [ "$FAILED_CHECKS" -gt 0 ]; then - echo "=== ❌ Security Configuration Validation FAILED ===" - echo "$FAILED_CHECKS out of 16 security directives are not properly configured" - exit 1 -fi -echo "✓ All 16 security directives are properly configured" -echo "" - -# Step 14: Get PID of the instance for runtime enforcement checks -echo "14. Getting PID of instance..." -INSTANCE_PID=$(systemctl show "$INSTANCE_NAME" --property=MainPID --value 2>/dev/null || echo "0") - -if [ "$INSTANCE_PID" = "0" ] || [ -z "$INSTANCE_PID" ]; then - echo " ⚠️ Instance PID not found, skipping process-level checks" -else - echo " ✓ Instance PID: $INSTANCE_PID" - echo "" - - # Step 15: Verify not running as root (DynamicUser runtime enforcement) - echo "15. Verifying DynamicUser runtime enforcement (not running as root)..." - INSTANCE_USER=$(ps -o user= -p "$INSTANCE_PID" 2>/dev/null || echo "unknown") - if [ "$INSTANCE_USER" = "root" ]; then - echo " ❌ ERROR: Instance running as root (DynamicUser not enforced at runtime)" - exit 1 - fi - echo " ✓ Running as dynamic user: $INSTANCE_USER" - echo "" - - # Step 16: Verify no network sockets (RestrictAddressFamilies runtime enforcement) - echo "16. Verifying RestrictAddressFamilies runtime enforcement (no network sockets)..." - - # Use /proc filesystem for portability (works on all distros without lsof) - # Note: Socket-activated services inherit the accepted connection as stdin/stdout (fd 0/1). - # This inherited socket is AF_INET but is expected and allowed. We only care about - # NEW sockets the service creates, not the inherited activation socket. - NETWORK_SOCKETS=0 - INHERITED_SOCKET_INODE="" - - if [ -d "/proc/$INSTANCE_PID/fd" ]; then - # Find the stdin socket inode (the inherited activation socket) - if [ -L "/proc/$INSTANCE_PID/fd/0" ]; then - STDIN_TARGET=$(readlink "/proc/$INSTANCE_PID/fd/0" 2>/dev/null || echo "") - INHERITED_SOCKET_INODE=$(echo "$STDIN_TARGET" | sed -n 's/^socket:\[\([0-9]*\)\]$/\1/p') - fi - - # Iterate through file descriptors to find sockets - for fd in /proc/"$INSTANCE_PID"/fd/*; do - if [ -L "$fd" ]; then - FD_TARGET=$(readlink "$fd" 2>/dev/null || echo "") - SOCKET_INODE=$(echo "$FD_TARGET" | sed -n 's/^socket:\[\([0-9]*\)\]$/\1/p') - if [ -n "$SOCKET_INODE" ]; then - - # Skip the inherited stdin/stdout socket from socket activation - if [ -n "$INHERITED_SOCKET_INODE" ] && [ "$SOCKET_INODE" = "$INHERITED_SOCKET_INODE" ]; then - continue - fi - - # Use ss to check if this socket is TCP/UDP (network socket) - if ss -tupn 2>/dev/null | grep -q "inode:$SOCKET_INODE"; then - NETWORK_SOCKETS=$((NETWORK_SOCKETS + 1)) - echo " Found unexpected network socket: inode=$SOCKET_INODE" - fi - fi - fi - done - else - echo " ⚠️ WARNING: Cannot access /proc/$INSTANCE_PID/fd, skipping socket inspection" - fi - - if [ "$NETWORK_SOCKETS" != "0" ]; then - echo " ❌ ERROR: Instance has $NETWORK_SOCKETS unexpected network socket(s) (RestrictAddressFamilies not enforced)" - exit 1 - fi - echo " ✓ No unexpected network sockets (AF_UNIX only, restriction enforced)" - echo "" - - # Step 17: Verify namespace isolation (RestrictNamespaces runtime enforcement) - echo "17. Verifying namespace isolation..." - if [ -d "/proc/$INSTANCE_PID/ns" ]; then - NS_COUNT=$(find /proc/"$INSTANCE_PID"/ns/ -mindepth 1 -maxdepth 1 2>/dev/null | wc -l) - if [ "$NS_COUNT" -lt 5 ]; then - echo " ⚠️ WARNING: Only $NS_COUNT namespaces (expected 5+ for proper isolation)" - else - echo " ✓ Process has $NS_COUNT namespaces (properly isolated)" - fi - else - echo " ⚠️ Cannot verify namespaces (proc not accessible)" - fi - echo "" -fi - -echo "=== ✓ Security Hardening Validation Passed ===" -echo "Configuration: All 16 systemd security directives verified on live instance" -if [ -n "${INSTANCE_PID:-}" ] && [ "${INSTANCE_PID:-0}" != "0" ]; then - echo "Runtime: DynamicUser, RestrictAddressFamilies, and namespace isolation enforced" -fi +echo "=== ✓ All localdns exporter functional validations passed ===" diff --git a/spec/parts/linux/cloud-init/artifacts/localdns_exporter_spec.sh b/spec/parts/linux/cloud-init/artifacts/localdns_exporter_spec.sh index 48f99e18ce9..a86219af00a 100644 --- a/spec/parts/linux/cloud-init/artifacts/localdns_exporter_spec.sh +++ b/spec/parts/linux/cloud-init/artifacts/localdns_exporter_spec.sh @@ -1,5 +1,89 @@ #!/bin/bash +Describe 'localdns-exporter@.service security hardening' + UNIT_FILE="./parts/linux/cloud-init/artifacts/localdns-exporter@.service" + + It 'should have DynamicUser=yes' + When run grep -q "^DynamicUser=yes$" "$UNIT_FILE" + The status should be success + End + + It 'should have PrivateTmp=yes' + When run grep -q "^PrivateTmp=yes$" "$UNIT_FILE" + The status should be success + End + + It 'should have ProtectSystem=strict' + When run grep -q "^ProtectSystem=strict$" "$UNIT_FILE" + The status should be success + End + + It 'should have ProtectHome=yes' + When run grep -q "^ProtectHome=yes$" "$UNIT_FILE" + The status should be success + End + + It 'should have ReadOnlyPaths=/' + When run grep -q "^ReadOnlyPaths=/$" "$UNIT_FILE" + The status should be success + End + + It 'should have NoNewPrivileges=yes' + When run grep -q "^NoNewPrivileges=yes$" "$UNIT_FILE" + The status should be success + End + + It 'should have ProtectKernelTunables=yes' + When run grep -q "^ProtectKernelTunables=yes$" "$UNIT_FILE" + The status should be success + End + + It 'should have ProtectKernelModules=yes' + When run grep -q "^ProtectKernelModules=yes$" "$UNIT_FILE" + The status should be success + End + + It 'should have ProtectControlGroups=yes' + When run grep -q "^ProtectControlGroups=yes$" "$UNIT_FILE" + The status should be success + End + + It 'should have RestrictAddressFamilies with AF_UNIX AF_INET AF_INET6' + When run grep -q "^RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6$" "$UNIT_FILE" + The status should be success + End + + It 'should have RestrictNamespaces=yes' + When run grep -q "^RestrictNamespaces=yes$" "$UNIT_FILE" + The status should be success + End + + It 'should have LockPersonality=yes' + When run grep -q "^LockPersonality=yes$" "$UNIT_FILE" + The status should be success + End + + It 'should have RestrictRealtime=yes' + When run grep -q "^RestrictRealtime=yes$" "$UNIT_FILE" + The status should be success + End + + It 'should have RestrictSUIDSGID=yes' + When run grep -q "^RestrictSUIDSGID=yes$" "$UNIT_FILE" + The status should be success + End + + It 'should have RemoveIPC=yes' + When run grep -q "^RemoveIPC=yes$" "$UNIT_FILE" + The status should be success + End + + It 'should have PrivateMounts=yes' + When run grep -q "^PrivateMounts=yes$" "$UNIT_FILE" + The status should be success + End +End + Describe 'localdns_exporter.sh HTTP request routing' SCRIPT_PATH="./parts/linux/cloud-init/artifacts/localdns_exporter.sh"