Skip to content

Commit 5975b92

Browse files
committed
Merge branch 'development' of github.com:cuos-dev/cuos into development
2 parents 396c75c + 5492c09 commit 5975b92

14 files changed

Lines changed: 241 additions & 10 deletions

‎docs/architecture.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ In-place patch (`cuos patch*`) modifies `/system.json` directly and keeps previo
140140
| Network | Minimal exposed ports, host network only if required |
141141
| Privileges | App container without `--privileged`; granular capabilities |
142142
| CA | Inject custom CA via `custom_ca_certs` & rotate regularly |
143+
| Confinement | AppArmor is on, and dockerd confines the app container with `docker-default` — `docs/common/apparmor.md` |
143144

144145
---
145146

@@ -150,3 +151,4 @@ In-place patch (`cuos patch*`) modifies `/system.json` directly and keeps previo
150151
* `image-factory/create_image.sh` – Raw image creation
151152
* `installer-factory/create_iso.sh` – Installer ISO
152153
* `docs/common/btrfs-usage.md` – Subvolume layout
154+
* `docs/common/apparmor.md` – What AppArmor confines, and what it does not

‎docs/common/apparmor.md‎

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# AppArmor
2+
3+
CuOS boots with AppArmor enabled (`apparmor=1` on the kernel command line) on
4+
every platform except LXC. `cuos selftest` reports the state under the
5+
`apparmor` check.
6+
7+
## What is confined
8+
9+
| | Profile | Where it comes from |
10+
|---|---|---|
11+
| Every container dockerd starts | `docker-default` | dockerd's built-in template, loaded when the daemon starts |
12+
| `dhclient` | `/usr/sbin/dhclient` | Debian's `isc-dhcp-client` package |
13+
| PAM's password check | `unix-chkpwd`, in complain mode | Debian's `apparmor` package |
14+
15+
That is the whole list, and `docker-default` is the point of the exercise: it is
16+
what stands between a container and the host — the application container and
17+
everything deployed through cuos-iac alike. Debian's `apparmor-profiles` and
18+
`apparmor-profiles-extra` packages are not installed — they cover samba,
19+
syslog, avahi and desktop software, none of which is here.
20+
21+
The `apparmor` package's own ~100 profiles are inert here: most are the Debian
22+
13 userns stubs (`flags=(unconfined)`, body `userns,`), and every one of the
23+
rest is either in complain mode or carries no attachment path. None of them can
24+
deny anything, including inside a container that runs unconfined.
25+
26+
## What is not confined
27+
28+
- **dockerd, and the CuOS scripts.** A daemon that must be able to do anything
29+
cannot usefully be confined, and the CuOS scripts run as root and start
30+
privileged containers.
31+
- **The updater.** `do-update.sh` runs it with `--privileged`, and Docker runs
32+
privileged containers unconfined. It repartitions the root disk, so it needs
33+
to be. It is pulled by digest, and the digest is checked before it runs.
34+
- **sshd.** Debian ships no profile for it, and it is masked unless
35+
`os_ssh_server` is set.
36+
- **The LXC platform.** Profiles are loaded into the host's kernel and a
37+
container cannot load its own, so the LXC image carries no AppArmor at all.
38+
Confining a CuOS guest is the LXC host's business.
39+
- **The installer ISO.** A separate image without the AppArmor package.
40+
41+
## What `docker-default` forbids a container
42+
43+
The profile is permissive by design: `network,`, `capability,`, `file,` and
44+
`umount,` are allowed outright, so every file and every capability is on the
45+
table. What it carries is a short list of denials, and three of them reach
46+
ordinary containers.
47+
48+
| Denied | What stops working |
49+
|---|---|
50+
| `deny mount,` | FUSE (sshfs, s3fs, rclone), `mount --bind` in an entrypoint, NFS/CIFS from inside, docker-in-docker, systemd as PID 1 |
51+
| writes below `/sys`, everything but `/sys/fs/cgroup/**` | sysfs GPIO (`/sys/class/gpio/export`), LEDs, PWM, backlight, driver `bind`/`unbind`, `/sys/class/net/*` |
52+
| `deny @{PROC}/sys/[^k]** w,` | `sysctl -w` run inside the container |
53+
54+
Reading is untouched — only writes to `/sys` and `/proc` are denied.
55+
56+
**`cap_add: [SYS_ADMIN]` does not lift any of this.** The capability is granted
57+
and AppArmor refuses anyway, and the error names neither. `privileged: true`
58+
does lift it, because Docker runs privileged containers unconfined.
59+
60+
Unaffected, and usually the cheaper way out of a denial:
61+
62+
- `volumes:`, `tmpfs:`, `read_only:` and `sysctls:` in a compose file — runc
63+
applies those before the container starts, and runc is unconfined.
64+
- Device nodes: `/dev/gpiochip*` (libgpiod), `/dev/i2c-*`, `/dev/spidev*` are
65+
covered by `file,`. Only the sysfs route to the same hardware is denied.
66+
67+
`docker-default` separates a container from the **host**, not from other
68+
containers. Every container carries the same profile name, so the
69+
`signal (send,receive) peer=docker-default` and
70+
`ptrace (trace,read) peer=docker-default` rules of the template apply between
71+
containers as soon as they share a PID namespace (`pid: host`,
72+
`pid: service:…`).
73+
74+
The rules above are moby's built-in template as of 26.1.5, the version in
75+
Debian trixie's `docker.io`. Read `aa-status` on the device rather than this
76+
page if a denial does not match.
77+
78+
## Giving an application more than `docker-default`
79+
80+
For a service deployed through cuos-iac, the profile is chosen per service in
81+
the compose file, and adds to what is already there:
82+
83+
```yml
84+
services:
85+
app:
86+
security_opt:
87+
- apparmor=unconfined
88+
```
89+
90+
For the application container, the flags come from the `dev.cuos.app_command`
91+
label on the app image (see `cuos-app-init.md`), so it is selected there:
92+
93+
```
94+
LABEL dev.cuos.app_command="--security-opt apparmor=my-profile ..."
95+
```
96+
97+
Note that the label **replaces** the default flag set rather than adding to it.
98+
99+
The profile itself has to be loaded into the kernel before the container
100+
starts, and CuOS ships no mechanism for that yet. So the choice today is
101+
`docker-default` or nothing: while an application is being brought up,
102+
`--security-opt apparmor=unconfined` is the escape hatch.
103+
104+
## Reading a denial
105+
106+
```
107+
journalctl -b | grep DENIED
108+
```
109+
110+
Each line names the profile, the operation and the path. To take one profile
111+
out of the way without touching the rest:
112+
113+
```
114+
aa-complain /etc/apparmor.d/usr.sbin.dhclient # log, do not block
115+
aa-enforce /etc/apparmor.d/usr.sbin.dhclient # back to blocking
116+
```
117+
118+
Both are lost on the next boot, which is what you want while diagnosing. A
119+
permanent addition goes into the profile's `local/` include —
120+
`/etc/apparmor.d/local/<profile>` — which survives package upgrades.
121+
122+
## The `/data` trap
123+
124+
CuOS keeps mutable state in `/data` and symlinks into it: `/var/log`,
125+
`/var/lib/dhcp`, `/var/lib/docker`, `/var/lib/containerd`, `/root`.
126+
127+
**AppArmor mediates the resolved path, not the symlink.** A stock profile that
128+
allows `/var/lib/dhcp/dhclient*` therefore denies the write that actually lands
129+
on `/data/dhcp/dhclient.leases`. That is why the image ships
130+
131+
```
132+
/data/dhcp/ r,
133+
/data/dhcp/dhclient* lrw,
134+
```
135+
136+
in `/etc/apparmor.d/local/usr.sbin.dhclient`. Any profile adopted from a
137+
distribution package has to be read against these symlinks first.

