Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Inconsistent behaviour when launching a privileged container with a custom apparmor profile #51242

@codragu-cisco

Description

@codragu-cisco

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

  1. 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.
  2. Use docker inspect on the created container. That output will show AppArmorProfile=my-profile, Privileged=True, and indicate that apparmor=my-profile under 'SecurityOpts`.
  3. Start the container using docker start.
  4. Run docker-inspect again- this will indicate that though the Privileged and SecurityOpts fields are the same, AppArmorProfile=unconfined now.
  5. 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.
  6. 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:

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/security/apparmorkind/bugBugs are bugs. The cause may or may not be known at triage time so debugging may be needed.status/0-triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions