Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 1 addition & 290 deletions e2e/localdns/validate-localdns-exporter-metrics.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ==="
84 changes: 84 additions & 0 deletions spec/parts/linux/cloud-init/artifacts/localdns_exporter_spec.sh
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
Loading