Monitoring self-hosted GitLab CI behind a VPN
Four ways to get CI failure and cost analytics out of a GitLab the internet cannot reach — firewall holes, in-pipeline uploaders, IP allowlists, or an outbound-only agent.
Monitoring self-hosted GitLab CI behind a VPN means getting failure rates, durations, and cost signals out of a GitLab instance that is deliberately unreachable from the internet — without undoing the reason it is unreachable. Most SaaS analytics tools work by pulling from a CI provider's API, which assumes the provider is on the public internet. A self-hosted GitLab CE/EE inside a corporate network, a VPC, or behind a VPN breaks that assumption, so the question becomes: how does pipeline data leave the network, what exactly leaves, and what does the network have to expose to make it happen?
This guide compares the four approaches teams actually use, in rough order of how much they cost in security posture, and closes with a checklist for evaluating an agent-based (push) integration — the option this guide recommends.
Why the standard integration does not work
A pull-based integration is simple: you give the analytics service an access token, it calls the GitLab API on a schedule, and it reads pipelines, jobs, and traces. The service needs two things — network reachability and a credential — and a VPN'd instance denies the first. Opening the second to a host you cannot reach is pointless.
The instinctive workarounds each trade away something the VPN was protecting:
| Approach | What the network exposes | What leaves the network | Who holds the GitLab token |
|---|---|---|---|
| Firewall hole / reverse proxy | An inbound path to GitLab's API | Whatever the token permits | The SaaS vendor |
| In-pipeline uploader | Nothing inbound | Whatever the uploader collects, per job | Your CI (job token) |
| IP-allowlisted pull | Inbound from the vendor's IP ranges | Whatever the token permits | The SaaS vendor |
| Outbound-only agent | Nothing inbound | A fixed, documented data set | Stays inside your network |
Option 1 — punch a hole for the vendor
Expose GitLab's API through a reverse proxy, a dedicated hostname, or a port-forward, then hand the vendor a read-scoped token. It works with any pull-based tool and needs no software in your network.
The cost is that you have re-created the exact exposure the VPN existed to prevent, and GitLab's
API has no scope narrower than read_api
— the same token that lists pipelines can read repository contents, merge requests, and snippets.
Your only guarantee that a vendor stays within pipeline endpoints is the vendor's own discipline. If
you take this route, restrict the proxy to /api/v4/projects/*/pipelines and /jobs paths at the
proxy layer, so the boundary is enforced by your infrastructure rather than by a promise.
Option 2 — upload from inside the pipeline
Add a step to .gitlab-ci.yml that posts each job's results to the analytics service. Nothing
inbound is required, and the data is scoped to whatever the step collects.
Three drawbacks. It requires editing every project's CI configuration and keeping that step working across hundreds of repositories — precisely the kind of code access a zero-code-access integration is supposed to avoid. It only sees jobs where the step ran, so cancelled pipelines, jobs that failed before the step, and runner-level failures are invisible — and those are often the expensive ones (see cancelled vs failed runs). And it adds a network call to every job, on the critical path of every build.
Option 3 — allowlist the vendor's IP ranges
A narrower version of option 1: keep GitLab private but permit inbound API traffic from the vendor's published egress addresses. This satisfies some security reviews and blocks casual scanning.
It is still an inbound path, still relies on the vendor's token discipline, and it fails quietly when the vendor changes its egress ranges — the sync stops and nobody is told why. It also does not help when the instance is on a private address space with no public ingress at all, which is the common case for a VPN'd GitLab.
Option 4 — run an outbound-only agent inside the network
Install a small agent process on a host that can reach GitLab over the LAN. The agent reads pipeline data locally and pushes it to the analytics service over HTTPS on port 443 — the same outbound traffic a browser generates. No inbound port, no proxy, no DNS change, and the GitLab token is configured only on the agent host.
This inverts the trust question. With a pull integration you ask "how far can the vendor reach into my GitLab?"; with a push agent you ask "what can this program send out?" — which is a question you can answer by reading the program, because a well-designed agent is open source and small. The data set that leaves is fixed by the agent's code and by the receiving API's schema, not by the token's scope.
The cost is one more process to run. A well-designed agent keeps that cost near zero: no local state to back up (the service remembers how far each project is synced, so a replacement agent resumes where the old one stopped), a single static binary or a minimal container image, and an automatic catch-up after any outage.
What to check in an agent
Not every "agent" is equivalent. Before running one inside a private network, verify:
- Outbound only. It must not listen on any port reachable from outside the host. A local health-check endpoint bound to localhost is fine; anything else is an inbound surface.
- Open source, small, auditable. Your security team should be able to read the entire thing in an afternoon. A dependency-free build is a strong signal — every third-party module is something else to audit.
- An explicit allowlist of GitLab endpoints. The agent should refuse, in code, to call anything outside the pipeline, job, trace, and project-listing paths — mirroring the boundary the vendor enforces on its own pull integrations. Look for that allowlist in the source and in the README.
- Where the tokens live. The GitLab token should be read from the agent's environment and used only against your GitLab. The agent's own credential for the vendor should be stored hashed on the vendor's side, so a breach of the vendor's database yields nothing usable.
- Stateless operation. If the agent keeps a local cursor file, a host migration or a reinstall becomes a support ticket. Server-side cursors mean any replacement agent resumes correctly.
- Bounded data. Know exactly what leaves: run and job metadata, timings, and — if the tool analyzes failures — a bounded tail of failed-job logs. Log tails carry whatever a build prints, so the same secret hygiene you apply to CI logs applies here.
- Honest failure handling. A dead agent should show as offline in the dashboard and notify someone; a returning agent should catch up automatically without re-sending months of history.
How TrimCI does it
TrimCI supports VPN'd and private-network GitLab through the open-source TrimCI Agent, which
implements option 4. The agent runs inside your network as a single container or static binary,
reads pipelines, jobs, and the last 8,000 characters of failed-job logs from your GitLab, and pushes
them to TrimCI over outbound HTTPS. Its GitLab-endpoint allowlist is a strict subset of the one TrimCI enforces on
its cloud GitLab integration, its own credential is stored only as a hash, and TrimCI keeps the sync
cursors — so restarting, moving, or replacing the agent never loses or duplicates data. Setup is a
single docker run command generated in the dashboard; the user guide
walks through it, and the agent is open source so its code and protocol can be reviewed before
it runs.
Whichever option you choose, the goal is the same: the failure analysis and cost estimates a self-hosted team gets should be no worse than what a gitlab.com team gets — and the network should be exactly as closed afterwards as it was before.
See what your CI failures actually cost.
Connect GitHub Actions or GitLab CI with read-only access — we never request repository contents, only pipeline runs, jobs and failed-job log excerpts — and get a ranked, dollar-costed fix list from your own data.