‎installer-factory/Dockerfile‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
2727
linux-image-amd64 \
2828
mtools \
2929
pv \
30-
selinux-policy-default \
3130
syslinux \
3231
xorriso \
3332
&& apt-get clean && rm -rf /var/lib/apt/lists/*

‎installer-factory/prepare_iso.sh‎

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

2222
# Create grub config
23+
# apparmor=0, unlike the system image: this one has no apparmor package.
2324
cat <<EOF >"${ISO_DIR}/boot/grub/grub.cfg"
2425
set timeout=0
2526
set default=0

‎system/Dockerfile‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ RUN sed -i 's/^Components: main$/Components: main non-free-firmware/' /etc/apt/s
4848
## cuos os extra for wake on lan \
4949
ethtool \
5050
## cuos os dependencies \
51+
apparmor \
5152
btrfs-progs \
5253
console-setup \
5354
dbus \
@@ -57,7 +58,6 @@ RUN sed -i 's/^Components: main$/Components: main non-free-firmware/' /etc/apt/s
5758
kbd \
5859
kmod \
5960
lsb-release \
60-
selinux-policy-default \
6161
systemd \
6262
systemd-sysv \
6363
systemd-timesyncd \
@@ -89,6 +89,9 @@ RUN sed -i 's/^Components: main$/Components: main non-free-firmware/' /etc/apt/s
8989
&& systemctl disable hv-kvp-daemon.service \
9090
&& systemctl disable hv-vss-daemon.service \
9191
&& systemctl enable systemd-timesyncd.service \
92+
&& systemctl enable apparmor.service \
93+
## /var/lib/dhcp is a symlink into /data, and AppArmor sees the resolved path \
94+
&& printf "/data/dhcp/ r,\n/data/dhcp/dhclient* lrw,\n" >>/etc/apparmor.d/local/usr.sbin.dhclient \
9295
&& systemctl mask ssh.socket \
9396
&& systemctl mask sshd-keygen.service \
9497
&& printf "Port 4222\nPermitRootLogin yes\n" >/etc/ssh/sshd_config.d/cuos.conf \

‎system/Dockerfile.lxc‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ RUN echo "Cache buster: v1" \
1616
## cuos system dependencies (lxc) \
1717
busybox-static \
1818
## cuos os dependencies \
19+
# A container cannot load profiles - that is the LXC host's business \
20+
#-apparmor \
1921
#-btrfs-progs \
2022
dbus \
2123
dbus-broker \
@@ -24,7 +26,6 @@ RUN echo "Cache buster: v1" \
2426
#-kbd \
2527
kmod \
2628
lsb-release \
27-
#-selinux-policy-default \
2829
systemd \
2930
systemd-sysv \
3031
#-systemd-timesyncd \

‎system/Dockerfile.orangepi-zero3‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
7575
# rpi-swap \
7676
# iw \
7777
## cuos os dependencies \
78+
apparmor \
7879
btrfs-progs \
7980
console-setup \
8081
dbus \
@@ -84,7 +85,6 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
8485
kbd \
8586
kmod \
8687
lsb-release \
87-
selinux-policy-default \
8888
systemd \
8989
systemd-sysv \
9090
systemd-timesyncd \
@@ -112,6 +112,9 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
112112
vim-tiny \
113113
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
114114
&& systemctl enable systemd-timesyncd.service \
115+
&& systemctl enable apparmor.service \
116+
## /var/lib/dhcp is a symlink into /data, and AppArmor sees the resolved path \
117+
&& printf "/data/dhcp/ r,\n/data/dhcp/dhclient* lrw,\n" >>/etc/apparmor.d/local/usr.sbin.dhclient \
115118
&& systemctl mask ssh.socket \
116119
&& systemctl mask sshd-keygen.service \
117120
&& printf "Port 4222\nPermitRootLogin yes\n" >/etc/ssh/sshd_config.d/cuos.conf \

‎system/Dockerfile.rpi-arm32‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
114114
# rpi-swap \
115115
# iw \
116116
## cuos os dependencies \
117+
apparmor \
117118
btrfs-progs \
118119
console-setup \
119120
dbus \
@@ -123,7 +124,6 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
123124
kbd \
124125
kmod \
125126
lsb-release \
126-
selinux-policy-default \
127127
systemd \
128128
systemd-sysv \
129129
systemd-timesyncd \
@@ -151,6 +151,9 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
151151
vim-tiny \
152152
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
153153
&& systemctl enable systemd-timesyncd.service \
154+
&& systemctl enable apparmor.service \
155+
## /var/lib/dhcp is a symlink into /data, and AppArmor sees the resolved path \
156+
&& printf "/data/dhcp/ r,\n/data/dhcp/dhclient* lrw,\n" >>/etc/apparmor.d/local/usr.sbin.dhclient \
154157
&& systemctl mask ssh.socket \
155158
&& systemctl mask sshd-keygen.service \
156159
&& printf "Port 4222\nPermitRootLogin yes\n" >/etc/ssh/sshd_config.d/cuos.conf \

‎system/Dockerfile.rpi-arm64‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
9595
# rpi-swap \
9696
# iw \
9797
## cuos os dependencies \
98+
apparmor \
9899
btrfs-progs \
99100
console-setup \
100101
dbus \
@@ -104,7 +105,6 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
104105
kbd \
105106
kmod \
106107
lsb-release \
107-
selinux-policy-default \
108108
systemd \
109109
systemd-sysv \
110110
systemd-timesyncd \
@@ -132,6 +132,9 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
132132
vim-tiny \
133133
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
134134
&& systemctl enable systemd-timesyncd.service \
135+
&& systemctl enable apparmor.service \
136+
## /var/lib/dhcp is a symlink into /data, and AppArmor sees the resolved path \
137+
&& printf "/data/dhcp/ r,\n/data/dhcp/dhclient* lrw,\n" >>/etc/apparmor.d/local/usr.sbin.dhclient \
135138
&& systemctl mask ssh.socket \
136139
&& systemctl mask sshd-keygen.service \
137140
&& printf "Port 4222\nPermitRootLogin yes\n" >/etc/ssh/sshd_config.d/cuos.conf \

‎system/armbianEnv.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ disp_mode=1920x1080p60
77
overlay_prefix=sun50i-h616
88
rootdev=LABEL=system
99
rootfstype=btrfs
10-
extraargs=rootflags=subvol=@os/system-{{SLOT}} loglevel=3 noresume apparmor=0
10+
extraargs=rootflags=subvol=@os/system-{{SLOT}} loglevel=3 noresume apparmor=1
1111
usbstoragequirks=0x2537:0x1066:u,0x2537:0x1068:u

0 commit comments

Comments
 (0)