Device Protection in Containers
Device Protection can run inside a Linux container image, so package installs in CI jobs, build containers, and containerized dev environments get inspected the same way they do on a workstation. You install the agent while the image is built and let the agent's container entrypoint start it when the container runs.
Add Device Protection to an image
The build has to run as root, and the agent's own installer expects curl and ca-certificates, which Debian slim images leave out.
The install script picks the package for the image's distribution and architecture, verifies its checksum, and installs it. It needs the token at build time.
FROM node:20-bookworm-slim
ARG AIKIDO_TOKEN
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& curl -fsSL https://github.com/AikidoSec/safe-chain/raw/refs/heads/main/install-scripts/install-endpoint-linux.sh \
| sh -s -- --token "$AIKIDO_TOKEN" --container \
&& rm -rf /var/lib/apt/lists/*
STOPSIGNAL SIGTERM
ENTRYPOINT ["aikido-endpoint-protection-container-entrypoint"]
# Your own command. This one downloads a package Aikido blocks, so it prints
# 403 while protection is working.
CMD ["curl", "-sS", "-o", "/dev/null", "-w", "%{http_code}\n", "--max-time", "15", \
"https://registry.npmjs.org/safe-chain-test/-/safe-chain-test-0.0.1-security.tgz"]Then build and run it:
docker build -t my-app --build-arg AIKIDO_TOKEN=<your-token> .
docker run --rm --init my-app--build-arg AIKIDO_TOKEN=<your-token> is not optional: the build cannot see your shell environment, and the installer stops with an error when the token is empty. To pass a variable you already have, give Docker the bare name instead, --build-arg AIKIDO_TOKEN.
Installing the package yourself lets you pin a version, install from an internal mirror, and build an image that has no token in it at all. Set the container-mode variables in the environment of the package manager, and set AIKIDO_TOKEN only if you want the token baked in.
Then build the image and pass the token when you run it:
Set both variable names. AIKIDO_CONTAINER is the current one, and AIKIDO_CI_CD is the older name that packages up to 1.8.2 read, so setting both works whichever package you install.
Use the package that matches the image, not your laptop: EndpointProtection-arm64.deb for arm64 images, and the el9 or el10 RPM for Red Hat-family base images. Install Device Protection for Linux lists all of them.
Where the token comes from
The agent takes the token at install time or at run time, and the difference is where the token ends up.
--build-arg AIKIDO_TOKEN=<your-token>
The image, and its build history. Anyone who can pull the image can read it
docker run -e AIKIDO_TOKEN=<your-token>
Only the running container. The entrypoint hands the token to the agent as it starts
Prefer the run-time token for images you push to a registry, and keep the build-time token for images that never leave the machine that built them. BuildKit warns about the build-time form (SecretsUsedInArgOrEnv) for exactly this reason.
A token passed at run time only applies to an image that was built without one. Once a token is baked in, that is the one the agent uses.
What --container changes
--container (or the AIKIDO_CONTAINER=1 and AIKIDO_CI_CD=1 variables when you install the package yourself) is what makes the agent work in a container:
The agent runs headless. No desktop UI and no tray icon.
The proxy keeps its CA in memory instead of the OS keyring, which containers and CI runners do not have.
Traffic goes through an explicit proxy on
127.0.0.1:7654rather than the transparent eBPF datapath used on workstations. That means the container needs no--privilegedand no added capabilities.Desktop dependencies are skipped. The install passes
--no-install-recommends, so GTK, WebKit, and the tray extensions stay out of the image.systemdandca-certificatesare hard dependencies and still get installed.
--ci-cd was the earlier name for this flag. The install script still accepts it, so existing Dockerfiles keep working, but use --container in anything you write now.
What the entrypoint does
aikido-endpoint-protection-container-entrypoint is a small supervisor, not a wrapper that hands off and exits:
Stages the token from
AIKIDO_TOKENwhen one is set in the container's environment.Starts the agent without systemd and waits up to 20 seconds for it to report healthy. If it never does, the container exits with an error instead of running your command unprotected.
Exports the proxy environment variables and runs your
CMDas a child process.On
SIGTERMit stops the agent, then your command, and exits143. It also handlesSIGRTMIN+3, which systemd-based images send ondocker stop.Exits with your command's exit code.
Because it stays PID 1 to receive that signal, it does not reap orphaned processes. --init (or tini) covers that, and is worth adding.
If your CMD is systemd itself, the entrypoint execs it and leaves the unit to start the agent.
Point Node.js and uv at the Aikido CA
The agent inspects HTTPS with a local CA. Tools that read the system trust store pick it up on their own, but Node.js and uv carry their own, so the agent writes the variables that point them at the CA into /etc/profile.d/. The entrypoint sources only the proxy settings from there, so a CMD that runs npm fails with SELF_SIGNED_CERT_IN_CHAIN until you source the rest.
Source every file the agent drops before your command runs:
The agent writes those files while it starts, so read them at run time rather than copying the paths into ENV at build time.
Inspect installs during the image build
The agent only runs while the container runs, so a RUN npm ci in your Dockerfile downloads packages uninspected. To cover the build too, start the agent inside the RUN step:
The agent needs a token while it runs, so this only works in an image built with the token baked in.
Job containers in GitHub Actions
jobs.<id>.container replaces the image's ENTRYPOINT, so the entrypoint never runs. Start the agent in the job's first step instead:
Each step runs in its own shell, so source /etc/profile.d/aikido-*.sh in every step that installs packages.
How containers show up in your device list
On Linux, device identity follows /etc/machine-id, and that file is created while the image is built. Every container started from one image therefore reports as the same device, no matter what its hostname is, and rebuilding the image creates a new device.
That is usually what you want for CI: one device per image, not one per job. To split them, give each container its own machine ID:
Verify it works
Run the image with the CMD above. A working container prints:
The 403 is the agent refusing the malware test package. Swap the URL for a package that is not blocked and the same command prints 200, which tells you the proxy is in the path rather than simply breaking downloads.
Inside a running container, aikido-doctor version reports the installed version and the agent's state.
Troubleshooting
exec aikido-endpoint-protection-container-entrypoint failed: No such file or directory
The image has an agent that predates container support. Install a current package
Error: daemon started but never became healthy
The container has no token, or the agent was installed without --container. Pass a token with -e AIKIDO_TOKEN=<your-token>, and check that the image installed the agent in container mode
npm error SELF_SIGNED_CERT_IN_CHAIN, or the same from uv
The Aikido CA variables never reached the process. Source /etc/profile.d/aikido-*.sh before the command that installs packages
Packages are blocked as expected, but the container never appears in your device list
The token was rejected. Malware blocking keeps working on a rejected token, so check the token itself, and that the container can reach *.aikido.dev over HTTPS
Downloads are not inspected at all
Your command bypassed the entrypoint, for example a docker run that overrides it or a job container that replaces it. Start the agent explicitly with aikido-endpoint-protection-start --wait-for-healthy
docker stop takes the full grace period and kills the container
Something between the entrypoint and your process is swallowing SIGTERM. Run the image without a shell wrapper, or add --init
Last updated
Was this helpful?