September 12, 2026
How to Lock Down Public Projects on Self-Managed GitLab
Why Public Projects Widen the Attack Surface
By Anthony Bahn
3 min read
Why Public Projects Widen the Attack Surface
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
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.
Originally published at https://www.anthonybahn.com. For a practical checklist, read the full learn article on https://www.anthonybahn.com/learn/how-to-lock-down-public-projects-on-self-managed-gitlab/ and explore more security notes at https://ambdigitalgroup.substack.com/.