Skip to content

daemon: respect explicit AppArmor profile on privileged containers#52215

Merged
thaJeztah merged 1 commit intomoby:masterfrom
agners:fix-apparmor-profile-load-if-privileged
Apr 2, 2026
Merged

daemon: respect explicit AppArmor profile on privileged containers#52215
thaJeztah merged 1 commit intomoby:masterfrom
agners:fix-apparmor-profile-load-if-privileged

Conversation

@agners
Copy link
Copy Markdown
Contributor

@agners agners commented Mar 25, 2026

What I did

Fixed saveAppArmorConfig unconditionally overwriting a user-specified AppArmor profile with "unconfined" on privileged containers. This caused containers with an explicit --security-opt apparmor=<profile> to lose their intended AppArmor policy after a restart (stop+start, host reboot, or daemon restart).

How I did it

Inverted the condition nesting in saveAppArmorConfig (daemon/container_linux.go) so that "unconfined" (privileged default) and "docker-default" are only applied when no explicit profile was set via SecurityOpt. Previously the Privileged check came first and unconditionally overwrote any profile parsed from SecurityOpt.

The bug manifests as follows:

  1. On first docker start, createSpecWithApparmor builds the OCI spec using the correct in-memory AppArmorProfile value (set during docker create).
  2. saveAppArmorConfig then runs after spec creation and overwrites AppArmorProfile to "unconfined" for privileged containers.
  3. CheckpointTo persists this incorrect "unconfined" value to disk.
  4. On subsequent starts, the container loads with AppArmorProfile = "unconfined", and WithApparmor picks up that stale value — the container runs without its intended AppArmor policy.

The fix makes saveAppArmorConfig consistent with the existing precedence in both WithApparmor (oci_linux.go) and execSetPlatformOpt (exec_linux.go), which already give explicit profiles priority over the privileged default.

The bug was introduced in d97a00d (Docker 17.04, #27083) and survived a refactor in 483aa62 (#43130).

How to verify it

  1. Load a custom AppArmor profile on the host (e.g. apparmor_parser -r /path/to/my-profile)
  2. Create a privileged container with an explicit profile:
    docker create --privileged --security-opt apparmor=my-profile --name test alpine sleep infinity
    
  3. Start the container:
    docker start test
    
  4. Verify the profile is applied:
    docker inspect test --format '{{.AppArmorProfile}}'
    # should show: my-profile
    cat /proc/$(docker inspect test --format '{{.State.Pid}}')/attr/apparmor/current
    # should show: my-profile (enforce)
    
  5. Restart the container:
    docker restart test
    
  6. Verify the profile is still applied (this was the broken path):
    docker inspect test --format '{{.AppArmorProfile}}'
    # should show: my-profile (previously showed: unconfined)
    cat /proc/$(docker inspect test --format '{{.State.Pid}}')/attr/apparmor/current
    # should show: my-profile (enforce) (previously showed: unconfined)
    

Human readable description for the release notes

- Fixed privileged containers losing their explicit AppArmor profile (`--security-opt apparmor=<profile>`) after a container restart.

A picture of a cute animal (not mandatory but encouraged)

image

From: https://en.wikipedia.org/wiki/File:9-banded-armadillo.jpg

An armadillo — because it knows a thing or two about wearing proper armor at all times 🛡️

@github-actions github-actions Bot added the area/daemon Core Engine label Mar 25, 2026
agners added a commit to home-assistant/operating-system that referenced this pull request Mar 25, 2026
This adds two patches with fixes/improvements for the Docker engine

- `0001-daemon-respect-explicit-AppArmor-profile-on-privileg.patch`:
  Makes sure that AppArmor rules are always loaded, also on reboot. This
  is a long standing bug in Docker and affects Supervisor which is a
  privileged container with an AppArmor profile.
  Upstream PR: moby/moby#52215
- `0002-bridge-protect-bridge-subnet-from-direct-external-ac.patch`:
  Makes sure that the whole network (including gateway IP) of any Docker
  bridge network in NAT mode is firewalled from access from the outside.
  This essentially implements on Docker level what Supervisor applies on
  startup with home-assistant/supervisor#6650.
  Upstream PR: moby/moby#52224.
agners added a commit to home-assistant/operating-system that referenced this pull request Mar 25, 2026
This adds two patches with fixes/improvements for the Docker engine

- `0001-daemon-respect-explicit-AppArmor-profile-on-privileg.patch`:
  Makes sure that AppArmor rules are always loaded, also on reboot. This
  is a long standing bug in Docker and affects Supervisor which is a
  privileged container with an AppArmor profile.
  Upstream PR: moby/moby#52215
- `0002-bridge-protect-bridge-subnet-from-direct-external-ac.patch`:
  Makes sure that the whole network (including gateway IP) of any Docker
  bridge network in NAT mode is firewalled from access from the outside.
  This essentially implements on Docker level what Supervisor applies on
  startup with home-assistant/supervisor#6650.
  Upstream PR: moby/moby#52224.
@thaJeztah thaJeztah added this to the 29.3.2 milestone Mar 26, 2026
@thaJeztah thaJeztah added the kind/bugfix PR's that fix bugs label Mar 26, 2026
@thaJeztah
Copy link
Copy Markdown
Member

Thanks! I think this makes sense to do, but also recall I looked at this code (but maybe it was on exec) at some point, and there was some discussion around it.

That may have been on docker exec though, so let me try if I can find back that discussion!

(change LGTM, but just want to be sure there wasn't something ... somewhere ...)

Comment thread daemon/container_linux.go Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm... actually; looks like we DO care now? Can you double check this part?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, good point, I tested this in real world, so now was wondering how this worked 😅

So turns out that parseSecurityOpt reparses the security options (container.SecurityOptions, which contains the promoted field container.AppArmorProfile). I've adjusted this comment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! I checked out your branch, and ... YES, now I understand it as well. I'll probably have a look at cleaning up that logic;

  • Naming it parseSecurityOpt doesn't call out "has side-effects"
  • parseSecurityOpt looks to patch the container's config in-place, but can return early on errors, so now we may end up in some halfway state.

It'd probably be clearer to make that function return a fresh container.SecurityOptions, then if there's no errors, we update it on the container.

Either way; unrelated to this PR, so keeping that for a follow-up.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, and now that I think of it; I THINK this was one of the reasons I started to split these into separate structs .. three years ago (yikes! time flies);

sairon pushed a commit to home-assistant/operating-system that referenced this pull request Mar 30, 2026
This adds two patches with fixes/improvements for the Docker engine

- `0001-daemon-respect-explicit-AppArmor-profile-on-privileg.patch`:
  Makes sure that AppArmor rules are always loaded, also on reboot. This
  is a long standing bug in Docker and affects Supervisor which is a
  privileged container with an AppArmor profile.
  Upstream PR: moby/moby#52215
- `0002-bridge-protect-bridge-subnet-from-direct-external-ac.patch`:
  Makes sure that the whole network (including gateway IP) of any Docker
  bridge network in NAT mode is firewalled from access from the outside.
  This essentially implements on Docker level what Supervisor applies on
  startup with home-assistant/supervisor#6650.
  Upstream PR: moby/moby#52224.
sairon pushed a commit to home-assistant/operating-system that referenced this pull request Mar 31, 2026
This adds two patches with fixes/improvements for the Docker engine

- `0001-daemon-respect-explicit-AppArmor-profile-on-privileg.patch`:
  Makes sure that AppArmor rules are always loaded, also on reboot. This
  is a long standing bug in Docker and affects Supervisor which is a
  privileged container with an AppArmor profile.
  Upstream PR: moby/moby#52215
- `0002-bridge-protect-bridge-subnet-from-direct-external-ac.patch`:
  Makes sure that the whole network (including gateway IP) of any Docker
  bridge network in NAT mode is firewalled from access from the outside.
  This essentially implements on Docker level what Supervisor applies on
  startup with home-assistant/supervisor#6650.
  Upstream PR: moby/moby#52224.

(cherry picked from commit 50c1efd)
Copy link
Copy Markdown
Contributor

@vvoland vvoland left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@vvoland vvoland modified the milestones: 29.3.2, 29.4.0 Apr 1, 2026
@thaJeztah thaJeztah modified the milestones: 29.4.0, 29.3.2 Apr 1, 2026
In saveAppArmorConfig, the privileged check unconditionally overwrites
the AppArmor profile to "unconfined", even when the user explicitly set
a custom profile via --security-opt apparmor=<profile>. This causes
the persisted container.AppArmorProfile to be "unconfined" regardless
of user intent.

On first container start this is masked because createSpec/WithApparmor
runs before saveAppArmorConfig and builds the OCI spec from the
in-memory value (which still has the correct profile from creation).
But saveAppArmorConfig then overwrites it to "unconfined" and
CheckpointTo persists that to disk. On subsequent starts (restart,
stop+start, host reboot), the container loads with AppArmorProfile
"unconfined", and WithApparmor picks up that stale value — resulting
in the container running without its intended AppArmor policy.

Fix the condition nesting so that "unconfined" and "docker-default" are
only applied as defaults when no explicit profile was set via
SecurityOpt. This makes saveAppArmorConfig consistent with the existing
behavior in both WithApparmor (oci_linux.go) and execSetPlatformOpt
(exec_linux.go), which already give explicit profiles precedence over
the privileged default.

The bug was introduced in d97a00d (Docker 17.04, moby#27083) and survived
a refactor in 483aa62 (moby#43130).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Signed-off-by: Stefan Agner <[email protected]>
@thaJeztah thaJeztah force-pushed the fix-apparmor-profile-load-if-privileged branch from ec31d01 to 80c22f0 Compare April 2, 2026 14:28
@thaJeztah
Copy link
Copy Markdown
Member

Did a quick rebase, and squashed the commits; it's clearer to have the comment change in the same commit.

Copy link
Copy Markdown
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@thaJeztah thaJeztah merged commit bebd977 into moby:master Apr 2, 2026
49 checks passed
@agners
Copy link
Copy Markdown
Contributor Author

agners commented Apr 7, 2026

Thanks for the quick turnaround on this PR 🙏 , I see it will be part of Docker v29.4.0 🎉

Also a huge thank you to @donaldguy, he initially reported the issue (part of home-assistant/supervisor#4839) on our end, and also traced it back to the code lines in Docker in private conversations 🙏 .

@agners agners deleted the fix-apparmor-profile-load-if-privileged branch April 7, 2026 10:50
@thaJeztah
Copy link
Copy Markdown
Member

Thank you (both!) for contributing, and for linking some of the changes related!

The history definitely was helpful here, because there's definitely some "illogical" code paths / behavior in our code that was intentional, but for which the exact reason may not be well-documented, so it sometimes requires a good amount of archeology to verify if there was a specific reason.

In this case, that didn't appear to be, so changing looks like the right decision. That said; tens of millions of installs it's possible someone, somewhere depends on the behavior, but we'll see if someone did 😂

I see it will be part of Docker v29.4.0 🎉

Yup! and v29.4.0 should be available for install now on download.docker.com 👍

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

Projects

None yet

3 participants