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 create and include the following CLI arguments: --privileged and --security-opt apparmor=my-profile, where my-profile is a custom apparmor profile you create.
- Use
docker inspect on the created container. That output will show AppArmorProfile=my-profile, Privileged=True, and indicate that apparmor=my-profile under 'SecurityOpts`.
- Start the container using
docker start.
- Run
docker-inspect again- this will indicate that though the Privileged and SecurityOpts fields are the same, AppArmorProfile=unconfined now.
- However, when you run
aa-status it shows that the processes spawned under the container are actually running under the my-profile profile, not unconfined.
- Restart the container using
docker restart and observe that the fields of docker inspect are the same, but the subprocesses are now running under unconfined mode as per aa-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 unconfined mode 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 podman does allow an apparmor profile to be used even when on privileged mode, so this would make docker more 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: de40ad0
docker 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
seccomp
Additional 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!
Description
When I launch a container in
--privilegedmode 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 inunconfinedmode.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 thecreatestage, the container is then started under that profile during thestartstage, and then the config is updated to useAppArmorProfile=unconfinedupon exit of thestartstage. This results in a privileged container with processes running under the apparmor profile passed through--security-opts, butdocker inspectshows thatAppArmorProfile=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
--privilegedmode without the custom profile. If--privilegedmode results in the container subprocesses running inunconfinedmode, those processes will beunconfinedunless 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
docker createand include the following CLI arguments:--privilegedand--security-opt apparmor=my-profile, wheremy-profileis a custom apparmor profile you create.docker inspecton the created container. That output will showAppArmorProfile=my-profile,Privileged=True, and indicate thatapparmor=my-profileunder 'SecurityOpts`.docker start.docker-inspectagain- this will indicate that though thePrivilegedandSecurityOptsfields are the same,AppArmorProfile=unconfinednow.aa-statusit shows that the processes spawned under the container are actually running under themy-profileprofile, notunconfined.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 runwith--privilegedand--security-opt apparmor=my-profilewhere 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 adocker restartwhich prompted me to investigate thecreateandstartflows and observe that the config changes duringstartbut the container is still launched with the originalcreateapparmor profile. The restart therefore did not work because it used the latest config which at that point would have hadAppArmorProfile=unconfined.Note that when following the same steps not under
--privilegedmode, the provided apparmor profile is preserved in the config duringcreate,start, andrestart.Expected behavior
I believe the ideal expected behaviour should be that the provided apparmor profile takes precedence over
--privilegedand should be preserved in the config duringstart. 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).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=unconfinedfrom 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
Additional 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.goThe
saveAppArmorConfigfromdaemon/container_linux.gois 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 inWithApparmorofdaemon/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!