Skip to main content

Private certificate authorities

If your organisation runs its own certificate authority, or a proxy that terminates TLS and re-signs it, the portal's outbound connections will not verify until it trusts that authority. The symptom is usually a scan that fails while cloning, or vulnerability feeds that stop refreshing.

This page is about that case. It is not about air-gapped operation, where there is no outbound connection to secure at all.

Two steps, in this order

  1. Put the certificate inside the containers.
  2. Point the tools at it.

Neither works alone, and the second one is where a correct-looking setup can still be wrong, because the tools do not all read the same variable and the same variable does not mean the same thing to all of them.

Putting the certificate in

Mount it read-only into the backend, worker and beat containers. With Docker Compose, add an override file next to docker-compose.yml:

# docker-compose.override.yml
services:
backend:
volumes:
- /etc/ssl/corp/ca-bundle.pem:/etc/ssl/corp-ca.pem:ro
worker:
volumes:
- /etc/ssl/corp/ca-bundle.pem:/etc/ssl/corp-ca.pem:ro
beat:
volumes:
- /etc/ssl/corp/ca-bundle.pem:/etc/ssl/corp-ca.pem:ro

Compose reads docker-compose.override.yml automatically, so upgrades that replace docker-compose.yml leave your mount alone.

The file must hold your authority and the public roots, concatenated. The next section says why.

cat /path/to/corp-root-ca.pem /etc/ssl/certs/ca-certificates.crt \
> /etc/ssl/corp/ca-bundle.pem

With Helm

Create the Secret yourself, then let the chart mount it and set the variables:

kubectl create secret generic corp-ca --from-file=ca-bundle.pem=/etc/ssl/corp/ca-bundle.pem
env:
extraVolumes:
- name: corp-ca
secret:
secretName: corp-ca
extraVolumeMounts:
- name: corp-ca
mountPath: /etc/ssl/corp-ca.pem
subPath: ca-bundle.pem
readOnly: true
extraEnv:
SSL_CERT_FILE: /etc/ssl/corp-ca.pem
NODE_EXTRA_CA_CERTS: /etc/ssl/corp-ca.pem
REQUESTS_CA_BUNDLE: /etc/ssl/corp-ca.pem
GIT_SSL_CAINFO: /etc/ssl/corp-ca.pem

subPath is what makes the certificate a file rather than a directory over that path, and without it the variables above name something that is not there. The mount goes to the backend, the scheduler and both workers, and not to the frontend or Redis, which make no outbound calls.

The same bundle rule applies: your authority and the public roots in one file, for the reason in the next section.

Pointing the tools at it

Add these to your .env:

SSL_CERT_FILE=/etc/ssl/corp-ca.pem
NODE_EXTRA_CA_CERTS=/etc/ssl/corp-ca.pem
REQUESTS_CA_BUNDLE=/etc/ssl/corp-ca.pem
GIT_SSL_CAINFO=/etc/ssl/corp-ca.pem

Four names for one file, because the tools disagree. Setting all four is the short answer; the table says which one each of them needs, for when only part of the pipeline is failing.

VariableRead byEffect
SSL_CERT_FILETrivy, cosign, govulncheck, and the portal's own HTTPS callsAdds for the Go tools, which keep reading the system directory. Replaces for the portal itself.
SSL_CERT_DIRThe same setA directory of hashed certificates, an alternative to the file.
NODE_EXTRA_CA_CERTScdxgenAdds. The built-in roots stay.
REQUESTS_CA_BUNDLEscancode, scanossAdds for those two. The portal's own calls ignore it.
GIT_SSL_CAINFOgit cloneAdds. git reads neither SSL_CERT_FILE nor CURL_CA_BUNDLE.
GIT_SSL_CAPATHgit cloneThe directory form of the above.

Two of those rows are worth reading twice.

SSL_CERT_FILE replaces the trust set for the portal's own connections. The vulnerability feeds, licence lookups, Slack and Teams notifications, the GitHub and GitLab calls and the ticket webhook all go out through one HTTP client, and that client builds its trust from this file alone. Point it at a file holding only your authority and every public endpoint becomes unverifiable, while Trivy and cdxgen keep working because they still consult the system store. The result reads as a feed outage rather than a certificate problem. Concatenating the public roots into the same file, as above, avoids it.

git clone reads only its own two variables. If cloning is the step that fails and everything else works, GIT_SSL_CAINFO is the one that was missing.

Checking it worked

Each of the three processes states its own trust set at boot, and names itself. Every line this page is about carries the prefix tls_trust, which is what to filter on. A worker's own output is mostly scan progress, so the line is there and unfindable without it:

docker-compose logs backend worker beat | grep tls_trust
tls_trust.outbound process=api authorities=140 bundled_authorities=120 source=SSL_CERT_FILE path=/etc/ssl/corp-ca.pem
tls_trust.outbound process=worker authorities=140 bundled_authorities=120 source=SSL_CERT_FILE path=/etc/ssl/corp-ca.pem
tls_trust.outbound process=beat authorities=140 bundled_authorities=120 source=SSL_CERT_FILE path=/etc/ssl/corp-ca.pem

Look for all three. Compose gives each service its own environment, so a certificate configured for one and missed on another is an ordinary mistake, and the worker is the one that matters most: the scanners reach the network from there. A process=worker line that still reads source=bundled means the worker never got the setting.

authorities is how many certificate authorities the portal's own HTTPS calls will accept and bundled_authorities is how many it ships with. A number larger than the bundled one means your authority was added to the public roots, which is what you want in most deployments.

If the file replaced them instead, a warning follows:

tls_trust.public_roots_dropped authorities=1 bundled_authorities=120 ...

That is the mistake described above. Concatenate the public roots into your bundle, or ignore the warning if trusting only your own authority is deliberate, which it is on a network that reaches nothing else.

When SSL_CERT_DIR is used instead of a file, authorities reads null. A trust store built from a directory loads certificates as it needs them, so there is no count to give, and printing zero would be worse than printing nothing.

Known limits

Container image pulls performed by the Docker daemon are outside the portal's configuration. If a container scan fails to pull an image from an internal registry behind a private authority, the certificate has to be installed for the daemon on that host; nothing in the portal's environment reaches it.