Skip to content

Commit 4050fbb

Browse files
simonwalz-worksimonwalz
authored andcommitted
feat(selftest): report AppArmor, and ask for docker-default by name
docker-default is what AppArmor is here for - it is the only thing confining the application container, and dockerd loads it silently or not at all. The check fails on an LSM that is off, on a missing docker-default, and on one loaded in complain mode, where it would look present and confine nothing. Skipped in a container, like subvolumes and network. Tests: 43 pass, shellcheck clean. Never run on a booted system. Signed-off-by: Simon Walz <contact@simonwalz.de>
1 parent 15cf039 commit 4050fbb

8 files changed

Lines changed: 86 additions & 20 deletions

File tree

‎installer-factory/prepare_iso.sh‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,7 @@ mv "/boot/vmlinuz"-* "${ISO_DIR}/boot/vmlinuz"
2020
mv "${INITRD_IMAGE}" "${ISO_DIR}/boot/initrd.img"
2121

2222
# Create grub config
23-
#
24-
# The installer keeps apparmor=0 while the system image boots with apparmor=1.
25-
# This image is not the system image: it carries no apparmor package, so there
26-
# is no profile to load and no apparmor.service to load one. Switching the LSM
27-
# on here would confine nothing and could only cost an installation.
23+
# apparmor=0, unlike the system image: this one has no apparmor package.
2824
cat <<EOF >"${ISO_DIR}/boot/grub/grub.cfg"
2925
set timeout=0
3026
set default=0

‎system/Dockerfile‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,7 @@ RUN sed -i 's/^Components: main$/Components: main non-free-firmware/' /etc/apt/s
9090
&& systemctl disable hv-vss-daemon.service \
9191
&& systemctl enable systemd-timesyncd.service \
9292
&& systemctl enable apparmor.service \
93-
## AppArmor mediates the resolved path, and /var/lib/dhcp below is a \
94-
## symlink into /data - without this rule the stock dhclient profile \
95-
## denies dhclient its own lease file \
93+
## /var/lib/dhcp is a symlink into /data, and AppArmor sees the resolved path \
9694
&& printf "/data/dhcp/ r,\n/data/dhcp/dhclient* lrw,\n" >>/etc/apparmor.d/local/usr.sbin.dhclient \
9795
&& systemctl mask ssh.socket \
9896
&& systemctl mask sshd-keygen.service \

‎system/Dockerfile.lxc‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ RUN echo "Cache buster: v1" \
1616
## cuos system dependencies (lxc) \
1717
busybox-static \
1818
## cuos os dependencies \
19-
# AppArmor profiles are loaded into the host's kernel, and a container \
20-
# cannot load its own. Confining an LXC guest is the host's business. \
19+
# A container cannot load profiles - that is the LXC host's business \
2120
#-apparmor \
2221
#-btrfs-progs \
2322
dbus \

‎system/Dockerfile.orangepi-zero3‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
113113
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
114114
&& systemctl enable systemd-timesyncd.service \
115115
&& systemctl enable apparmor.service \
116-
## AppArmor mediates the resolved path, and /var/lib/dhcp below is a \
117-
## symlink into /data - without this rule the stock dhclient profile \
118-
## denies dhclient its own lease file \
116+
## /var/lib/dhcp is a symlink into /data, and AppArmor sees the resolved path \
119117
&& printf "/data/dhcp/ r,\n/data/dhcp/dhclient* lrw,\n" >>/etc/apparmor.d/local/usr.sbin.dhclient \
120118
&& systemctl mask ssh.socket \
121119
&& systemctl mask sshd-keygen.service \

