Hardening¶
The image runs as root and uses a writable root filesystem by default. That is a deliberate trade-off; this page explains why and what you can still tighten at the orchestration layer.
Why root, FUSE, NFS¶
- Cron-as-root: busybox
crondwrites to/var/spool/cron/crontabs/rootand reads/etc/crontabs/root; restoring snapshots commonly needs root to recreate UIDs/GIDs and ACLs faithfully. - FUSE (
restic mount): requiresCAP_SYS_ADMINand access to/dev/fuse; that capability is meaningless without an effective UID 0 inside the container. - NFS (
NFS_TARGET): themountsyscall in busybox needsCAP_SYS_ADMIN; the same constraint as FUSE. - Hooks: user-supplied
/hooks/*.shscripts may need to read source files under tight ACLs; running as a non-root UID would break some perfectly reasonable backup setups. - Restic backends: most cloud backends (
s3:,swift:,rclone:) work fine as non-root, butsftp:plus~/.sshmounts and local repository mounts under/mnt/restictypically end up needing root.
A separate "slim" image (no FUSE, no NFS, no cron, runs as UID 1000) is on the backlog for users who can accept those trade-offs. The default image keeps the boring, batteries-included behaviour.
What you can tighten outside the image¶
Cap the blast radius outside the image — Docker / Compose / Kubernetes are the right place for these knobs.
Drop most kernel capabilities¶
cap_drop:
- ALL
cap_add:
- DAC_READ_SEARCH # source paths under tight ACLs
- SYS_ADMIN # FUSE / NFS mount / restic mount
The default Docker capability set is broader than the image needs. Drop everything and re-add only what FUSE/NFS / strict ACL reads actually require.
Read-only root filesystem¶
read_only: true
tmpfs:
- /tmp
- /run
- /var/run
- /var/spool/cron # crond writes the rendered crontab here
- /var/log # last-*.json + cron.log; switch to a named volume to persist
- /.cache/restic # restic cache; mount a volume to keep it across restarts
Trade-offs to know:
/var/logas tmpfs meanslast-*.json,cron.logarchives and*.promfiles are lost on container restart. Switch that one to a named volume if you scrape it externally (Prometheus textfile collector, log forwarder)./.cache/resticas tmpfs means every restart re-warms the restic cache (slower first backup after restart). A named volume is recommended for any non-trivial repository.
No new privileges¶
Read-only source mounts¶
:ro ensures a hostile hook script cannot mutate the backup source.
Seccomp / AppArmor¶
The upstream Docker default profiles already block the riskiest syscalls; an explicit profile path can tighten further but is environment-specific. Start with the default and only add a custom profile when you have evidence you need to.
Kubernetes securityContext¶
securityContext:
runAsUser: 0
runAsGroup: 0
capabilities:
drop: [ALL]
add: [DAC_READ_SEARCH, SYS_ADMIN]
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
Combine with emptyDir mounts for /tmp, /var/spool/cron,
/var/log (or PVC for persistence) and /.cache/restic.
The included Kubernetes manifest already drops all
capabilities and re-adds only DAC_READ_SEARCH + SYS_ADMIN.
What you should not tighten¶
- Don't try to run as a non-root UID inside the image. The cron
daemon writes to
/var/spool/cron/crontabs/root; restoringchown -Rownership commonly needs UID 0. Use the orchestration-layer knobs above instead. - Don't strip
SYS_ADMINif you use FUSE (restic mount) or NFS (NFS_TARGET). The image will fall over in confusing ways. - Don't drop
DAC_READ_SEARCHif your backup source has tight ACLs that prevent root from reading without it.
TL;DR¶
Don't try to make the image non-root or read-only inside the
container — tighten it at the orchestration layer with cap_drop,
read_only + tmpfs, :ro source mounts and no-new-privileges. You
keep the well-tested cron / FUSE / NFS behaviour and still meet most
CIS-style benchmarks.
See also¶
- Docker Compose — reference Compose stack.
- Kubernetes — reference manifest.
- Security — secret handling and credential masking.