diff --git a/mage/download/apparmor_abi.go b/mage/download/apparmor_abi.go new file mode 100644 index 0000000..99929fd --- /dev/null +++ b/mage/download/apparmor_abi.go @@ -0,0 +1,92 @@ +package download + +// apparmorABI30 is the AppArmor 3.0 feature-ABI declaration, taken verbatim +// from Alpine apparmor-profiles-3.1.7-r4 (/etc/apparmor.d/abi/3.0), sha256 +// a786a2fde512336f885805b557b0fc4df6e0c2e21e336bba8d6f9b3c8d637a66. +// +// containerd contrib/apparmor generates the ephemerd-default profile with +// "abi ," as its first rule; apparmor_parser resolves that include +// from /etc/apparmor.d/abi/3.0 and fails the load if it is missing. We ship +// exactly this one file into the rootfs (buildRootfsTarball) instead of the +// whole apparmor-profiles package, which would also install tunables/global +// and abstractions/base and flip containerds macroExists, pulling +// glibc/musl-mismatched includes into the generated profile. +const apparmorABI30 = `query {label {multi_transaction {yes +} +data {yes +} +perms {allow deny audit quiet +} +} +} +dbus {mask {acquire send receive +} +} +signal {mask {hup int quit ill trap abrt bus fpe kill usr1 segv usr2 pipe alrm term stkflt chld cont stop stp ttin ttou urg xcpu xfsz vtalrm prof winch io pwr sys emt lost +} +} +ptrace {mask {read trace +} +} +caps {mask {chown dac_override dac_read_search fowner fsetid kill setgid setuid setpcap linux_immutable net_bind_service net_broadcast net_admin net_raw ipc_lock ipc_owner sys_module sys_rawio sys_chroot sys_ptrace sys_pacct sys_admin sys_boot sys_nice sys_resource sys_time sys_tty_config mknod lease audit_write audit_control setfcap mac_override mac_admin syslog wake_alarm block_suspend audit_read perfmon bpf +} +} +rlimit {mask {cpu fsize data stack core rss nproc nofile memlock as locks sigpending msgqueue nice rtprio rttime +} +} +capability {0xffffff +} +namespaces {pivot_root {no +} +profile {yes +} +} +mount {mask {mount umount pivot_root +} +} +network {af_unix {yes +} +af_mask {unspec unix inet ax25 ipx appletalk netrom bridge atmpvc x25 inet6 rose netbeui security key netlink packet ash econet atmsvc rds sna irda pppox wanpipe llc ib mpls can tipc bluetooth iucv rxrpc isdn phonet ieee802154 caif alg nfc vsock kcm qipcrtr smc xdp +} +} +network_v8 {af_mask {unspec unix inet ax25 ipx appletalk netrom bridge atmpvc x25 inet6 rose netbeui security key netlink packet ash econet atmsvc rds sna irda pppox wanpipe llc ib mpls can tipc bluetooth iucv rxrpc isdn phonet ieee802154 caif alg nfc vsock kcm qipcrtr smc xdp +} +} +file {mask {create read write exec append mmap_exec link lock +} +} +domain {version {1.2 +} +attach_conditions {xattr {yes +} +} +computed_longest_left {yes +} +post_nnp_subset {yes +} +fix_binfmt_elf_mmap {yes +} +stack {yes +} +change_profile {yes +} +change_onexec {yes +} +change_hatv {yes +} +change_hat {yes +} +} +policy {set_load {yes +} +versions {v8 {yes +} +v7 {yes +} +v6 {yes +} +v5 {yes +} +} +} +` diff --git a/mage/download/download.go b/mage/download/download.go index 91cbe9f..77286e6 100644 --- a/mage/download/download.go +++ b/mage/download/download.go @@ -284,8 +284,17 @@ type apkPkg struct { } // Packages to pre-install in the rootfs, pinned to Alpine 3.21.3. -// These are the transitive dependencies of gcompat and iptables. +// These are the transitive dependencies of gcompat and iptables, plus +// apparmor (for apparmor_parser) and its libintl dependency. // Update versions when bumping AlpineVersion. +// +// AppArmor: the in-VM ephemerd confines job containers with the generated +// "ephemerd-default" profile, which containerd's contrib/apparmor loads by +// exec'ing apparmor_parser. Ship the parser (apparmor) and the libintl.so.8 +// it links against. apparmor_parser does NOT link libapparmor, and the +// generated profile is self-contained on this minimal rootfs (no +// /etc/apparmor.d abstractions to #include), so nothing else is required. +// See pkg/runtime/apparmor.go and the securityfs mount in the init script. var rootfsPackages = []apkPkg{ {"musl-obstack", "1.2.3-r2", "main"}, {"libucontext", "1.3.2-r0", "main"}, @@ -294,6 +303,8 @@ var rootfsPackages = []apkPkg{ {"libnftnl", "1.2.8-r0", "main"}, {"libxtables", "1.8.11-r1", "main"}, {"iptables", "1.8.11-r1", "main"}, + {"libintl", "0.22.5-r0", "main"}, + {"apparmor", "3.1.7-r4", "main"}, } // Rootfs builds a custom Alpine rootfs with gcompat and iptables pre-installed. @@ -1227,6 +1238,14 @@ mkdir -p /newroot/sys/fs/cgroup mount -t cgroup2 none /newroot/sys/fs/cgroup || \ echo "ephemerd-init: WARNING: cgroup2 mount failed" +# securityfs holds AppArmor's interface (/sys/kernel/security/apparmor). The +# kernel creates the /sys/kernel/security mountpoint but does not mount +# securityfs on it; without this the in-VM ephemerd cannot read the loaded- +# profile list or load the ephemerd-default profile, so job containers run +# unconfined. Requires apparmor=1 on the kernel cmdline (see linuxvm_darwin.go). +mount -t securityfs none /newroot/sys/kernel/security 2>/dev/null || \ + echo "ephemerd-init: WARNING: securityfs mount failed (AppArmor confinement unavailable)" + # Mount virtio-fs share at the final location mount -t virtiofs "$SHARE_TAG" /newroot/mnt/ephemerd || \ echo "ephemerd-init: WARNING: could not mount virtio-fs share" @@ -1689,6 +1708,14 @@ mkdir -p /newroot/sys/fs/cgroup mount -t cgroup2 none /newroot/sys/fs/cgroup || \ echo "ephemerd-init: WARNING: cgroup2 mount failed" +# securityfs holds AppArmor's interface (/sys/kernel/security/apparmor). The +# kernel creates the /sys/kernel/security mountpoint but does not mount +# securityfs on it; without this the in-VM ephemerd cannot read the loaded- +# profile list or load the ephemerd-default profile, so job containers run +# unconfined. Requires apparmor=1 on the kernel cmdline (see linuxvm_windows.go). +mount -t securityfs none /newroot/sys/kernel/security 2>/dev/null || \ + echo "ephemerd-init: WARNING: securityfs mount failed (AppArmor confinement unavailable)" + export PATH=/usr/bin:/usr/sbin:/bin:/sbin export HOME=/root @@ -1958,6 +1985,16 @@ func buildRootfsTarball(dest string, baseData []byte, pkgData [][]byte, packages } } + // Ship the AppArmor 3.0 ABI file the generated ephemerd-default profile + // includes via "abi ,". Without it apparmor_parser fails the load + // and job containers fall back to unconfined. See apparmorABI30. + if terr := appendApparmorABI(tw); terr != nil { + _ = tw.Close() + _ = gw.Close() + _ = f.Close() + return fmt.Errorf("writing apparmor abi: %w", terr) + } + if err = tw.Close(); err != nil { _ = gw.Close() _ = f.Close() @@ -1975,6 +2012,34 @@ func buildRootfsTarball(dest string, baseData []byte, pkgData [][]byte, packages return nil } +// appendApparmorABI writes /etc/apparmor.d/abi/3.0 into the rootfs tar so +// apparmor_parser can resolve the "abi ," include at the top of the +// generated ephemerd-default profile. The content is CR-stripped in case the +// source file was checked out with CRLF endings (core.autocrlf), which +// apparmor_parser would otherwise reject. +func appendApparmorABI(tw *tar.Writer) error { + abi := []byte(strings.ReplaceAll(apparmorABI30, "\r\n", "\n")) + for _, d := range []string{"etc/apparmor.d/", "etc/apparmor.d/abi/"} { + if err := tw.WriteHeader(&tar.Header{ + Name: d, + Typeflag: tar.TypeDir, + Mode: 0o755, + }); err != nil { + return err + } + } + if err := tw.WriteHeader(&tar.Header{ + Name: "etc/apparmor.d/abi/3.0", + Typeflag: tar.TypeReg, + Mode: 0o644, + Size: int64(len(abi)), + }); err != nil { + return err + } + _, err := tw.Write(abi) + return err +} + // Golangcilint downloads golangci-lint to ./bin/. func Golangcilint() error { goos := runtime.GOOS diff --git a/pkg/vm/linuxvm_darwin.go b/pkg/vm/linuxvm_darwin.go index 5aaef71..9657989 100644 --- a/pkg/vm/linuxvm_darwin.go +++ b/pkg/vm/linuxvm_darwin.go @@ -241,8 +241,17 @@ func (l *darwinLinuxVM) boot() error { bootLoader, err := vz.NewLinuxBootLoader( l.kernelPath(), vz.WithInitrd(l.initrdPath()), + // apparmor=1 security=apparmor turns on the AppArmor LSM in the guest. + // The Alpine linux-virt kernel ships AppArmor compiled in but disabled + // at boot (its CONFIG_LSM omits apparmor), so without this the in-VM + // ephemerd's apparmorOpts fails open and Linux job containers run + // unconfined — unlike native Linux hosts. There is no lsm= here, so + // security=apparmor is honored (not superseded). The init script mounts + // securityfs so the profile can be loaded. Keep in sync with the + // Windows sidecar (linuxvm_windows.go). vz.WithCommandLine(fmt.Sprintf( - "console=hvc0 root=/dev/vda rw ephemerd.containerd_port=%d ephemerd.share_tag=ephemerd", + "console=hvc0 root=/dev/vda rw ephemerd.containerd_port=%d ephemerd.share_tag=ephemerd "+ + "apparmor=1 security=apparmor", l.cfg.ContainerdPort, )), ) diff --git a/pkg/vm/linuxvm_windows.go b/pkg/vm/linuxvm_windows.go index 4047e71..9977695 100644 --- a/pkg/vm/linuxvm_windows.go +++ b/pkg/vm/linuxvm_windows.go @@ -524,6 +524,15 @@ func (l *hypervLinuxVM) createAndBootVM() error { // /dev/sda as root and find init there, which fails on an unformatted VHDX. // - 8250_core: enable serial UART for console output via named pipe // - ephemerd.*: custom params parsed by our init script + // - apparmor=1 security=apparmor: turn on the AppArmor LSM. The Alpine + // linux-virt kernel ships AppArmor compiled in but disabled at boot + // (its CONFIG_LSM omits apparmor), so without this the in-VM ephemerd's + // apparmorOpts fails open and Linux job containers run unconfined — + // unlike native Linux hosts. apparmor=1 flips the module's enabled + // parameter; security=apparmor appends apparmor to the LSM order (there + // is no lsm= on this cmdline, so security= is honored, not superseded). + // The init script mounts securityfs so the profile can be loaded. Keep + // this in sync with the darwin sidecar (linuxvm_darwin.go). // The dind* params are redundant when the host's config.toml rides in // via the initrd tail (the in-VM ephemerd reads the same [dind] // section the host does). Kept as a fallback for the no-config path — @@ -538,7 +547,8 @@ func (l *hypervLinuxVM) createAndBootVM() error { cmdline := fmt.Sprintf( "rdinit=/init ephemerd.containerd_port=%d ephemerd.root_disk=/dev/sda%s "+ "pci=off brd.rd_nr=0 pmtmr=0 nr_cpus=%d "+ - "8250_core.nr_uarts=1 8250_core.skip_txen_test=1 console=ttyS0,115200", + "8250_core.nr_uarts=1 8250_core.skip_txen_test=1 console=ttyS0,115200 "+ + "apparmor=1 security=apparmor", l.cfg.ContainerdPort, dindFlag, l.cfg.CPUs, )