Scripts for verifying OpenRC isolation/containment, AppArmor enforcement, Protocol 7 daemon hardening, and overall system state. https://lainos.net
Find a file
2026-08-18 19:35:24 +02:00
lainos-security-status add test scripts 2026-07-26 12:44:31 -07:00
LICENSE Initial commit 2026-07-26 21:28:31 +02:00
openrc-security-status.sh Upload files to "/" 2026-08-18 19:35:24 +02:00
protocol7-core-security-status add test scripts 2026-07-26 12:32:33 -07:00
README.md Update README.md 2026-08-18 19:34:53 +02:00

🔍 Security Verification Suite

LainOS Layer 02 ships with three complementary security verification tools that work together to confirm your system's security posture at every level:

Tool Purpose Impact When to Run
lainos-security-status Read-only status dashboard None — safe anytime Daily checks, troubleshooting
openrc-security-status Isolation stack verification None — read-only After configuration changes
protocol7-core-security-status Adversarial test suite Brief service interruption After security changes, before releases

Together, they move beyond "configuration review" to runtime verification — they tell you what the system is actually doing, not just what it's configured to do.


lainos-security-status

A read-only status dashboard that reports current security state without attempting to break anything. Safe to run any time, including in production, with no side effects beyond reading /proc, dmesg, and calling aa-status/rc-service status.

Covers:

  • Installed package versions (lainos-apparmor, protocol7-core, apparmor)
  • AppArmor kernel module state, profile counts (enforce/complain/unconfined)
  • Which profiles are currently enforced against running processes
  • OpenRC service status for core daemons and the DNS/networking stack
  • AppArmor denial count since boot (via dmesg), with the most recent denials summarized
  • Basic connectivity and NTP sync state
  • Per-daemon hardening state for Protocol 7 daemons:
    • AppArmor enforcement
    • Capability bounding set
    • Seccomp filter mode
    • Privilege drop (running as nobody)
  • Boot-time enforce symlink presence for each profile
lainos-security-status
📋 Example output (click to expand)
doas (lain@navi) password:
╔══════════════════════════════════════════╗
║   LainOS Layer 02 — Security Status      ║
╚══════════════════════════════════════════╝
  Host:    navi
  Kernel:  7.1.4-arch1-1
  Uptime:  up 10 hours, 54 minutes

--- Layer 02 Packages ---
    lainos-apparmor 1.2.1-29
    protocol7-core 5.5.3-27
    apparmor 4.1.7-1

--- AppArmor Global State ---
  ✓ AppArmor kernel module active
    Total profiles:   24
    Enforce mode:     24
    Complain mode:    0
    Unconfined procs: 31

--- Enforced Profiles ---
    /usr/bin/chronyd
    /usr/bin/dhcpcd
    /usr/bin/dnsmasq
    /usr/bin/gpg
    /usr/bin/gpg-agent
    /usr/bin/keepassxc
    /usr/bin/mpv
    /usr/bin/nft
    /usr/bin/pipewire
    /usr/bin/pipewire-pulse
    /usr/bin/snowflake-pt-client
    /usr/bin/ssh
    /usr/bin/stubby
    /usr/bin/syslog-ng
    /usr/bin/tor
    /usr/bin/unbound
    /usr/bin/vlc
    /usr/bin/wireplumber
    /usr/lib/iwd/iwd
    /usr/lib/librewolf/librewolf
    /usr/libexec/lainos/lainos-dbus-bridge
    /usr/libexec/lainos/lainos-init
    /usr/libexec/lainos/lainos-notifyd
  ✓ /usr/bin/chronyd (8405)
  ✓ /usr/bin/dhcpcd (8237)
  ✓ /usr/bin/dnsmasq (9945)
  ✓ /usr/bin/unbound (9908)
  ✓ /usr/libexec/lainos/lainos-dbus-bridge (16715)
  ✓ /usr/libexec/lainos/lainos-notifyd (16773)

--- OpenRC Services ---
  ✓ dhcpcd running
  ✓ dnsmasq running
  ✓ unbound running
  ✓ dnscrypt-proxy running
  ✓ iwd running
  ✓ chrony running
  ✓ syslog-ng running
  ✓ lainos-dbus-bridge running
  ✓ lainos-notifyd running

--- AppArmor Denials ---
  ✗ 0 denials since boot

--- Network & Time ---
  ✓ Internet connectivity OK
  ✓ NTP sync: Normal

