mirror of
https://github.com/kerberos-io/agent.git
synced 2026-11-05 20:20:23 +00:00
Harden production Docker image and verify runtime permissions
Exclude local secrets and mutable data from image builds, restore Go checksum verification, remove runtime capability tooling and setuid/setgid permissions, and preserve non-root media and demo startup behavior. Add native image smoke checks and document runtime constraints. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot App
parent
8016b3ba93
commit
024bb0abf9
@@ -0,0 +1,27 @@
|
||||
# Only application sources are needed by the production build.
|
||||
**
|
||||
!Dockerfile
|
||||
!machinery/
|
||||
!machinery/**
|
||||
!ui/
|
||||
!ui/**
|
||||
|
||||
**/.git
|
||||
**/.env
|
||||
**/.env.*
|
||||
**/node_modules
|
||||
**/.DS_Store
|
||||
machinery/main
|
||||
machinery/vendor
|
||||
machinery/go.work
|
||||
machinery/go.work.sum
|
||||
machinery/www
|
||||
machinery/data/**
|
||||
!machinery/data/config/
|
||||
!machinery/data/config/config.json
|
||||
!machinery/data/config/user.json
|
||||
!machinery/data/test-480p.mp4
|
||||
ui/build
|
||||
ui/coverage
|
||||
ui/.npmrc
|
||||
ui/.yarnrc
|
||||
@@ -27,6 +27,8 @@ jobs:
|
||||
CID=$(docker create ${{matrix.architecture}})
|
||||
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
|
||||
docker rm ${CID}
|
||||
- name: Test production image
|
||||
run: sh deployments/docker/test-image.sh ${{ matrix.architecture }}
|
||||
- name: Strip binary
|
||||
run: tar -cf agent-${{matrix.architecture}}.tar -C output-${{matrix.architecture}} . && rm -rf output-${{matrix.architecture}}
|
||||
- name: Upload artifact
|
||||
|
||||
+20
-36
@@ -14,12 +14,11 @@ ARG TARGETARCH
|
||||
ENV GOROOT=/usr/local/go
|
||||
ENV GOPATH=/go
|
||||
ENV PATH=$GOPATH/bin:$GOROOT/bin:/usr/local/lib:$PATH
|
||||
ENV GOSUMDB=off
|
||||
|
||||
##########################################
|
||||
# Installing some additional dependencies.
|
||||
|
||||
RUN apt-get update && apt-get install -y --fix-missing --no-install-recommends \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git build-essential cmake pkg-config unzip libgtk2.0-dev \
|
||||
curl ca-certificates libavcodec-dev libavutil-dev libcurl4-openssl-dev \
|
||||
libssl-dev libjpeg62-turbo-dev libswscale-dev && \
|
||||
@@ -37,7 +36,6 @@ RUN go mod download
|
||||
# Copy the rest of the source after dependencies are primed.
|
||||
|
||||
COPY machinery ./
|
||||
RUN rm -rf .env
|
||||
|
||||
##################
|
||||
# Build Machinery
|
||||
@@ -52,6 +50,7 @@ RUN RESOLVED_VERSION="${VERSION:-0.0.0}" && \
|
||||
mv version /agent && \
|
||||
mv data /agent && \
|
||||
mkdir -p /agent/data/cloud && \
|
||||
mkdir -p /agent/data/tus && \
|
||||
mkdir -p /agent/data/snapshots && \
|
||||
mkdir -p /agent/data/log && \
|
||||
mkdir -p /agent/data/recordings && \
|
||||
@@ -96,42 +95,27 @@ RUN mkdir -p ./agent && cp -r /go/src/github.com/kerberos-io/agent/machinery/www
|
||||
|
||||
FROM ${RUNTIME_IMAGE}
|
||||
|
||||
############################
|
||||
# Protect by non-root user.
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl ffmpeg libatomic1 libcap2-bin libstdc++6 && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
groupadd --system kerberosio && \
|
||||
useradd --system --gid kerberosio --groups video --create-home agent && \
|
||||
chmod 0755 /home/agent
|
||||
|
||||
#################################
|
||||
# Copy files from previous images
|
||||
|
||||
COPY --chown=0:0 --from=build-machinery /dist /
|
||||
COPY --chown=0:0 --from=build-ui /dist /
|
||||
COPY --chown=0:0 --from=build-machinery /dist/agent/ /home/agent/
|
||||
COPY --chown=0:0 --from=build-ui /dist/agent/ /home/agent/
|
||||
|
||||
##################
|
||||
# Try running agent
|
||||
|
||||
RUN mv /agent/* /home/agent/
|
||||
RUN /home/agent/main version
|
||||
|
||||
#######################
|
||||
# Make template config
|
||||
|
||||
RUN cp /home/agent/data/config/config.json /home/agent/data/config.template.json
|
||||
|
||||
###########################
|
||||
# Set permissions correctly
|
||||
|
||||
RUN chown -R agent:kerberosio /home/agent/data
|
||||
RUN chown -R agent:kerberosio /home/agent/www
|
||||
|
||||
###########################
|
||||
# Grant the necessary root capabilities to the process trying to bind to the privileged port
|
||||
RUN setcap 'cap_net_bind_service=+ep' /home/agent/main
|
||||
# Keep media support and the health probe, but not privilege-management tools.
|
||||
# UID/GID match the existing trixie image; only mutable application data is owned by agent.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl ffmpeg libatomic1 libcap2-bin libstdc++6 && \
|
||||
groupadd --system --gid 999 kerberosio && \
|
||||
useradd --system --uid 999 --gid kerberosio --groups video \
|
||||
--home-dir /home/agent --no-create-home agent && \
|
||||
chmod 0755 /home/agent /home/agent/main && \
|
||||
cp /home/agent/data/config/config.json /home/agent/data/config.template.json && \
|
||||
chown -R agent:kerberosio /home/agent/data /home/agent/www && \
|
||||
setcap 'cap_net_bind_service=+ep' /home/agent/main && \
|
||||
apt-get purge -y --auto-remove libcap2-bin && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
find / -xdev -type f -perm /6000 -exec chmod a-s {} + && \
|
||||
/home/agent/main version
|
||||
|
||||
###################
|
||||
# Run non-root user
|
||||
@@ -146,7 +130,7 @@ EXPOSE 80
|
||||
######################################
|
||||
# Check if agent is still running
|
||||
|
||||
HEALTHCHECK CMD curl --fail "http://localhost:${AGENT_PORT:-80}/health" || exit 1
|
||||
HEALTHCHECK --timeout=10s CMD curl --noproxy '*' --fail --silent --show-error --max-time 5 "http://localhost:${AGENT_PORT:-80}/health" || exit 1
|
||||
|
||||
###################################################
|
||||
# Leeeeettttt'ssss goooooo!!!
|
||||
|
||||
@@ -228,8 +228,13 @@ You attach a volume to your container by leveraging the `-v` option. To mount yo
|
||||
|
||||
More examples for each deployment and automation tool [can be found in the deployment section](https://github.com/kerberos-io/agent/tree/master/deployments). Be sure to verify the permissions of the directory/volume you are attaching. More information in [this issue](https://github.com/kerberos-io/agent/issues/80).
|
||||
|
||||
chmod -R 755 kerberos-agent/
|
||||
chown 100:101 kerberos-agent/ -R
|
||||
The current image runs as UID `999`, GID `999`. Give that user ownership of
|
||||
the mounted directories (older image tags may use different IDs):
|
||||
|
||||
sudo chown -R 999:999 agent/config agent/recordings
|
||||
|
||||
See [container permissions and hardening](deployments/docker/README.md#container-permissions-and-hardening)
|
||||
for reduced-capability deployment and migration guidance.
|
||||
|
||||
## Configure with environment variables
|
||||
|
||||
|
||||
@@ -16,6 +16,77 @@ Feel free to use another port if your host system already has a workload running
|
||||
|
||||
docker run -p 8082:80 --name mycamera -d kerberos/agent:latest
|
||||
|
||||
### Container permissions and hardening
|
||||
|
||||
The production image runs as `agent` (UID `999`, primary GID `999`), with the
|
||||
supplementary `video` group retained. The binary and home directory are owned by
|
||||
root; within `/home/agent`, only `data/` and `www/` are writable by the Agent. `www/` must remain writable
|
||||
because demo mode replaces `env.js` at startup. Existing `/home/agent/data/...`
|
||||
mount paths, the default port `80`, and the startup command are unchanged.
|
||||
|
||||
FFmpeg is required for AAC-to-PCMU audio transcoding, curl for the health check,
|
||||
and `/bin/sh` for remote-access sessions. They are intentionally retained. The
|
||||
capability-management tools and setuid/setgid permissions on bundled utilities
|
||||
are removed. The Agent binary retains only the file capability
|
||||
`NET_BIND_SERVICE`, needed for port `80` on hosts with privileged low ports.
|
||||
For an RTSP-only deployment, you can drop the other container capabilities:
|
||||
|
||||
```sh
|
||||
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
|
||||
-p 80:80 --name mycamera -d kerberos/agent:latest
|
||||
```
|
||||
|
||||
Do not use `--privileged` for ordinary IP cameras. Local cameras still use the
|
||||
separate RTSP sidecar; any device access belongs to that sidecar. Host networking
|
||||
remains supported. `no-new-privileges` prevents gaining the binary's file
|
||||
capability: if enabling it, use an unprivileged internal port (`AGENT_PORT=8082`)
|
||||
and ensure the container's `net.ipv4.ip_unprivileged_port_start` permits that
|
||||
port. Do not assume port `80` works with that option on every host.
|
||||
|
||||
Bind mounts must be writable by the image's UID/GID; the container does not
|
||||
change host ownership at startup. Before migrating an older volume, check the
|
||||
user of the exact old and new image with `docker run --rm --entrypoint id IMAGE`
|
||||
and adjust only the mounted directories as necessary. For example:
|
||||
|
||||
```sh
|
||||
sudo chown -R 999:999 agent/config agent/recordings
|
||||
```
|
||||
|
||||
A read-only root filesystem is not enabled by default: both `data/` and `www/`
|
||||
need writable storage (and FFmpeg or shell sessions may need `/tmp`).
|
||||
Change the default application credentials before exposing the service.
|
||||
|
||||
### Building and checking the image
|
||||
|
||||
Build from a clean checkout: the default configuration files under
|
||||
`machinery/data/config/` are included in the image, so do not replace them with
|
||||
production credentials before building. `.dockerignore` excludes local `.env`
|
||||
files, recordings, upload checkpoints, dependency directories, and unrelated
|
||||
repository files from the build context. Go module downloads use Go's default
|
||||
checksum database verification.
|
||||
|
||||
```sh
|
||||
docker build --pull -t kerberos/agent:local .
|
||||
sh deployments/docker/test-image.sh kerberos/agent:local
|
||||
```
|
||||
|
||||
The smoke check uses temporary containers and a Docker volume, cleans them up,
|
||||
and checks permissions, FFmpeg transcoding, volume initialization/writes,
|
||||
default and custom HTTP ports, health probes, and demo-mode startup. It enforces
|
||||
privileged low ports to check the file capability rather than relying on
|
||||
Docker's usual low-port exemption, and checks `no-new-privileges` on port `8082`. PR builds
|
||||
run it on native AMD64 and ARM64 runners. It does not replace testing against a
|
||||
real camera or checking all supported ARM variants. Base image tags remain
|
||||
updateable; use reviewed digest-pinned base-image arguments (`GO_IMAGE` and
|
||||
`RUNTIME_IMAGE`) when a reproducible build is required, and rebuild regularly
|
||||
for security updates.
|
||||
|
||||
Architecture selections and base image tags are unchanged by this hardening.
|
||||
The current upstream `golang:1.25-trixie` manifest has no `linux/arm/v6` variant,
|
||||
so the existing ARMv6 nightly target cannot be validated with that default base.
|
||||
ARMv6 needs a separately reviewed compatible builder/runtime combination; do
|
||||
not assume an ARMv7 image runs on ARMv6 hardware.
|
||||
|
||||
### Attach a volume
|
||||
|
||||
By default your Kerberos agent will store all its configuration and recordings inside the container. It might be interesting to store both configuration and your recordings outside the container, on your local disk. This helps persisting your storage even after you decide to wipe out your Kerberos agent.
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
image=${1:?Usage: sh deployments/docker/test-image.sh IMAGE}
|
||||
container=
|
||||
volume=
|
||||
cleanup() {
|
||||
if [ -n "$container" ]; then docker rm -f "$container" >/dev/null; fi
|
||||
if [ -n "$volume" ]; then docker volume rm "$volume" >/dev/null; fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 1' HUP INT TERM
|
||||
|
||||
docker run --rm --entrypoint /bin/sh "$image" -ec '
|
||||
test "$(id -u)" = 999
|
||||
test "$(id -g)" = 999
|
||||
test ! -w /home/agent
|
||||
test ! -w /home/agent/main
|
||||
test -w /home/agent/data/config
|
||||
test -w /home/agent/data/recordings
|
||||
test -w /home/agent/data/tus
|
||||
test -w /home/agent/www
|
||||
test -z "$(find / -xdev -type f -perm /6000 -print 2>/dev/null)"
|
||||
! command -v setcap
|
||||
! command -v capsh
|
||||
test ! -e /home/agent/.env
|
||||
test -z "$(find /home/agent/data/tus -type f -print)"
|
||||
./main -action version
|
||||
ffmpeg -nostdin -v error -f lavfi -i sine=frequency=1000:sample_rate=8000 \
|
||||
-t 0.1 -c:a aac -f adts /tmp/test.aac
|
||||
ffmpeg -nostdin -v error -i /tmp/test.aac -ar 8000 -ac 1 \
|
||||
-c:a pcm_mulaw -f mulaw /tmp/test.pcm
|
||||
test -s /tmp/test.pcm
|
||||
'
|
||||
|
||||
wait_healthy() {
|
||||
attempt=0
|
||||
while [ "$attempt" -lt 60 ]; do
|
||||
status=$(docker inspect --format '{{.State.Health.Status}}' "$container")
|
||||
if [ "$status" = healthy ]; then return; fi
|
||||
if [ "$(docker inspect --format '{{.State.Running}}' "$container")" != true ]; then
|
||||
break
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
docker logs "$container"
|
||||
docker inspect --format '{{json .State.Health}}' "$container"
|
||||
echo "Agent did not become healthy" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Exercise the default port, Docker volume initialization, and reduced capabilities.
|
||||
volume=$(docker volume create)
|
||||
container=$(docker run -d --cap-drop ALL --cap-add NET_BIND_SERVICE \
|
||||
--sysctl net.ipv4.ip_unprivileged_port_start=1024 \
|
||||
--health-interval=1s --health-start-period=0s \
|
||||
--mount "type=volume,source=$volume,target=/home/agent/data" "$image")
|
||||
wait_healthy
|
||||
docker exec "$container" sh -ec '
|
||||
test -r data/config/config.json
|
||||
touch data/config/.permission-test data/recordings/.permission-test data/tus/.permission-test
|
||||
rm data/config/.permission-test data/recordings/.permission-test data/tus/.permission-test
|
||||
'
|
||||
docker stop --time 10 "$container" >/dev/null
|
||||
docker rm "$container" >/dev/null
|
||||
container=
|
||||
|
||||
# Demo mode must still be able to replace env.js; probes must bypass proxy settings.
|
||||
container=$(docker run -d --cap-drop ALL --cap-add NET_BIND_SERVICE \
|
||||
--security-opt no-new-privileges=true \
|
||||
--sysctl net.ipv4.ip_unprivileged_port_start=1024 \
|
||||
--health-interval=1s --health-start-period=0s \
|
||||
-e AGENT_PORT=8082 -e AGENT_MODE=demo \
|
||||
-e http_proxy=http://127.0.0.1:9 -e HTTP_PROXY=http://127.0.0.1:9 \
|
||||
-e no_proxy= -e NO_PROXY= "$image")
|
||||
wait_healthy
|
||||
docker exec "$container" sh -ec 'test -f www/env.js; test ! -e www/env.demo.js'
|
||||
echo "Image permissions, media transcoding, volume writes, default/custom ports, and demo startup passed."
|
||||
Reference in New Issue
Block a user