How to Lock Down Public Projects on Self-Managed GitLab
Public projects give anonymous users an API surface on your GitLab server. Here is how to inventory them, reduce the set, and stop new ones from appearing without an admin.
When watchTowr analyzed GitLab's CVE-2026-85706, the unauthenticated path traversalPath Traversal🛡️A web vulnerability (CWE-22) where user-supplied input in a file path escapes the directory the application intended to serve from, typically via parent-directory references, letting an attacker read or write files elsewhere on the server. patched in September 2026, its head of threat intelligence stated that exploitation had one requirement: at least one public project on the target instance. That detail is worth acting on. Public projects on a self-managed server are anonymous entry points to your API, and most organizations have more of them than they realize. This guide covers how to find them, how to decide which should stay public, and how to make sure new ones do not appear by accident.
Patching is still the fix. GitLab's advisory offers no workaround, and Horizon3's analysis says network restriction only reduces risk temporarily. What follows is a compensating controlCompensating Control🛡️A security measure applied in place of a primary control that cannot be implemented yet, such as network restriction while a patch is unavailable. It reduces risk to an acceptable level without fixing the underlying flaw. that shrinks your exposure to the next bug of the same shape.
Why Public Projects Widen the Attack Surface
A self-managed GitLab instance exposes two kinds of surface. The first is what authenticated users can reach. The second is what anyone on the internet can reach without a session. Public projects live in the second category. Their repositories, issues, and API endpoints answer to unauthenticated requests by design, and any vulnerabilityVulnerability🛡️A weakness in software, hardware, or processes that can be exploited by attackers to gain unauthorized access or cause harm. in those code paths is exposed to every scanner on the internet.
That matters because the API surface for a public project is large. Repository files, commits, branches, tags, releases, pipelines, and package registries all have read endpoints that an anonymous caller can hit. A bug in any of them is an unauthenticated bug, and unauthenticated bugs get exploited first. The explainer How Path Traversal Bugs Let Attackers Read Files Outside the Web Root covers why the missing authentication check in the GitLab commits API was as important as the traversal itself.
Public projects also tend to accumulate without oversight. A team open-sources a small tool. A contractor sets visibility to public so an external reviewer can see it. A migration script imports projects with their original visibility. None of these decisions are wrong on their own, but together they mean the instance's anonymous surface grows over time and no one owns it.
Step 1: Inventory What Is Public Today
Start by listing every public project on the instance. GitLab exposes project visibility through both the administrative UI and the API. The visibility levelVisibility Level🛡️A per-project or per-group setting on platforms like GitLab that controls who can see it: public (anyone, including anonymous users), internal (any signed-in user), or private (explicit members only). Public projects expose API endpoints to unauthenticated callers. is a property of each project and each group, and public is the most permissive of the three levels alongside internal and private.
Build the list with these questions attached to each entry:
- Who owns it, and do they know it is public?
- Does it need to be public, or was it made public for a one-time reason that has passed?
- Does it contain anything sensitive in history, CI configuration, or attached artifacts?
Expect surprises. Instances that have been running for years usually have public projects whose owners have left the organization.
Step 2: Reduce the Public Set
For each project that does not have a current reason to be public, change its visibility to internal or private. Internal is visible to any authenticated user on the instance and is usually the right default for code that is shared across teams but not with the world. Private restricts access to explicit members.
Where a project must stay public, check whether the group containing it also needs to be public. Group visibility governs what can be discovered by browsing, and a private group with a public project inside it limits some enumeration paths.
If you have public projects that exist only so external parties can read them, consider whether those parties could instead be given accounts, or whether a mirror on a hosted service would remove the need to expose your own instance at all. This is the same trade-off discussed in Why One File Read Can Compromise Your Entire CI/CD Platform: the server that holds your CI/CD secrets should carry as little anonymous surface as possible.
Step 3: Stop New Public Projects From Appearing
Cleaning up once is not enough. GitLab's administrator settings include a control for this. Under Admin, then Settings, then General, expand Visibility and access controls. The restricted visibility levels setting lets you restrict the Public level, and GitLab's documentation states that when Public is restricted, only administrators can create public groups, projects, and snippets.
That single setting converts public visibility from a per-user choice into an administrative decision. Users who need a public project must ask, and the request creates a record. The same settings page also sets the default project visibility; GitLab's docs describe the Private option as granting project access explicitly to each user, which is the sensible default for a corporate instance.
Enable both. Restrict the Public level, and set the default to private. Existing public projects are unaffected until you change them, which is why the inventory in Step 1 comes first.
Step 4: Verify the Instance Is Patched
None of this replaces the upgrade. GitLab's fix for CVE-2026-85706 ships in 19.3.2, 19.2.6, and 19.1.8. Confirm what you are running: GitLab's API documentation describes `GET /api/v4/version`, which requires a private token and returns the version and revision. If the number is below the fixed build for your track, patch before you do anything else on this list.
Step 5: Watch the Anonymous Surface
Once the public set is minimal, monitor it. GitLab's request log for the API, at `/var/log/gitlab/gitlab-rails/api_json.log` on Linux package installations, records method, path, params, remote IP, status, and user ID for each call. Requests with no user ID are anonymous, and anonymous requests to endpoints that should not answer anonymously are the earliest signal of a new unauthenticated bug being probed. For the September flaw, watchTowr's detection guidance was to look for POST requests to the project commits endpoint carrying a file path parameter. Build that kind of query into a saved search so the next advisory takes minutes to check rather than hours.
What This Does Not Do
Restricting public projects narrows the precondition watchTowr identified, but it is a compensating control with limits. GitLab's own advisory says exploitation works under certain conditions without naming them, so do not treat a zero-public-project instance as safe. It does nothing for authenticated bugs, including CVE-2026-87719 and CVE-2026-88765 from the same release, which require a logged-in user. And it does nothing if your instance sits on the internet for reasons unrelated to public code, such as remote developers or webhook delivery.
For those cases the broader controls still apply: put the instance behind a VPN or identity-aware proxy, restrict which source addresses can reach the API, and keep the secrets file backed up somewhere the application cannot read. The September GitLab bug will not be the last unauthenticated flaw in a developer platform. The goal is to make sure the next one lands on an instance that exposes as little as possible to strangers.