Local certs in alpine image leads to certutil warning

1. The problem I’m having:

When specifying local_certs in global options, and while running in alpine, I get a warning that the root cert can’t be installed. I presume this is happening when the local un-trusted CA is created and Caddy (smallstep/truststore?) tries to inject the CA into the system trust.

Neither package is listed in Alpine. I found nss, installed it, and I still get the same error. I looked into the caddy and truststore code, it seems this log message prints when it tries to install the CA certificate into the mozilla profiles, which are almost definitely not part of an alpine container.

Looking through the code, I don’t see any way to conditionally skip this step.

I’d like to eliminate the warning.

2. Error messages and/or full log output:

{"level":"warn","ts":1746819871.0233943,"logger":"pki.ca.local","msg":"installing root certificate (you might be prompted for password)","path":"storage:pki/authorities/local/root.crt"}
2025-05-09T19:44:31.024384967Z {"level":"info","ts":1746819871.0243294,"msg":"warning: \"certutil\" is not available, install \"certutil\" with \"apt install libnss3-tools\" or \"yum install nss-tools\" and try again"}

3. Caddy version:

Caddy version v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=

4. How I installed and ran Caddy:

FROM caddy:2.10.0-builder AS builder

RUN xcaddy build \
    --with github.com/caddyserver/cache-handler \
    --with github.com/darkweak/storages/badger/caddy \
    --with github.com/gamalan/caddy-tlsredis@master


# re-create caddy dockerfile https://github.com/caddyserver/caddy-docker/tree/master
FROM alpine:3.21.3

RUN apk add --no-cache \
	ca-certificates \
	libcap \
	mailcap \
    curl

RUN set -eux; \
	mkdir -p \
		/config/caddy \
		/data/caddy \
		/etc/caddy \
		/usr/share/caddy \
	;

# https://github.com/caddyserver/caddy/releases
ENV CADDY_VERSION v2.10.0

# See https://caddyserver.com/docs/conventions#file-locations for details
ENV XDG_CONFIG_HOME /config
ENV XDG_DATA_HOME /data

VOLUME /config
VOLUME /data

EXPOSE 80
EXPOSE 443
EXPOSE 443/udp
EXPOSE 2019

WORKDIR /srv

#HEALTHCHECK CMD curl -f http://localhost/ || exit 1

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

# Update mime types
COPY ./build/configs/mime.types /tmp/mime.types.tmp
RUN cat /tmp/mime.types.tmp >> /etc/mime.types

COPY ./build/configs/bootstrap.sh /bootstrap.sh
CMD ["sh", "/bootstrap.sh"]

a. System environment:

See dockerfile above

d. My complete Caddy config:

{
	order cache before rewrite 

	{$caddy_globalOptions}
	email {$tls_acmeEmail}
	storage redis {
		module redis
		address {$redis_host}:{$redis_port}
		db {$redis_database_caddy}
		password {$redis_password}
		key_prefix "caddy:"
		tls_enabled false
		tls_insecure false
		aes_key "redistls-x123456789x-caddytls-32"
	}

	cache {
		ttl 60s
		badger
	}

	# When connecting via an IP address, no certificate will be found; this sets a default certificate
	default_sni {$site_host}

	# listen for connections beyond localhost
	admin :2019

	#debug

	metrics
}

(turbokivaweb) {
	encode gzip
	log

	@sendToOldServer {
		header_regexp migrationCookie Cookie {$migration_cookie}=([^;]*)
		not {
			header x-sentToOldServer 1
		}
	}

	@exploitPaths {
		path /wp-login.php /xmlrpc.php /assets/images/s_eval.php
	}
	handle @exploitPaths {
		respond 400
	}

	handle /static/* {
		@nocache {
			query v=nocache
		}
		@cache {
			not {
				query v=nocache
			}
		}

		# 1 year; version-based cache busting is in place
		header @cache Cache-Control "max-age=31536000, immutable"
		header @nocache Cache-Control "no-store"

		cache {
			# cache so gzipped files don't get re-gzipped
			ttl 60s
		}

		header X-Content-Type-Options nosniff
		file_server {
			root /app/turbokiva/web
		}
	}
	handle /robots.txt {
		file_server {
			root /app/turbokiva/web/static
		}
	}
	handle /favicon.ico {
		file_server {
			root /app/turbokiva/web/static/favicon
		}
	}

	handle {$documentUpload_path}* {
		reverse_proxy documentrouterandextractor:80
	}
	handle @sendToOldServer {
		reverse_proxy {
			to {$migration_oldServerHost}:{$web_portMigration}
			header_up x-sentToOldServer 1
		}
	}
	handle {
		header Cache-Control no-cache
		header X-Content-Type-Options nosniff
		header -x-powered-by
		php_fastcgi php{$web_upstreamSuffix}:9000
	}
	handle_errors {
		rewrite * /static/errorPages/general.html
		templates
		file_server
	}
}

# Redirect www.
www.{$site_host}:{$web_portSecure} {
	redir https://{$site_host}{uri}
}

# The first matcher matches the hostname, the second matcher matches all IP addresses
https://{$site_host}:{$web_portSecure}, https://:{$web_portSecure} {
	import turbokivaweb
	# Will automatically get HTTPS because prefixed with https://
	# Auto-HTTPS will setup http redirect to https
}

# Insecure for reverse proxy during migration
http://{$site_host}:{$web_portMigration} {
	import turbokivaweb
	# Won't automatically get HTTPS because port isn't 443
}

# Used by unit test on local and teamcity
https://localhost:{$web_portSecure} {
	import turbokivaweb
	# Will automatically get HTTPS because prefixed with https://
	# Auto-HTTPS will setup http redirect to https
}

http://localhost:{$web_port} {
	import turbokivaweb
}

5. Links to relevant resources:

You’re looking for the skip_install_trust global option

2 Likes

That does indeed prevent the warning, thank you!

A point of improvement though: I’d imagine nearly every instance of caddy is in a container or server where having that CA added is either impossible or not useful. I appreciate the automation, so I can see why this is on by default. I’d suggest adding or reference to that global option but the warning is actually printed by the library :neutral_face: