Home / Stories / GitLab, GitHub & Forgejo
Story · Platforms Live

Proposals land where your code lives. Cluster access rides the same forge.

GitLab is the forge the assistant runs against in production every day: draft MRs, issues, reviews, and a security review of the GitLab instance itself. Through the GitLab agent for Kubernetes it reads your clusters without a kubeconfig on its side and without any inbound path to your API servers. GitHub draft pull requests are built and waiting for their first live write; Forgejo is being added.

Starts fromThe forge that hosts your repositories, and the GitLab agent already running in your clusters.
Looks atRepository contents, CI configuration, project access and branch protection, the runner fleet, and live cluster state through the agent's proxy.
You getDraft MRs and issues on your forge, a GitLab security review, and cluster reads with no firewall change.
01 · One pipeline, three forges

Every proposal goes through one router.

Agents produce proposals without knowing the forge. A router picks the client for each repository from your configuration, and a proposal is only published to a repository the configuration positively names. Anything unmatched is refused at emission, and the GitLab client refuses repositories configured for another forge.

The same rule holds on every forge: the assistant opens drafts and never merges them.

02 · The usual way in

A kubeconfig, and a network path to the API server.

A tool that reads a cluster normally needs a kubeconfig with long-lived credentials, and a route to the API server: a public endpoint, a VPN, or an allowlisted static egress address on the tool's side.

For a customer cluster behind a VPN, that is a firewall change and a credential to rotate, per cluster.

03 · Through the GitLab agent

The cluster dials out. Nothing dials in.

The GitLab agent (agentk) runs inside the cluster and keeps an outbound connection to GitLab's agent server (KAS). The assistant talks to KAS's Kubernetes proxy with a GitLab token scoped to k8s_proxy, and builds its kubeconfig in memory.

No inbound port, no VPN, no kubeconfig file. Access is granted in the agent's config file in git, to a project or group the bot belongs to.

04 · What it may do, in git

The bot's cluster role is a file you review.

On GitLab CE every proxied request runs as the agent's service account, so the agent's RBAC is the assistant's access. It is read-only by contract, plus a few narrow, separately bound extras such as requesting a Flux reconcile.

That RBAC file lives in your deployment repo. The assistant checks it for drift, and any change the assistant itself proposes to it is escalated to human review before an MR is opened.

05 · Keep the path up

Two agent replicas, two zones, one disruption budget.

If both agent replicas land on one node, a single spot reclaim or drain takes the whole KAS context down: your CI deploys and the assistant's reads together. The baseline pins zone and host spread and a PodDisruptionBudget, which the chart has no value for.

06 · Review the forge itself

Branch protection, CI secrets, the runner fleet.

Three deterministic reviewers read GitLab through a GET-only client: project access and branch protection, the merged CI configuration of every project, and the runners each project can use. No LLM is involved.

When the bot's own role cannot see an endpoint, it files one "could not review" advisory. A blind review is never reported as a clean one.

07 · GitHub In development

Draft pull requests, same rules.

The GitHub client reads repositories, pull requests and history, and writes draft pull requests. Writing needs an explicit write token, a writable repository row and a positive config match; without all of them it makes no HTTP call at all.

It is built and tested, and waiting for its first live write. Known gaps: marking a PR ready and assigning reviewers.

08 · Forgejo In development

Read path proven. Writes next.

A Forgejo repository is read through the same reader as GitHub, with a thin dialect for Forgejo's API. At the same commit it returns byte-identical files to a local clone, and a large tree it cannot page through reads as incomplete, never as complete.

Planned, not built: draft pull requests. Forgejo marks drafts by a WIP: title prefix, so the writer reads the PR back and closes it if it did not come out as a draft.

09 · The proposal

A one-line CI fix, found by the forge review.

Most forge findings are issues: a committed secret needs rotating, not a diff. One is mechanical: CI_DEBUG_TRACE left on, which prints every masked variable into the job log. When the setting is verbatim in the project's own .gitlab-ci.yml, you get a draft MR. The full MR is below.

Agentsproposals
Forge routerpositive config match
GitLabdraft MRs · issues
Live
GitHubdraft PRs
In development
Forgejoread path · draft PRs next
In development
no positive config match → not published · foreign-forge repos refused by the GitLab client
Assistantkubeconfig file · long-lived credential
VPN / firewallallowlisted egress address
API servermust be reachable
inbound path per clustercredential per clusterrotation per cluster
a firewall change and a secret to manage, for every cluster you add
Assistantin-memory kubeconfig
GitLab KAS/k8s-proxy · token scope k8s_proxy
your cluster · no inbound port
agentkdials out to KAS
API serverprivate
outbound from the cluster onlyaccess declared in gitrevoke = one token
infrastructure/rbac/gitlab-agent-rbac.yaml
kind: ClusterRole
metadata:
  name: gitops-assistant-read
  labels: { …/operator-pack: agentk-rbac }
rules:
  - apiGroups: ["", apps, helm.toolkit.fluxcd.io, …]
    verbs: [get, list, watch]