--- Protocol 7 Daemon Hardening ---
  ✓ lainos-dbus-bridge (PID 16715) AppArmor enforced
  ✓ lainos-dbus-bridge capability bounding set empty
  ✓ lainos-dbus-bridge seccomp filter mode
  ✓ lainos-dbus-bridge running as nobody
  ✓ lainos-notifyd (PID 16773) AppArmor enforced
  ✓ lainos-notifyd capability bounding set empty
  ✓ lainos-notifyd seccomp filter mode
  ✓ lainos-notifyd running as nobody

--- Boot Security Chain ---
  ✓ lainos-dbus-bridge enforce symlink present
  ✓ lainos-notifyd enforce symlink present

========================================

openrc-security-status

A per-service containment layer verifier that checks every OpenRC service's isolation stack at runtime. Unlike configuration review, this reads the actual kernel state for each running service to confirm:

  • Mount namespace isolation (own mount namespace vs. host)
  • Network namespace isolation (with rc_network_access verification)
  • PID namespace isolation (with rc_unshare_pid verification)
  • Cgroup limits enforcement (memory.max, pids.max, PID confirmed in cgroup.procs)
  • Seccomp-bpf active status (filter mode, filter count, no_new_privs set)
  • Capability narrowing (CapEff and CapBnd read from /proc/[pid]/status)
  • AppArmor confinement (via aa-status PID mapping)

Verified services: dnsmasq, unbound, dnscrypt-proxy, tor, dhcpcd, iwd, sdwdate, chrony, acpid, syslog-ng

doas openrc-security-status
📋 Example output (click to expand)
=== lainOS OpenRC Isolation Stack — Containment Layer Verification ===

=== dnsmasq ===
  process               running as PID 4191 (comm=dnsmasq)
  namespace: mount      ISOLATED (own mount ns)
  namespace: network    shared with host (rc_network_access=YES, correct)
  namespace: pid        ISOLATED (own PID namespace)
  cgroup limits         ENFORCED (memory.max=67108864 pids.max=20, PID confirmed member)
  seccomp-bpf           ACTIVE (filter mode, 1 filter(s) loaded, no_new_privs set)
  Landlock              cannot verify directly (no /proc/status field exists for it);
                        inferred from successful sandboxed operation below
  capabilities          NARROWED (CapEff=0x0000000000002400 CapBnd=0x00000000000024c3)
  AppArmor              CONFINED (/usr/bin/dnsmasq)

=== unbound ===
  process               running as PID 4124 (comm=unbound)
  namespace: mount      ISOLATED (own mount ns)
  namespace: network    shared with host (rc_network_access=YES, correct)
  namespace: pid        ISOLATED (own PID namespace)
  cgroup limits         ENFORCED (memory.max=268435456 pids.max=20, PID confirmed member)
  seccomp-bpf           ACTIVE (filter mode, 1 filter(s) loaded, no_new_privs set)
  capabilities          NARROWED (CapEff=0x0000000001000402 CapBnd=0x0000000001000402)
  AppArmor              CONFINED (/usr/bin/unbound)

=== iwd ===
  process               running as PID 2636 (comm=iwd)
  namespace: mount      NOT ISOLATED (expected — iwd bypasses bwrap entirely)
  namespace: network    NOT ISOLATED (expected — iwd bypasses bwrap entirely)
  namespace: pid        NOT ISOLATED (expected — iwd bypasses bwrap entirely)
  note: rfkill's write-mode device access is incompatible with any
        bwrap-created mount namespace on this kernel, confirmed via
        raw bwrap with zero other restrictions. Landlock, seccomp,
        capabilities, cgroups, and AppArmor remain fully enforced
        without it — see project notes.
  cgroup limits         ENFORCED (memory.max=max pids.max=max, PID confirmed member)
  seccomp-bpf           ACTIVE (filter mode, 1 filter(s) loaded, no_new_privs set)
  capabilities          NARROWED (CapEff=0x00000000000030c6 CapBnd=0x00000000000030c6)
  AppArmor              CONFINED (/usr/lib/iwd/iwd)

=== syslog-ng ===
  process               running as PID 1710 (comm=syslog-ng-main)
  namespace: mount      ISOLATED (own mount ns)
  namespace: network    ISOLATED (rc_network_access!=YES, correct)
  namespace: pid        ISOLATED (own PID namespace)
  cgroup limits         ENFORCED (memory.max=134217728 pids.max=10, PID confirmed member)
  seccomp-bpf           ACTIVE (filter mode, 1 filter(s) loaded, no_new_privs set)
  capabilities          NARROWED (CapEff=0x0000000000000000 CapBnd=0x0000000401203ddf)
  AppArmor              CONFINED (/usr/bin/syslog-ng)

=== Functional proof ===
  chrony                real NTP sync confirmed
  unbound               real DNS resolution confirmed
  syslog-ng             logger round-trip confirmed

