Skip to content

Stage 3: container modules including k3s - #22

Draft
VectoDE wants to merge 30 commits into
feat/lss-2from
feat/lss-3
Draft

VectoDE wants to merge 30 commits into
feat/lss-2from
feat/lss-3

Conversation

@VectoDE

@VectoDE VectoDE commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Stage 3 of lss 2.0 (#5): Docker, Podman, k3s and Portainer.

  • docker (Docker CE from download.docker.com, the distribution package where needed) and podman (conflict), hardened daemon settings (no-new-privileges, icc off, live-restore, userns-remap, log limits).
  • Published container ports stay behind the lss firewall: the nftables table inet lss_containers lets a redirected connection through only to ports a module opened with fw_allow.
  • lib/container.sh: containers pinned by digest, cap_drop ALL, read-only root, memory and PID limits, no host namespaces, no engine socket (Portainer is the one documented exception); host paths are checked as written and after following symbolic links. Compose files are checked against the same rules.
  • k3s with secrets encryption, Pod Security restricted, audit log and CIS kernel parameters; SELinux policy on the RHEL family. Trusted networks are bound to their interface.
  • portainer on 127.0.0.1 or the WireGuard network.
  • guard: daily Trivy scan of the container images.
  • Profile k8s-node; all gains docker and portainer.
  • CI: podman in the container matrix, docker/portainer and k3s in VMs on every supported system, minimal-secure on Ubuntu 26.04 in a VM (Stage 1b: arm64, more test VMs, Lynis 85, remaining guard checks #12).

Not run on a real machine before this pull request; the CI runs are the first test.

Refs #5

🤖 Generated with Claude Code

VectoDE and others added 30 commits October 14, 2026 02:37
…tworks

lib/container.sh runs containers for modules under fixed rules: image pinned
by digest, no new privileges, cap_drop ALL, read-only root, memory and PID
limits, no engine socket except for Portainer. It also checks Compose files,
installs the pinned Trivy release and manages the container firewall.

libexec/lss-container-firewall keeps published container ports behind the
lss firewall rules: an own nftables forward chain drops redirected
connections to container networks unless fw_allow opened the port.
fw_trust accepts host traffic from networks such as the k3s pod network.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
docker installs Docker CE from download.docker.com (pinned keys for the apt
and dnf repositories) on Debian 12/13, Ubuntu 22.04/24.04/26.04 and the RHEL
9/10 family, and docker.io from the distribution where Docker builds nothing.
daemon.json follows CIS Docker section 2: icc off, live-restore, local log
driver with limits, no-new-privileges, userns-remap (option), no userland
proxy, default ulimits, SELinux labels on RHEL, default seccomp.

podman uses the distribution packages with process and log limits. Both
install the container firewall and Trivy. AIDE skips the container data
directories.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
k3s runs a pinned, checksum-verified binary (v1.36.5+k3s1, amd64 and arm64)
as a systemd unit with /etc/rancher/k3s/config.yaml after the k3s CIS
hardening guide: encrypted secrets, protect-kernel-defaults with the kernel
values the kubelet expects, kubeconfig mode 0600, Pod Security admission,
audit log, optional Traefik. On RHEL with SELinux k3s-selinux comes as a
pinned package. The pod and service networks are trusted, the API port
follows LSS_K3S_API_ACCESS.

portainer runs Portainer CE from a pinned digest on 127.0.0.1:9443 or the
WireGuard network, with the admin password from the secrets and a data
volume. It is the only container that gets the engine socket. Optionally
below /portainer/ on the admin port of the web server.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Host paths of bind mounts were checked only as written, so a symbolic link
to the engine socket, /etc or /root passed. Both the path as written and
its realpath are now checked, and a path that cannot be resolved stops the
run. All of /etc is denied except /etc/lss/containers/NAME for container
NAME (Portainer's admin password moves there), as are /var/lib/lss*,
/var/log/lss*, /run/systemd and /run/dbus.

The Compose check covers the same rules: bind sources go through the same
path check, and privileged, cap_add ALL or denied capabilities, unconfined
security options, devices and host or container namespaces are refused.
First unit tests for lib/container.sh.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…oad ranges

fw_trust accepted traffic by source address alone, so a neighbour on the
LAN could spoof a pod address and reach every port of the host. It now
takes NETWORK INTERFACE: ufw allows "in on INTERFACE from NETWORK",
firewalld binds the interface to its trusted zone. The registry records
both, removal stays symmetric and tolerates rules that are already gone.

Networks are validated properly (address bytes, prefix ranges) and
refused when broader than /8 (IPv4) or /32 (IPv6) or when they contain the
address of the default route. A failed ufw allow stops the run. k3s
trusts its pod network on cni0 only.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
When docker or podman is installed, the guard scans every image with the
pinned Trivy for HIGH and CRITICAL vulnerabilities and reports one
finding per affected image with the numbers. Podman images are saved to an
archive first, so no socket is needed. A scan that fails is a note.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…le metadata

Unit tests for the port list and reload of the container firewall, its
setup, verify and removal, the nftables rule set, engine detection,
Compose projects, Trivy upkeep, the metadata and conflicts of the four
container modules and the pinned download checksums.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
k8s-node is minimal-secure plus k3s. profiles/all.conf gains docker and
portainer; k3s stays out because it conflicts with docker and is covered
by k8s-node.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
New container group podman (storage on a volume, not overlayfs). New VM
runs on every supported system including Ubuntu 22.04, 24.04 and 26.04
cloud images: firewall with docker and portainer, then firewall with k3s.
Profile k8s-node on Debian 13 and AlmaLinux 10. The Lynis minimums of the
profiles all and k8s-node are estimates until the first runs.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
README: container module list and section, settings examples, the
k8s-node profile, resource rows to be filled by the integration runs.
hardening.md: rows for docker, podman, the container rules, the container
firewall, portainer, k3s and the Trivy scan with their CIS Docker and k3s
CIS guide areas, plus the deliberate deviations and what is not covered.
Architecture and module guide describe the container firewall, fw_trust
and the container helpers. Changelog entries.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Quote the Trivy severity list (SC2054), keep escaped newlines in printf
formats and test strings, and wrap the long list of rejected networks.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…en a run fails

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Docker runs with user namespace remapping, so a new volume belongs to the
remapped root. Portainer runs in the host user namespace as the real
root without capabilities and stopped with 'mkdir /data/certs:
permission denied'.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…sockets

dockerd holds the published ports of the containers itself, so Portainer
on 127.0.0.1:9443 made the check report a TCP API. The check now reads
dockerd's command line and the hosts in daemon.json.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AlmaLinux 10 (RHEL 10 kernel) has no addrtype xtables match, so dockerd
fails to add its NAT rules. There dockerd now writes nftables rules
itself, and lss enables IP forwarding, which that backend leaves to the
system.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
With the nftables backend dockerd sets no FORWARD drop policy, so the
forwarding lss enables for it would route between every interface. The
container firewall now drops forwarding that involves no container
network while that sysctl file exists. Forwarding goes on only after
the rule is loaded and goes off on removal before the firewall goes.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
icc=false and the missing userland proxy need the bridge netfilter
hooks; dockerd loads br_netfilter itself only for its iptables rules.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…tables

The AlmaLinux 10 cloud image has only kernel-modules-core, which lacks
br_netfilter and xt_addrtype. Installing kernel-modules for the running
kernel lets dockerd keep its iptables rules, so the experimental
nftables backend, the forwarding sysctl and its firewall guard go away
again.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
xt_addrtype and br_netfilter are in kernel-modules-extra there, which
cloud images leave out and iptables no longer pulls in
(docker/docker-ce-packaging#1340). The check now loads the modules
instead of only resolving them.

Refs #5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
The encryption controller can report later than the API on a busy host;
the profile run on AlmaLinux 10 failed now and then right after start.
The check now waits up to 60 seconds and logs the status (key names and
hash state, no key material) when it still fails.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>

# Conflicts:
#	.github/workflows/integration.yml
#	tests/integration/vm-images.conf
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant