Jenkins and CI Server Ports Left Open to the Internet

Jenkins and CI Server Ports Left Open to the Internet

A Jenkins controller that answers on port 8080 to any IP address is one of the fastest ways for an attacker to go from an internet scan to your source code and cloud credentials. If you run DevOps or sysadmin work and inherited a build server, you can check in a few minutes whether any Jenkins or CI server ports are open to the internet. Closing them usually takes an afternoon. This article covers which ports CI tools expose, why attackers go after them first, how to lock them down without breaking webhooks, and the mistakes that quietly open them again.

Why Build Servers Are Worth More Than Production Web Apps

A CI server is not just another web application. It holds deploy keys, AWS and Azure tokens, container registry passwords, code-signing certificates and often SSH access to production. If an attacker takes over the controller, they get the same trust as every environment it deploys to.

Jenkins makes this especially direct. The Groovy script console at /script runs arbitrary code as the Jenkins user. From that console, one line of Groovy can decrypt every credential stored in the credentials plugin. An exposed login page is one weak password or one auth bug away from that console.

Real attacks have already shown this. CVE-2024-23897 was disclosed on January 24, 2024. It let attackers read files on the controller through a flaw in how the built-in CLI parsed arguments, and it affected Jenkins 2.441, LTS 2.426.2 and earlier. Within days, Shadowserver counted about 45,000 unpatched instances online. CISA added the bug to its Known Exploited Vulnerabilities catalog in August 2024, after it was used as the entry point in the ransomware attack on Brontoo Technology Solutions. That attack disrupted payment systems at several Indian banks.

JetBrains TeamCity followed the same pattern. CVE-2023-42793 and CVE-2024-27198 were both authentication bypasses, and attackers exploited them within days. APT29 was one of them.

Which CI Server Ports Show Up on an External Scan

An external scan with service detection usually finds a predictable set of ports. An experienced penetration tester looks at these first:

– 8080 and 8443 – the Jenkins web UI and HTTP CLI, plus Argo CD and leftover reverse-proxy backends
– 50000 – the Jenkins inbound TCP agent listener
– 8111 – the TeamCity default
– 2375 and 2376 – the Docker daemon API on build hosts, often left without authentication
– 8081, 8082 and 9000 – Nexus, Artifactory and SonarQube running next to the pipeline

Port 50000 deserves extra attention. Admins put 8080 behind a VPN and forget that someone opened the agent port separately for a cloud agent two years earlier. Jenkins also sends an X-Jenkins response header containing the exact version. That header is all an attacker needs to match your server against a CVE list automatically.

The Myth That Login Protection Makes Exposure Acceptable

The usual defense goes like this: anonymous read is off, matrix authorization is set up, and everyone signs in through SSO. That feels safe. It isn’t.

CVE-2024-23897 let completely unauthenticated users read the first lines of files on the controller. Any account with Overall/Read permission could read whole files. The TeamCity bugs skipped authentication entirely. Authentication stops password guessing. It does nothing against a bug in the authentication layer itself. The only control that holds up against a pre-auth zero-day is not being reachable at all.

A related misconception is that Jenkins has to be public for GitHub webhooks to work. It doesn’t. GitHub publishes the IP ranges its webhooks come from at api.github.com/meta, and GitLab.com publishes its ranges too. A reverse proxy can allow only those ranges, and only on the webhook path. The rest of the UI stays private.

A Scenario Most DevOps Teams Will Recognize

A development team sets up Jenkins on an EC2 instance for a proof of concept in 2021. To get GitHub webhooks working, someone sets the security group to allow 0.0.0.0/0 on port 8080 “just for now.” The proof of concept becomes the production pipeline. Three years later the controller is still on 2.401, because plugin compatibility problems kept delaying upgrades.

An external scan flags 8080 and 50000 as open and shows the exact version string. The fix takes about a day: put Nginx in front on 443 and allow only GitHub’s hook ranges, close the public ports, and upgrade to the current LTS line. Rotating credentials takes most of a week. There were more than 40 secrets across AWS, Docker Hub, npm and Slack, and there was no way to prove none of them had already been read.