=== Summary ===
All containment layers confirmed active on all services.

Why iwd Is Not Namespace-Isolated

iwd requires write access to /dev/rfkill to control WiFi radios. A bwrap-created mount namespace breaks this access on the current kernel. Instead of breaking WiFi functionality, iwd runs without namespace isolation while retaining all other containment layers:

  • Seccomp filtering
  • Capability dropping
  • Cgroup limits
  • AppArmor confinement

The script explicitly documents this exception so you know it's intentional, not a security gap.


protocol7-core-security-status

An adversarial test suite that actively attempts to break containment — exactly what a real attacker would try. Unlike the status scripts above, this one:

  • Attempts to escape the mount namespace
  • Tries reading blocked paths
  • Attempts cross-process ptracing
  • Tries writing to /proc/sys
  • Verifies each attempt is actually denied

⚠️ Note: This script restarts both Protocol 7 daemons at the end of every run (one test sends SIGTERM to lainos-notifyd as part of testing shutdown behavior, and a gdb ptrace attempt can leave lainos-dbus-bridge in a traced/stopped state). Expect a brief interruption to Protocol 7 services while it runs.

36 Tests Across 12 Categories

# Category What It Actually Proves
1 AppArmor enforcement Both daemons are confined, not just loaded
2 Privilege drop Both daemons run as nobody, not root
3 Capability drop CapBnd reads zero on both — no regainable capabilities
4 Seccomp Filter mode active (not just loaded, actually filtering)
5 Mount namespace /tmp, /home, /root are tmpfs overlays, not the real filesystem
5b Filesystem isolation effectiveness /home//root are genuinely empty inside the namespace even when the host has real data; /tmp is bidirectionally isolated
6 Path blocking (DAC baseline) /etc/shadow, /home, /root are inaccessible — not AppArmor-specific
6b AppArmor negative control /var/tmp write is denied even though it's world-writable (1777) — this is the actual proof of AppArmor enforcement
6c ptrace denial Neither daemon can PTRACE_ATTACH the other
7 Network No listening sockets; profile text contains no network inet/inet6 grant
8 Capability abuse /proc/sys write blocked
9 Denial log Zero new denials during this run
10 D-Bus The bridge actually responds to a real D-Bus introspection call
11 lainos-init whitelist Unsafe relative and nonexistent-absolute P7_CMD values are rejected
12 notifyd regressions Survives embedded-newline payload; exits promptly on SIGTERM

Why Tests 6 and 6b Are Both Present: Test 6 alone would keep passing even if the AppArmor profile were unloaded entirely, since plain filesystem permissions already block nobody from those specific paths. Test 6b uses a path that is world-writable at the permission layer — if it's still blocked, something beyond DAC has to be doing the blocking. That's the actual proof of enforcement.

doas protocol7-core-security-status
📋 Example output (click to expand)
==========================================
Protocol 7 AppArmor Adversarial Test Suite
==========================================

--- Layer 02 Packages ---
lainos-apparmor 1.2.1-29
protocol7-core 5.5.3-27
apparmor 4.1.7-1

Target PIDs:
  lainos-dbus-bridge: 2639
  lainos-notifyd:     1124

--- Test 1: AppArmor Enforcement ---
  [PASS] lainos-dbus-bridge in enforce mode
  [PASS] lainos-notifyd in enforce mode

--- Test 2: Privilege Drop ---
  [PASS] dbus-bridge as nobody
  [PASS] notifyd as nobody

--- Test 3: Capability Drop ---
  [PASS] lainos-dbus-bri CapBnd zero
  [PASS] lainos-notifyd CapBnd zero

--- Test 4: Seccomp ---
  [PASS] lainos-dbus-bri seccomp active
  [PASS] lainos-notifyd seccomp active

--- Test 5: Mount Namespace ---
  [PASS] lainos-dbus-bri /tmp tmpfs
  [PASS] lainos-dbus-bri /home tmpfs
  [PASS] lainos-dbus-bri /root tmpfs
  [PASS] lainos-notifyd /tmp tmpfs
  [PASS] lainos-notifyd /home tmpfs
  [PASS] lainos-notifyd /root tmpfs

--- Test 5b: Filesystem Isolation Effectiveness ---
  [PASS] /home is empty in daemon namespace (host has 1 entries)
  [PASS] /root is empty in daemon namespace
  [PASS] Daemon /tmp isolated from host
  [PASS] Host /tmp isolated from daemon

--- Test 6: Blocked Path Access (DAC baseline — not AppArmor-specific) ---
  [PASS] /etc/shadow blocked
  [PASS] /home write blocked
  [PASS] /root write blocked