---  separately bound, separately revocable
kind: ClusterRole
metadata: { name: gitops-assistant-flux-poke }
# patch Flux objects to request a reconcile
  • drift from the canonical rules convergence MR
  • bot-proposed change to this file held for human review
zone a
node-1
ag
·
·
zone b
node-4
ag
·
gitlab-agent baseline · beyond the chart values
topologySpreadConstraints: # zone + hostname, via post-renderer
PodDisruptionBudget: minAvailable: 1
one spot reclaim no longer takes CI deploys and the assistant down together
GitLab review · GET-only · deterministic
  • shop/orders-api · main allows force-push access · issue
  • platform/tooling · private key committed in CI config CI/CD · issue, redacted
  • payments/ledger · CI_DEBUG_TRACE: "true" CI/CD · draft MR
  • runners for 12 projects not visible to the bot's role · 1 advisory
never proposes touching its own access, token or runner · never removes the last owner
write authority · GitHub
  • repository row is kind github and writable
  • write token resolved
  • positive config match for this repo
  • any one missing 0 HTTP calls · proposal stays unpublished
reads: incomplete lists are marked incomplete, and "unknown" is never read as "absent"
reader compare · Forgejo API vs local clone · same commit
 path set          identical
 file bytes        identical
 parsed manifests  identical
 resolved commit   identical
  exit 0 · IDENTICAL
create PRtitle "WIP: …"
read backdraft is true?
else closeproposal fails
Draft · Disable CI_DEBUG_TRACE in payments/ledger
variables:
-  CI_DEBUG_TRACE: "true"
+  CI_DEBUG_TRACE: "false"
category:securitysource:gitlab_cicdrisk:lowxrev::LGTM
↓ the full merge request is below

The merge request you get.

Illustrative project names; the finding, the verbatim-source check and the single-line flip are what the CI/CD reviewer does today.

Draft

Disable CI_DEBUG_TRACE in payments/ledger

from gitops-ai/cicd-debug-trace-ledger into main · 1 file · +1 −1
category:securitysource:gitlab_cicdrisk:lowxrev::LGTM
OverviewWhere it comes fromAfter mergeChanges
Summary

CI_DEBUG_TRACE is set to true in this project's .gitlab-ci.yml. With debug tracing on, the runner prints every command and every CI/CD variable, including masked ones, into the job log. Anyone who can read the job log can read the secrets.

This MR sets it to false. Nothing else changes.

Also do
  • Rotate any secret that was available to jobs while debug tracing was on.
  • Consider deleting job logs from that period.
Why this is an MR and not an issue

The reviewer reads the merged CI configuration, which also contains what comes from includes, parent groups and UI variables. It only proposes a diff after re-reading the project's own .gitlab-ci.yml and finding the setting there verbatim. Had it come from an include or a UI variable, you would get an issue pointing at the source instead.

CheckResult
Merged CI config has CI_DEBUG_TRACE truthyyes
Present verbatim in .gitlab-ci.ymlyes, top-level variables:
Key is unique in the fileyes
  • The next pipeline's job logs no longer contain the shell trace of every command and variable.
  • The CI/CD review stops reporting this project on its next run.
  • Secrets available to this project have been rotated (see Overview).
   variables:
-    CI_DEBUG_TRACE: "true"
+    CI_DEBUG_TRACE: "false"
     DOCKER_DRIVER: overlay2
R
independent reviewer second opinion, no pipeline context

Flip is minimal and matches the title. Rotation advice is appropriate. LGTM.

What keeps it honest.

A tool with access to your forge and your clusters has to be boring about both.

No inbound pathCluster reads go through the GitLab agent's outbound tunnel. Access is declared in the agent config in git and revoked by removing it or the token.
Your RBAC, in your repoThe assistant's cluster permissions are a reviewed file. It drift-checks that file, and cannot quietly widen it: its own changes to it are held for review.
Never about itselfThe GitLab reviewers never propose touching the bot's own access, token or runner, never remove the last owner, and never tighten a protection past the bot's own push path.
Blind is not cleanIf the bot's role cannot read an endpoint, the affected checks are skipped and one advisory says so. A partial read never produces a "healthy" result.
Positive match to publishA proposal is only published to a repository your configuration names. Unmatched repositories are refused at emission, and the GitLab client refuses repositories configured for another forge. Strict default-deny for unknown forge kinds is being added with Forgejo.
Drafts only, everywhereThe assistant opens drafts and never merges them. Where drafts are a title convention, the draft state is read back and a PR that did not come out as a draft is closed.

Where this stands.

Live
  • GitLab draft MRs, issues and MR review
  • Cluster access through the GitLab agent (KAS) on production clusters
  • Agent RBAC drift checks
  • GitLab access, CI/CD and runner review; CI_DEBUG_TRACE fix as a draft MR
In development
  • GitHub reads and draft pull requests (built; first live write pending)
  • Forgejo read path, verified identical to a local clone
Roadmap
  • Forgejo draft pull requests
  • GitHub: mark ready for review, reviewer assignment
← Previous storyCross-domain context