‎system/Dockerfile.rpi-arm32‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
152152
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
153153
&& systemctl enable systemd-timesyncd.service \
154154
&& systemctl enable apparmor.service \
155-
## AppArmor mediates the resolved path, and /var/lib/dhcp below is a \
156-
## symlink into /data - without this rule the stock dhclient profile \
157-
## denies dhclient its own lease file \
155+
## /var/lib/dhcp is a symlink into /data, and AppArmor sees the resolved path \
158156
&& printf "/data/dhcp/ r,\n/data/dhcp/dhclient* lrw,\n" >>/etc/apparmor.d/local/usr.sbin.dhclient \
159157
&& systemctl mask ssh.socket \
160158
&& systemctl mask sshd-keygen.service \

‎system/Dockerfile.rpi-arm64‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
133133
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
134134
&& systemctl enable systemd-timesyncd.service \
135135
&& systemctl enable apparmor.service \
136-
## AppArmor mediates the resolved path, and /var/lib/dhcp below is a \
137-
## symlink into /data - without this rule the stock dhclient profile \
138-
## denies dhclient its own lease file \
136+
## /var/lib/dhcp is a symlink into /data, and AppArmor sees the resolved path \
139137
&& printf "/data/dhcp/ r,\n/data/dhcp/dhclient* lrw,\n" >>/etc/apparmor.d/local/usr.sbin.dhclient \
140138
&& systemctl mask ssh.socket \
141139
&& systemctl mask sshd-keygen.service \

‎system/cuos/selftest.sh‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ listening_on() {
8080
ss -ltn 2>/dev/null | grep -qE "[:.]$1[[:space:]]"
8181
}
8282