How to Lock Down Jenkins and CI Server Ports Step by Step

Step 1 – See what the internet sees. From a host outside your network, run nmap -sV -p 8080,8443,50000,8111,2375,2376 against your public IPs. What you see from inside the network is misleading, because NAT rules, load balancer listeners and IPv6 addresses often expose things the security group view hides. For a structured way to compare what you intended with what is actually reachable, see validating firewall rules with external port scans.

Step 2 – Put the UI behind a private access layer. WireGuard, Tailscale, Cloudflare Access or Google Cloud IAP all work. Only engineers should be able to reach the login page.

Step 3 – Publish only the webhook path. In Nginx, allow the /github-webhook/ location only from GitHub’s published ranges and return 403 for every other path and source. Refresh the ranges automatically, because GitHub changes them.

Step 4 – Deal with the agent port. Since Jenkins 2.217, inbound agents can connect over WebSocket through the normal HTTPS port. Switch to WebSocket or SSH agents, then set the “TCP port for inbound agents” option to Disable. Port 50000 then stops listening.

Step 5 – Remove what you don’t use and keep patching. If nobody uses the CLI, disable it. Stay on the LTS line and subscribe to the Jenkins security advisories. Treat the plugin list as attack surface, too – many Jenkins advisories are about plugins, not core.

Step 6 – Monitor continuously. A one-off cleanup lasts until the next Helm upgrade or Terraform change. Adding external checks to the release process catches regressions before they stay open for months. Integrating port monitoring into your CI/CD pipeline covers that pattern.

Common Mistakes That Reopen the Exposure

The first mistake is patching without rotating credentials. Upgrading past a file-read bug doesn’t undo whatever was read while you were vulnerable. If the controller was exposed and unpatched during a known exploitation window, treat every stored secret as compromised.

The second mistake is locking down the controller but not the build nodes. Build agents running Docker with the daemon on 2375 and no TLS are a common finding. An attacker who can reach that socket gets root on the agent and can read any secrets that jobs inject into it.

The third mistake is trusting the cloud console instead of checking from outside. Someone changes the Jenkins Helm chart’s controller.serviceType from ClusterIP to LoadBalancer during a debugging session, and the security group review never notices. Exposed management interfaces get found quickly. How threat actors weaponize exposed management interfaces explains how short that window is in practice.

When the Standard Advice Does Not Apply

Teams under 10 people using GitHub-hosted runners or GitLab SaaS shared runners have no inbound CI port at all. For them, the risk moves to secrets stored in the SaaS platform and to overly broad OIDC trust policies. Self-hosted GitHub Actions runners and Buildkite agents poll outbound over 443, so they don’t need an inbound rule either. If one shows up anyway, it’s worth investigating.

Air-gapped or regulated on-prem environments, such as defense contractors or banks subject to DORA, don’t face scans from the internet. Their CI controllers are still a prime target for lateral movement from a compromised workstation, so internal segmentation matters just as much.

FAQ

What ports does Jenkins use by default?
Jenkins serves its web UI and HTTP CLI on 8080 and, when configured, listens for inbound TCP agents on 50000. Neither should be reachable from the public internet. If you need webhooks, publish only the webhook path through a reverse proxy on 443.

Is Jenkins safe to expose if it uses GitHub OAuth or SSO?
No. SSO protects against credential attacks, but pre-authentication vulnerabilities like CVE-2024-23897 and the TeamCity bypasses work before any login happens. Network-level restriction is the control that still works when authentication has a bug.

How can I tell whether an exposed Jenkins server was already compromised?
Check the script console audit trail if the Audit Trail plugin is installed. Look for unfamiliar jobs, new admin users, changed agent configurations, and outbound connections from the controller in VPC flow logs. If you can’t rule compromise out, rotate every stored credential.

Before you change a single firewall rule, list every credential your CI server holds. That list tells you how serious the exposure really is. It also becomes your rotation plan if you ever need one.