Gitea Version
1.27.3
What happened?
act_runner: actions/checkout fails against an HTTPS Gitea instance with a private CA — job containers have no way to trust the cert
Summary
When a self-hosted Gitea instance is served over HTTPS with a certificate
signed by a private/internal CA (a very common self-hosted setup — e.g. a lab
or corporate root CA via step-ca / internal PKI), every CI job that uses
actions/checkout to clone a repo fails because the job container's trust
store does not include the internal CA. There is no first-class mechanism in
act_runner to inject a CA certificate into job containers, so the operator
must discover and wire up a multi-env-var + bind-mount workaround for every
TLS client the job images might use.
This is the primary onboarding papercut for "self-hosted Gitea over HTTPS
with a private CA" — a core self-hosted use case.
Environment
gitea/act_runner:0.2.13
- Gitea instance:
https://gitea.example.internal (TLS cert signed by an internal root CA)
- Runner host: podman (rootful)
Steps to reproduce
- Run a Gitea instance behind HTTPS with a cert signed by your own root CA.
- Register an act_runner against it (see "Related: runner registration" below
— that has its own CA-trust problem, but assume it's solved for now).
- Create a workflow that uses
actions/checkout:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- The job fails at the checkout step:
fatal: unable to access 'https://gitea.example.internal/Foundry/my-repo.git/':
SSL certificate problem: unable to get local issuer certificate
or, with the Go-based checkout path:
remote: ... x509: certificate signed by unknown authority
Why it fails
actions/checkout runs inside the job container (the image named by the
workflow's runs-on: / container:), not inside the act_runner container.
The job image (e.g. node:24, ubuntu:24.04) ships its own CA bundle that
trusts only public CAs. actions/checkout shells out to git fetch https://gitea.example.internal/<org>/<repo>.git, and git (via libcurl) cannot
verify the cert against the public-only bundle.
Critically, the act_runner container's trust store is not inherited by
job containers — they are a separate podman/docker process with their own
rootfs and trust store. Even if the runner itself trusts the CA (via
SSL_CERT_FILE or a rebuilt bundle), the job container starts from scratch.
Expected behavior
There should be a first-class, documented way to make job containers trust
the Gitea instance's private CA. Ideally:
- A
container.ca_cert config option (or GITEA_RUNNER_CA_CERT env var) that
act_runner automatically mounts into job containers and wires into the
common TLS env vars (GIT_SSL_CAINFO, NODE_EXTRA_CA_CERTS,
CURL_CA_BUNDLE, REQUESTS_CA_BUNDLE, SSL_CERT_FILE), so the operator
doesn't have to know every client's env-var name, or
- At minimum, a documented "Private CA" section in the act_runner README
covering the workaround below.
Workaround (what we do today)
We mount the CA bundle into every job container via the runner config's
container.options and set env vars for every TLS client the job images
might use:
container:
# Mount the CA bundle (root + intermediate) into every job container.
options: "-v /var/lib/act_runner/ca/lab-ca-bundle.crt:/etc/ssl/certs/lab-ca-bundle.crt:ro,Z"
envs:
# git (libcurl backend) -- used by actions/checkout's `git fetch`.
GIT_SSL_CAINFO: "/etc/ssl/certs/lab-ca-bundle.crt"
# Node.js TLS (actions themselves run on node; checkout.js, etc).
NODE_EXTRA_CA_CERTS: "/etc/ssl/certs/lab-ca-bundle.crt"
# curl CLI + python-requests, for ad-hoc HTTPS in job steps.
CURL_CA_BUNDLE: "/etc/ssl/certs/lab-ca-bundle.crt"
REQUESTS_CA_BUNDLE: "/etc/ssl/certs/lab-ca-bundle.crt"
# Go-based tools built inside jobs default to the system store;
# SSL_CERT_FILE overrides it (Go prefers this over the OS path).
SSL_CERT_FILE: "/etc/ssl/certs/lab-ca-bundle.crt"
This works, but it requires the operator to know in advance that:
- The job container won't trust the Gitea cert.
- The CA must be mounted via
container.options (not just set as an env var —
the file has to physically exist in the job container).
- Every common TLS client has a different env var name, and missing any
one of them means that specific tool fails opaquely.
Alternative workaround: bake the CA into a custom job image
The daemon-level injection above only reaches jobs that use the default
runner image (the one runs-on: resolves to without a workflow-level
container: block). When a workflow declares its own container: image,
act_runner does not merge the daemon's container.options /
container.envs into it — so neither the bind-mount nor the *_CAINFO env
vars reach that job, and checkout fails again with the same x509 error.
To cover that case we build a custom job image with the lab CA baked into the
system trust store, publish it to the lab registry, and expose it as a runner
label that workflows opt into.
Containerfile
FROM docker.io/library/node:24
# Bake the lab root + intermediate CA into the image's system trust store.
COPY lab-ca-bundle.crt /usr/local/share/ca-certificates/lab-internal-root-ca.crt
COPY lab-internal-intermediate-ca.crt /usr/local/share/ca-certificates/lab-internal-intermediate-ca.crt
RUN update-ca-certificates
# Node.js reads this in preference to the OS store for its own TLS.
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
Build, save, and load into the lab registry
Because the build host could not push directly to the HTTPS registry
the registry's cert is signed by the CA we are
trying to bake in), the image was built, saved to a tarball, transferred, and
loaded on a host that already trusted the CA:
# On the build host (has the CA bundle available + can reach docker.io):
podman build -t registry.lab.internal/ci/node:24-lab \
-f Containerfile .
podman save -o node-24-lab.tar registry.lab.internal/ci/node:24-lab
# Transfer node-24-lab.tar to the registry host:
scp node-24-lab.tar registry-vm:/tmp/
# On the registry host (CA already trusted there):
podman load -i /tmp/node-24-lab.tar
podman push registry.lab.internal/ci/node:24-lab
Runner label + workflow opt-in
The runner config maps the custom label to the pullable image:
runner:
labels:
- "node24-lab:docker://registry.lab.internal/ci/node:24-lab"
Workflows opt in by pinning both runs-on and container.image to it (the
label must resolve to the same pullable image the job container runs):
jobs:
build:
runs-on: node24-lab
container:
image: registry.lab.internal/ci/node:24-lab
steps:
- uses: actions/checkout@v4
This makes checkout "just work" for workflow-declared containers with zero
runtime injection — but it requires the operator to maintain and rebuild a
custom image per base image they want to support, and every workflow author
must know to opt into the -lab label. A first-class container.ca_cert
knob would remove both of these burdens.
Related: runner registration also needs the CA (secondary issue)
Separately from checkout, the act_runner container itself must trust the CA
to register (act_runner register --instance https://...) and to poll for
jobs. The gitea/act_runner:0.2.13 image is a distroless-ish Alpine build
that ships no update-ca-certificates (or ca-certificates beyond a static
bundle), so mounting the CA into the runner container is not enough on its
own — Go's crypto/x509 doesn't auto-pick-up arbitrary files on Alpine
unless they're merged into /etc/ssl/certs/ca-certificates.crt or
SSL_CERT_FILE is set.
Workaround for registration
Override the entrypoint to sh, mount the lab certs, concatenate the image's
existing CA bundle with the internal root + intermediate into a single file,
and point Go at it via SSL_CERT_FILE:
entrypoint: sh
command: >
-c '
cat /etc/ssl/certs/ca-certificates.crt
/usr/local/share/ca-certificates/internal-root-ca.crt
/usr/local/share/ca-certificates/internal-intermediate-ca.crt
> /tmp/lab-ca-bundle.crt;
act_runner register --no-interactive
--instance https://gitea.example.internal
--token ${RUNNER_TOKEN}
--name my-runner
--config /etc/act_runner/config.yaml || true;
exec act_runner daemon --config /etc/act_runner/config.yaml
'
environment:
SSL_CERT_FILE: /tmp/lab-ca-bundle.crt
The || true on register is because re-running register on an existing
.runner state file is a no-op (the runner validates and proceeds to
daemon), so the container restarts cleanly.
A bundled update-ca-certificates (or a tiny shim that cats files from
/usr/local/share/ca-certificates/ into /etc/ssl/certs/ca-certificates.crt)
in the act_runner image would make the runner-side half "just work" with a
mounted cert — but the job-side trust problem (the primary issue above)
is the bigger usability gap and needs its own solution.
How are you running Gitea?
Podman Quadlet - the repo is below.
https://github.com/chris-briddock/homelab-iac
Gitea Version
1.27.3
What happened?
act_runner:
actions/checkoutfails against an HTTPS Gitea instance with a private CA — job containers have no way to trust the certSummary
When a self-hosted Gitea instance is served over HTTPS with a certificate
signed by a private/internal CA (a very common self-hosted setup — e.g. a lab
or corporate root CA via step-ca / internal PKI), every CI job that uses
actions/checkoutto clone a repo fails because the job container's truststore does not include the internal CA. There is no first-class mechanism in
act_runner to inject a CA certificate into job containers, so the operator
must discover and wire up a multi-env-var + bind-mount workaround for every
TLS client the job images might use.
This is the primary onboarding papercut for "self-hosted Gitea over HTTPS
with a private CA" — a core self-hosted use case.
Environment
gitea/act_runner:0.2.13https://gitea.example.internal(TLS cert signed by an internal root CA)Steps to reproduce
— that has its own CA-trust problem, but assume it's solved for now).
actions/checkout:or, with the Go-based checkout path:
Why it fails
actions/checkoutruns inside the job container (the image named by theworkflow's
runs-on:/container:), not inside the act_runner container.The job image (e.g.
node:24,ubuntu:24.04) ships its own CA bundle thattrusts only public CAs.
actions/checkoutshells out togit fetch https://gitea.example.internal/<org>/<repo>.git, and git (via libcurl) cannotverify the cert against the public-only bundle.
Critically, the act_runner container's trust store is not inherited by
job containers — they are a separate podman/docker process with their own
rootfs and trust store. Even if the runner itself trusts the CA (via
SSL_CERT_FILEor a rebuilt bundle), the job container starts from scratch.Expected behavior
There should be a first-class, documented way to make job containers trust
the Gitea instance's private CA. Ideally:
container.ca_certconfig option (orGITEA_RUNNER_CA_CERTenv var) thatact_runner automatically mounts into job containers and wires into the
common TLS env vars (
GIT_SSL_CAINFO,NODE_EXTRA_CA_CERTS,CURL_CA_BUNDLE,REQUESTS_CA_BUNDLE,SSL_CERT_FILE), so the operatordoesn't have to know every client's env-var name, or
covering the workaround below.
Workaround (what we do today)
We mount the CA bundle into every job container via the runner config's
container.optionsand set env vars for every TLS client the job imagesmight use:
This works, but it requires the operator to know in advance that:
container.options(not just set as an env var —the file has to physically exist in the job container).
one of them means that specific tool fails opaquely.
Alternative workaround: bake the CA into a custom job image
The daemon-level injection above only reaches jobs that use the default
runner image (the one
runs-on:resolves to without a workflow-levelcontainer:block). When a workflow declares its owncontainer:image,act_runner does not merge the daemon's
container.options/container.envsinto it — so neither the bind-mount nor the*_CAINFOenvvars reach that job, and checkout fails again with the same
x509error.To cover that case we build a custom job image with the lab CA baked into the
system trust store, publish it to the lab registry, and expose it as a runner
label that workflows opt into.
Containerfile
Build, save, and load into the lab registry
Because the build host could not push directly to the HTTPS registry
the registry's cert is signed by the CA we are
trying to bake in), the image was built, saved to a tarball, transferred, and
loaded on a host that already trusted the CA:
Runner label + workflow opt-in
The runner config maps the custom label to the pullable image:
Workflows opt in by pinning both
runs-onandcontainer.imageto it (thelabel must resolve to the same pullable image the job container runs):
This makes checkout "just work" for workflow-declared containers with zero
runtime injection — but it requires the operator to maintain and rebuild a
custom image per base image they want to support, and every workflow author
must know to opt into the
-lablabel. A first-classcontainer.ca_certknob would remove both of these burdens.
Related: runner registration also needs the CA (secondary issue)
Separately from checkout, the act_runner container itself must trust the CA
to register (
act_runner register --instance https://...) and to poll forjobs. The
gitea/act_runner:0.2.13image is a distroless-ish Alpine buildthat ships no
update-ca-certificates(orca-certificatesbeyond a staticbundle), so mounting the CA into the runner container is not enough on its
own — Go's
crypto/x509doesn't auto-pick-up arbitrary files on Alpineunless they're merged into
/etc/ssl/certs/ca-certificates.crtorSSL_CERT_FILEis set.Workaround for registration
Override the entrypoint to
sh, mount the lab certs, concatenate the image'sexisting CA bundle with the internal root + intermediate into a single file,
and point Go at it via
SSL_CERT_FILE:The
|| trueonregisteris because re-runningregisteron an existing.runnerstate file is a no-op (the runner validates and proceeds todaemon), so the container restarts cleanly.A bundled
update-ca-certificates(or a tiny shim that cats files from/usr/local/share/ca-certificates/into/etc/ssl/certs/ca-certificates.crt)in the act_runner image would make the runner-side half "just work" with a
mounted cert — but the job-side trust problem (the primary issue above)
is the bigger usability gap and needs its own solution.
How are you running Gitea?
Podman Quadlet - the repo is below.
https://github.com/chris-briddock/homelab-iac