83+
apparmor_enabled() {
84+
[[ "$(cat /sys/module/apparmor/parameters/enabled 2>/dev/null)" == "Y" ]]
85+
}
86+
87+
# "<profile> (<mode>)" per line, for every profile loaded into the kernel.
88+
apparmor_profiles() {
89+
cat /sys/kernel/security/apparmor/profiles 2>/dev/null
90+
}
91+
8392
# How many cuos journal entries of priority error or worse this boot produced.
8493
journal_errors() {
8594
journalctl -t cuos -b -p 3 --no-pager -o cat 2>/dev/null | grep -c . || true
@@ -177,6 +186,35 @@ check_docker() {
177186
failed "docker" "docker is not responding"
178187
}
179188

189+
# docker-default is the only thing confining the application container, and
190+
# both ways of losing it - the LSM off, or dockerd never loading the profile -
191+
# are silent. So it is asked for by name.
192+
check_apparmor() {
193+
if is_container; then
194+
skipped "apparmor" "profiles belong to the host's kernel; a container loads none of its own"
195+
return
196+
fi
197+
if ! apparmor_enabled; then
198+
failed "apparmor" "the kernel has AppArmor off - check apparmor=1 on the command line"
199+
return
200+
fi
201+
202+
local profiles count mode
203+
profiles="$(apparmor_profiles)"
204+
count="$(grep -c . <<<"${profiles}")"
205+
mode="$(sed -n 's/^docker-default (\(.*\))$/\1/p' <<<"${profiles}")"
206+
207+
if [[ -z "${mode}" ]]; then
208+
failed "apparmor" "dockerd loaded no docker-default; profiles in the kernel: ${count}"
209+
return
210+
fi
211+
if [[ "${mode}" != "enforce" ]]; then
212+
failed "apparmor" "docker-default is in ${mode} mode, so it confines nothing"
213+
return
214+
fi
215+
ok "apparmor" "${count} profiles loaded, docker-default enforcing"
216+
}
217+
180218
check_app() {
181219
local status
182220
status="$(container_state "${APP_CONTAINER}")"
@@ -278,6 +316,7 @@ selftest() {
278316
check_subvolumes
279317
check_state
280318
check_docker
319+
check_apparmor
281320
check_app
282321
check_network
283322
check_hostname

‎system/cuos/selftest.test.sh‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ MOCK_DOCKER_OK=0
3333
MOCK_APP_STATE="running"
3434
MOCK_LISTENING=0
3535
MOCK_JOURNAL_ERRORS=0
36+
MOCK_APPARMOR_ENABLED=0
37+
MOCK_APPARMOR_PROFILES="docker-default (enforce)
38+
/usr/sbin/dhclient (enforce)
39+
unix-chkpwd (enforce)"
3640

3741
virt_type() { echo "${MOCK_VIRT}"; }
3842
subvolumes() { echo "${MOCK_SUBVOLUMES}"; }
@@ -42,6 +46,8 @@ docker_ok() { return "${MOCK_DOCKER_OK}"; }
4246
container_state() { echo "${MOCK_APP_STATE}"; }
4347
listening_on() { return "${MOCK_LISTENING}"; }
4448
journal_errors() { echo "${MOCK_JOURNAL_ERRORS}"; }
49+
apparmor_enabled() { return "${MOCK_APPARMOR_ENABLED}"; }
50+
apparmor_profiles() { echo "${MOCK_APPARMOR_PROFILES}"; }
4551

4652
# A healthy system, which each test then breaks in exactly one way.
4753
reset_system() {
@@ -56,6 +62,10 @@ reset_system() {
5662
MOCK_APP_STATE="running"
5763
MOCK_LISTENING=0
5864
MOCK_JOURNAL_ERRORS=0
65+
MOCK_APPARMOR_ENABLED=0
66+
MOCK_APPARMOR_PROFILES="docker-default (enforce)
67+
/usr/sbin/dhclient (enforce)
68+
unix-chkpwd (enforce)"
5969

6070
cat >"${CONFIG_PATH}" <<'EOF'
6171
{
@@ -151,6 +161,36 @@ expect "state: anything but running fails" "failed" status_of state is_updating
151161
break_docker() { MOCK_DOCKER_OK=1; }
152162
expect "docker: an unresponsive daemon fails" "failed" status_of docker break_docker
153163

164+
lsm_off() { MOCK_APPARMOR_ENABLED=1; }
165+
expect "apparmor: a kernel with the LSM off fails" "failed" status_of apparmor lsm_off
166+
expect "apparmor: the failure names the command line" \
167+
"the kernel has AppArmor off - check apparmor=1 on the command line" \
168+
detail_of apparmor lsm_off
169+
170+
no_docker_default() { MOCK_APPARMOR_PROFILES="/usr/sbin/dhclient (enforce)"; }
171+
expect "apparmor: enabled but no docker-default fails" "failed" status_of \
172+
apparmor no_docker_default
173+
expect "apparmor: the failure counts what did load" \
174+
"dockerd loaded no docker-default; profiles in the kernel: 1" detail_of \
175+
apparmor no_docker_default
176+
177+
complaining() { MOCK_APPARMOR_PROFILES="docker-default (complain)"; }
178+
expect "apparmor: a complaining docker-default fails" "failed" status_of \
179+
apparmor complaining
180+
expect "apparmor: and says it confines nothing" \
181+
"docker-default is in complain mode, so it confines nothing" detail_of \
182+
apparmor complaining
183+
184+
# "docker-default-something" is a different profile, not this one.
185+
similar_name() { MOCK_APPARMOR_PROFILES="docker-default-old (enforce)"; }
186+
expect "apparmor: a profile merely starting with the name does not count" \
187+
"failed" status_of apparmor similar_name
188+
189+
expect "apparmor: a healthy system reports the count" \
190+
"3 profiles loaded, docker-default enforcing" detail_of apparmor nothing
191+
expect "apparmor: skipped in a container" "skipped" status_of apparmor \
192+
be_a_container
193+
154194
app_exited() { MOCK_APP_STATE="exited"; }
155195
expect "app: a stopped application fails" "failed" status_of app app_exited
156196
app_missing() { MOCK_APP_STATE=""; }
@@ -193,7 +233,7 @@ expect "a skipped check does not" "true" overall be_a_container
193233
check_count() {
194234
report nothing | jq -r '.checks | length'
195235
}
196-
expect "every check reports" "11" check_count
236+
expect "every check reports" "12" check_count
197237

198238
summary_adds_up() {
199239
report nothing | jq -r '

0 commit comments

Comments
 (0)