docker manifest inspect fails with "permission denied" on /etc/docker/certs.d
Symptom
A deployment script verifies that the requested tag exists before it touches a running stack. Under the deploy user it fails:
$ docker manifest inspect registry.example.com/backend:1.42
error: open /etc/docker/certs.d/registry.example.com/ca.crt: permission denied
Under root on the same node the identical command returns the manifest. Pulling images works for the
deploy user too, which makes the failure look arbitrary.
Why root works and your user does not
docker manifest inspect is one of the CLI commands that talks to the registry from the client
process, not through the daemon. So it needs to read the registry CA itself — and
/etc/docker/certs.d/... is intentionally readable only by root and the daemon.
docker pull, by contrast, is executed by the daemon, which runs as root and can read the CA. That is the
whole asymmetry: adding your user to the docker group grants access to the daemon socket, not to
root-owned files on disk.
Three tempting fixes and why I rejected them:
chmod/chgrpon/etc/docker/certs.d— loosens the trust store on customer nodes for a convenience check. No.sudo docker manifest inspect— needs a sudoers rule per node, and now your deploy script requires privilege escalation for a read-only test.- Copying the CA into the user’s home and pointing the CLI at it — another copy of a certificate to rotate and forget.
Fix: ask the registry API directly
An existence check does not need Docker at all. The registry v2 manifest endpoint answers a HEAD:
image="backend"; tag="1.42"; reg="registry.example.com"
if curl -sfk -X HEAD "https://${reg}/v2/${image}/manifests/${tag}" \
-H 'Accept: application/vnd.docker.distribution.manifest.v2+json'; then
echo "tag exists"
else
echo "tag missing — aborting deploy" >&2
exit 1
fi
Notes from making this work in a real pipeline:
-kis deliberate here and it is the trade-off you should be aware of: the client process cannot read the private CA, which is what started this whole problem. If that is unacceptable in your environment, ship the CA to a user-readable path and drop-k— but then own the rotation.- Send the
Acceptheader. Without it some registries answer404for images stored only as a v2 / OCI manifest. - If your registry requires auth, a
HEADreturns401with aWww-Authenticatechallenge; fetch a token from the realm in that header and retry with-H "Authorization: Bearer …". - Some registries — including the customer-operated ones I deploy to — serve manifests anonymously, so the check needs no credentials at all. Verify which case you are in before you build a token dance.
Keep the pre-check honest
Whatever mechanism you use, make the failure loud and make it stop the deploy:
set -euo pipefail
image_exists "$reg" "$image" "$tag" || { echo "refusing to deploy missing tag" >&2; exit 1; }
docker stack deploy -c "docker-compose-${stack}.yml" "$stack"
A silent pre-check that fails open is worse than no pre-check: the stack update proceeds, the service cannot pull, and you get a rolling failure instead of a clean abort.