-
Notifications
You must be signed in to change notification settings - Fork 18.8k
Description
Description
When I launch a container in --privileged mode with an apparmor profile specified, the subprocesses of that container run under the profile I specified. However when I restart the container, those processes then run in unconfined mode.
After some digging, I found that there is a bug in the code where when a custom apparmor profile passed through to docker run, it is applied to the config during the create stage, the container is then started under that profile during the start stage, and then the config is updated to use AppArmorProfile=unconfined upon exit of the start stage. This results in a privileged container with processes running under the apparmor profile passed through --security-opts, but docker inspect shows that AppArmorProfile=unconfined. This then produces inconsistent behaviour upon restart, as the container is restarted with the updated profile (i.e. unconfined) and this could potentially cause failures for containers that depend on the custom profile being passed.
Note: I can't run the container in --privileged mode without the custom profile. If --privileged mode results in the container subprocesses running in unconfined mode, those processes will be unconfined unless they match any of the already existing apparmor profiles on the host. In my case, they do, and those profiles happen to be less permissive than I need them to be. Therefore, by passing my own profile - which is as permissive as it can be as I am trying to emulate true unconfined mode - I can ensure that all my subprocesses run with my profile in an unconfined-like mode without being throttled by any existing profiles on the host.
Reproduce
- Create a container with
docker createand include the following CLI arguments:--privilegedand--security-opt apparmor=my-profile, wheremy-profileis a custom apparmor profile you create. - Use
docker inspecton the created container. That output will showAppArmorProfile=my-profile,Privileged=True, and indicate thatapparmor=my-profileunder 'SecurityOpts`. - Start the container using
docker start. - Run
docker-inspectagain- this will indicate that though thePrivilegedandSecurityOptsfields are the same,AppArmorProfile=unconfinednow. - However, when you run
aa-statusit shows that the processes spawned under the container are actually running under themy-profileprofile, notunconfined. - Restart the container using
docker restartand observe that the fields ofdocker inspectare the same, but the subprocesses are now running underunconfinedmode as peraa-status.
The way I actually discovered this bug was by running docker run with --privileged and --security-opt apparmor=my-profile where I know that the container would not be able to launch unless it did so under my custom apparmor profile. This caused the container to launch successfully, but it then failed to start again after I did a docker restart which prompted me to investigate the create and start flows and observe that the config changes during start but the container is still launched with the original create apparmor profile. The restart therefore did not work because it used the latest config which at that point would have had AppArmorProfile=unconfined.
Note that when following the same steps not under --privileged mode, the provided apparmor profile is preserved in the config during create, start, and restart.
Expected behavior
I believe the ideal expected behaviour should be that the provided apparmor profile takes precedence over --privileged and should be preserved in the config during start. The following reasons support my claim:
-
There are many raised issues where people are having problems with running their containers under AppArmor unconfined mode on privileged containers. This is largely due to the fact that
unconfinedmode is only for processes which don't match a profile already on the host (see my "Note" in the description section).- Allowing users to specify their own profile and pass it though on privileged mode means that users can create and use their own profiles that won't break their container functionality, as I am doing.
- This would be a good compromise until AppArmor comes up with a truly unconfined mode.
- Some of the issues I am referring to are:
-
I have tested that
podmandoes allow an apparmor profile to be used even when on privileged mode, so this would makedockermore consistent with that as well.
The alternative behaviour is that privileged mode should take precedence over any provided profile and the container should have been created with AppArmorProfile=unconfined from the beginning, however I believe that the aforementioned reasons provide clear evidence of why this alternative behaviour is not suitable.
docker version
Client: Docker Engine - Community
Version: 28.5.1
API version: 1.51
Go version: go1.24.8
Git commit: e180ab8
Built: Wed Oct 8 12:17:26 2025
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 28.5.1
API version: 1.51 (minimum version 1.24)
Go version: go1.24.8
Git commit: f8215cc
Built: Wed Oct 8 12:17:26 2025
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v1.7.28
GitCommit: b98a3aace656320842a23f4a392a33f46af97866
runc:
Version: 1.3.0
GitCommit: v1.3.0-0-g4ca628d1
docker-init:
Version: 0.19.0
GitCommit: de40ad0docker info
Relevant Section of docker info are:
Server Version: 28.5.1
Kernel Version: 6.8.0-51-generic
Operating System: Ubuntu 24.04.1 LTS
OSType: linux
Security Options:
apparmor
seccompAdditional Info
To fix the code in the way I suggested above (i.e. to not overwrite the provided apparmor profile), I believe the key area to look at is the following block, which is called after starting the container in daemon/start.go
if err := daemon.saveAppArmorConfig(container); err != nil {
return err
}
The saveAppArmorConfig from daemon/container_linux.go is what rewrites the currently set AppArmorProfile. It should include a check for if a profile is already suggested by the security options before setting the profile to be unconfined, similarly to in WithApparmor of daemon/oci_linux.go.
This is just what I noticed from sifting through the code- I am not familiar with the code base nor with go, so please do correct me if I am wrong!