--- Test 6b: AppArmor Negative Control ---
  [PASS] lainos-dbus-bri: /var/tmp write blocked by AppArmor (DAC alone would have allowed this)
  [PASS] lainos-notifyd: /var/tmp write blocked by AppArmor (DAC alone would have allowed this)

--- Test 6c: ptrace Denial ---
  [PASS] dbus-bridge cannot ptrace notifyd

--- Test 7: Network ---
  [PASS] dbus-bridge no network sockets
  [PASS] notifyd no network sockets
  [PASS] lainos-dbus-bridge profile text contains no inet/inet6 network rule
  [PASS] lainos-notifyd profile text contains no inet/inet6 network rule

--- Test 8: Capability Abuse ---
  [PASS] /proc/sys write blocked

--- Test 9: Denial Log ---
  [PASS] No current denials

--- Test 10: D-Bus ---
  [PASS] D-Bus responding

--- Test 11: lainos-init P7_CMD Whitelist ---
  [PASS] unsafe relative P7_CMD rejected
  [PASS] nonexistent absolute P7_CMD rejected
  [PASS] root session refused without P7_ALLOW_ROOT

--- Test 12: notifyd Behavioral Regressions ---
  [PASS] notifyd survived newline payload, socket functional
  [PASS] notifyd exited promptly on SIGTERM


==========================================
Test Summary: 36 passed, 0 failed
==========================================

Restarting Protocol 7 daemons...
 * Stopping lainos-dbus-bridge ...                                                                 [ ok ]
 * Starting lainos-dbus-bridge ...                                                                 [ ok ]
 * WARNING: you are stopping a sysinit service
 * Stopping lainos-notifyd ...
 * start-stop-daemon: no matching processes found                                                  [ ok ]
 * Starting lainos-notifyd ...                                                                     [ ok ]
All tests PASSED

Exit code: 0 if all tests pass, 1 if any fail.

Requires: gdb (for the ptrace test in 6c; skipped gracefully with a warning if not installed), nsenter, socat, dbus-send, pgrep. Both Protocol 7 daemons (lainos-dbus-bridge, lainos-notifyd) must be running before the suite starts, or it exits immediately with an error.


When to Use Which Tool

Scenario Tool Why
Routine check — you want to see if anything looks off lainos-security-status Fast, read-only, safe anywhere
You changed a service's isolation config openrc-security-status Verifies all containment layers are actually active
You updated an AppArmor profile protocol7-core-security-status Tries to break out — confirms the profile actually restricts
You changed Protocol 7 daemon code protocol7-core-security-status Tests the daemons' privilege model and confinement
Before a release protocol7-core-security-status Full adversarial test suite proves security holds
You want actual proof, not configuration review protocol7-core-security-status Tests real enforcement rather than just reading state

Verification Matrix

Service Mount NS Network NS PID NS Cgroups Seccomp Capabilities AppArmor
dnsmasq (host)
unbound (host)
dnscrypt-proxy (host)
tor (host)
dhcpcd (host) (host, intentional)
iwd (intentional)
chrony (host) (host, intentional)
acpid (host) (zeroed)
syslog-ng (isolated)

Note: iwd bypasses bwrap entirely due to rfkill device access requirements. All other containment layers (seccomp, capabilities, cgroups, AppArmor) remain fully enforced. See the iwd exception note above.


Why Runtime Verification Matters

Most security tools check configuration files — they tell you what the system should be doing. These scripts tell you what it is doing:

Configuration Review Runtime Verification
"The config says seccomp is enabled" "Seccomp ACTIVE (filter mode, 1 filter loaded, no_new_privs set)"
"The service should be in a mount namespace" "namespace: mount ISOLATED (own mount ns)"
"AppArmor profile is loaded" "AppArmor CONFINED (/usr/bin/dnsmasq)"
"The service should have dropped capabilities" "CapEff=0x0000000000000000 CapBnd=0x0000000000000000"
"Cgroup limits are configured" "ENFORCED (memory.max=67108864 pids.max=20, PID confirmed member)"

A misconfigured mount namespace won't pass the inode comparison test. A service running outside its cgroup won't pass the cgroup.procs check. A seccomp filter that failed to load won't show as active in /proc/[pid]/status.

If the system is secure, the tests pass. If they fail, you know exactly which layer is broken.


Requirements

  • LainOS Layer 02 (or any OpenRC-based system running protocol7-core and lainos-apparmor)
  • doas configured for the invoking user
  • AppArmor active in the kernel (/sys/kernel/security/apparmor present)
  • protocol7-core-security-status additionally requires: gdb, nsenter, socat, dbus-send, pgrep

License

GPLv3, matching the rest of the lainOS project.