Repository navigation
[BUG] Certificates in /app/certs are never imported for the default non-root container user #285
Description
Summary
The container entrypoint imports custom CA certificates from /app/certs only when it runs as root or when the system trust store is writable. The image runs as UID 64000 / GID 0 and leaves the Alpine trust store owned by root and not group-writable, so for the default user the import is always skipped with a message. Operators who mount an internal CA (for a private registry, a Trivy server, Portainer, a TLS Docker endpoint or an OTLP collector) get TLS validation failures unless they run the container as root, which defeats the non-root hardening.
Area
docs/areas/hosting.md(added in [Docs] Describe the current behavior by area #263) — Container image, Open question 3- Security area (
.squad/project.md): Container image and runtime user (src/DockerUpdateGuard/Dockerfile, non-root UID 64000, GID 0); also affects which certificates outbound TLS trusts (TLS and transport relaxations).
Current behavior
src/DockerUpdateGuard/Dockerfile:51installsca-certificatesas root;:68-73changes ownership only of/appand then switches toUSER 64000:0. Nothing changes the ownership or mode of/usr/local/share/ca-certificatesor/etc/ssl/certs, which stayroot:rootand not group-writable (the comment at:66-67already says the import "degrades gracefully when the trust store is not writable").src/DockerUpdateGuard/entrypoint.sh:104-129:
# Only attempt to import when running as root or when trust store is writable
if [ "$(id -u)" = "0" ] || [ -w /usr/local/share/ca-certificates ] ; then
...
update-ca-certificates || echo "Warning: update-ca-certificates failed"
else
echo "No permission to write system trust store; skipping certificate import"
fi- Even if
/usr/local/share/ca-certificateswere writable,update-ca-certificatesmust also write/etc/ssl/certs/ca-certificates.crt(and links in/etc/ssl/certs), which are root-owned too.
Reproduction:
docker runthe published image with-v ./my-ca:/app/certs:ro(containingmy-root-ca.crt) and the default user.- The log shows
Found certificate files in /app/certs; attempting import into system trust store...followed byNo permission to write system trust store; skipping certificate import. - Any outbound HTTPS call to a host signed by that CA (e.g. a private registry,
TrivyBaseUrl, a PortainerBaseUrl, a TLSOtlpEndpoint) fails certificate validation.
(Verified by reading the Dockerfile and entrypoint; a container runtime was not available to run the image during analysis — the acceptance criteria require verifying it in the built image.)
Expected behavior
- With the default user (UID
64000, GID0) — and with any other UID in group0— every.crtand.pemfile in/app/certsis trusted for outbound TLS by the application (.NETHttpClient) and by the bundled Trivy CLI, in addition to the image's system CAs. - The container still never runs as UID
0; the import does not require root. - The import still never stops the container: missing directory, no files, unreadable or invalid files produce a message and the container starts.
- The system CAs remain trusted; mounted certificates add to them and never replace them.
- When the container runs as root, behavior is equivalent (certificates trusted).
Acceptance criteria
- Built image, default user,
/app/certswith one.crtand one.pem: the entrypoint reports the certificates as imported (no "skipping" message) and an HTTPS request from the app to a server signed by that CA succeeds (manual verification documented in the PR, e.g. against a local test server). - Same with
--user 12345:0(arbitrary UID in group 0). - Same with the image started as root.
- No
/app/certs/ empty/app/certs/ a non-certificate file: container starts, message printed, system CAs unaffected. - The Trivy CLI (
/usr/local/bin/trivy, client mode against a server signed by the mounted CA) trusts the mounted CA. - The image still runs as
USER 64000:0; no new write permission for group0outside what the chosen option below needs. - If any C# is touched, unit tests per
docs/UNIT_TESTS.md; the shell change itself is verified manually (there is no shell test harness in this repository). -
INSTALL.md/README document mounting/app/certsfor the default user. - The area document
docs/areas/hosting.mddescribes the new behavior and its Known gaps entry for this issue is removed;docs/ARCHITECTURE.md(Security posture) is updated if it mentions the import.
Implementation notes
Two options:
- Make the trust store group-writable in the Dockerfile (
chgrp -R 0 /usr/local/share/ca-certificates /etc/ssl/certs && chmod -R g=u …) and keepupdate-ca-certificatesin the entrypoint. Simple, but widens write access to the system trust store for group0inside the container and does not work with a read-only root filesystem. - Build a combined bundle in a writable location and point the runtime at it (recommended). In
entrypoint.sh, when/app/certscontains.crt/.pemfiles: create a directory under/tmp(already used for the Trivy cache, writable for any UID), write/etc/ssl/certs/ca-certificates.crtplus the mounted certificates into one bundle there, andexport SSL_CERT_FILE=<bundle>beforeexec dotnet …. .NET on Linux (OpenSSL) and Go programs such as Trivy honorSSL_CERT_FILE. No root, no change to the system trust store, works with a read-only root filesystem when/tmpis a tmpfs. Keep the root path identical (no special case needed).
- Validate each file minimally before adding it (e.g. it contains a
-----BEGIN CERTIFICATE-----block); skip others with a message, as today for non-certificate files. - Do not let a pre-set
SSL_CERT_FILEfrom the operator be silently overwritten: if it is already set, append to/derive from that file or print a message and leave it. - Keep the readability check for
appsettings*.json(entrypoint.sh:85-94) unchanged. - Update the Dockerfile comment at
:62-67. - Must not change: non-root UID
64000, GID0,/appownership model, exit code1only for unreadable settings files. - No configuration keys, no migration.
Related
- PR [Docs] Describe the current behavior by area #263 (area documents)
- Support private registries in the Trivy vulnerability provider and document credential handling #171 (Trivy private registries) — private registries often use internal CAs; this fix is a prerequisite for that scenario with the default user.
Generated by Claude Code
Activity
LarsLaskowski commentedon Oct 26, 2026
Squad working record
Tier:
security. Plan: https://github.com/LarsLaskowski/DockerUpdateGuard/blob/3f6ebb537c4a930868f109acbdacb516ac67c64b/specs/issue-285/plan.md (revision 1; decision record 0011Mounted CA certificates are trusted through a private SSL_CERT_DIR). Pull request: #335. Squad lesson: #336.Summary: The entrypoint writes every accepted PEM certificate block from
/app/certsinto its own file in a private0700directory under/tmpand appends that directory toSSL_CERT_DIR(/etc/ssl/certsor the operator's value first). This differs from the issue's option 2 (combinedSSL_CERT_FILEbundle): Security measured that .NET 10 stops reading a bundle at the first undecodable block and then ignores every later CA, while a directory isolates a bad file. No root, no trust-store write, read-only root filesystem works with a tmpfs/tmp. Plan challenge: 4 minor objections, all taken in; Security plan review: 2 blocking findings, resolved in one revision and confirmed. Diff review: Reviewer 0 blocking (1 non-blocking doc fix applied), Security diff review APPROVED.log.md
Squad run metrics
Generated by Claude Code