Securing Jenkins: A Hardening Checklist for Self-Hosted CI

How to secure a self-hosted Jenkins controller: authentication, authorization, TLS, plugins, agents, credentials — and how to receive webhooks without exposing Jenkins to the internet.

A Jenkins controller is two dangerous things in one process: a credential store and a remote code execution service you built on purpose. Anyone who reaches a job configuration can usually run commands on a build machine, and from there read the credentials that machine is allowed to use — cloud keys, registry tokens, deploy SSH keys, signing material.

That is why unauthenticated Jenkins instances get found and cryptomined within hours of being exposed. Securing Jenkins is mostly about shrinking two things: who can reach it and what a compromised build can touch.

This is a practical hardening checklist for a self-hosted controller, ordered roughly by how much risk each step removes.

What "Jenkins security" actually covers

Jenkins security is not one setting. It spans four layers, and a gap in any one of them undoes the others:

LayerWhat you are protectingMain controls
NetworkReachability of the controllerPrivate networking, TLS, no inbound ports
AccessWho can log in and what they can doSecurity realm, authorization strategy, API tokens
ExecutionWhat a build can do to its hostAgents, script security, ephemeral workspaces
Supply chainWhat code and plugins you runPatching, plugin review, pinned dependencies

A controller with perfect RBAC that is publicly reachable and three plugin CVEs behind is not secure. Work the list top to bottom.

1. Keep the controller off the public internet

The most common reason a Jenkins controller ends up public is boring: someone needed GitHub to deliver a webhook.

The usual fix is to open :8080 — or put a reverse proxy on :443 — so that https://jenkins.example.com/github-webhook/ resolves from the internet. But the provider only needs one path. What you actually exposed is the login page, the whole REST API, the CLI endpoint, every plugin's HTTP surface, and any pre-auth vulnerability in the version you happen to be running. Internet-wide scanners find that within hours.

Ranked from weakest to strongest:

  • IP allow-listing the provider's hook ranges. GitHub publishes its hook source ranges via the meta API, and you can restrict /github-webhook/ to them. This is real defense in depth, but the ranges change, you must re-sync them, and every other endpoint on that host is still publicly reachable — you have narrowed one path, not removed the exposure.
  • A reverse proxy that only forwards the webhook path. Better: the UI and API stop being reachable. You still run a public TLS origin that must be patched, and a proxy misconfiguration re-exposes everything.
  • A VPN or private network. Strong for humans, but SaaS providers are not on your VPN — it does not solve webhook delivery at all.
  • An outbound-only relay. The controller keeps no public IP and no inbound firewall rule.

That last option is the one that removes the problem rather than narrowing it. An agent running beside Jenkins opens an outbound connection to a relay; the provider sends its webhook to a stable public HTTPS input URL, and the relay pushes it inward over the connection the agent already established:

GitHub / GitLab / Bitbucket / Gitea
                 |
                 | HTTPS webhook
                 v
        public relay input URL
                 |
                 | outbound agent connection (no inbound rule)
                 v
     private Jenkins  http://localhost:8080/github-webhook/

Because the connection is established from inside, the firewall rule you need is the one you already have: allow outbound HTTPS. Jenkins can sit on localhost, an RFC1918 address, or a Kubernetes ClusterIP service with no ingress.

Two things worth being clear about. First, this moves the public endpoint to a third party, so keep verifying the provider's HMAC signature at Jenkins — the relay changes where the request enters your network, not whether you should trust it. Second, an SSH reverse tunnel or a self-hosted tunnel gets you the same network property; the reason to use a managed relay is the operational part — durable retries when Jenkins is down for a deploy, delivery logs when a build did not trigger, and fan-out to more than one internal endpoint.

Webhook Relay's Jenkins webhook guide walks through the GitHub, Bitbucket, GitLab and Gitea setups, and forwarding to an internal server covers the general pattern. There is also a Jenkins plugin if you would rather not run a separate agent process.

For human access to the UI, keep using a VPN, an identity-aware proxy, or SSO — the two problems are separate, and webhooks are the one that pushes people into exposing the controller.

2. Enable a real security realm

Under Manage Jenkins > Security:

  • Pick a security realm. Jenkins' own user database is fine for a small team; beyond that, use LDAP or SSO via a SAML/OIDC plugin so that offboarding a person in your IdP actually removes their Jenkins access.
  • Turn off "Allow users to sign up." It is the single worst default anyone leaves on. It has repeatedly turned "internal Jenkins" into "anyone with the URL is an admin."
  • Enforce MFA at the identity provider. Jenkins does not do this well natively, which is another argument for SSO.

3. Choose an authorization strategy that is not "anything"

The default "Logged-in users can do anything" is a starting point, not a configuration.

  • Use Matrix-based security, Project-based Matrix Authorization, or the Role-based Authorization Strategy plugin.
  • Remove anonymous read. Anonymous read access leaks job names, build logs, and frequently the secrets people accidentally echo into build logs.
  • Grant Overall/Administer to as few accounts as you can live with. Job/Configure is close to admin in practice — a user who can edit a Jenkinsfile can run code on an agent.
  • Use folders to separate teams, and scope permissions per folder rather than globally.

4. Serve everything over TLS

Terminate TLS at nginx, Caddy, or a load balancer in front of Jenkins, or run Jenkins with --httpsPort and a certificate directly.

Then — and this is the step people miss — set the Jenkins URL in Manage Jenkins > System to the https:// address. Jenkins uses that value to build the URLs it hands to agents, plugins and webhook endpoints. Leave it on http:// or on a stale hostname and you will get agents connecting over plaintext and webhook endpoints advertising the wrong address.

While you are there, keep the default CSP for served artifacts. Relaxing hudson.model.DirectoryBrowserSupport.CSP to make an HTML report render is a common and genuinely risky shortcut — it turns build artifacts into a stored-XSS vector against your own admins.

5. Patch the controller and plugins on a schedule

Jenkins publishes security advisories regularly, and the large majority concern plugins, not core.

  • Track the LTS line and upgrade on a cadence you actually keep.
  • Subscribe to the advisory mailing list, or watch the update center's warning badges — Jenkins flags installed plugins with known vulnerabilities directly in the plugin manager.
  • Uninstall plugins you no longer use. Every plugin is code running with controller privileges and its own HTTP endpoints; an unused plugin is pure attack surface.
  • Before installing a plugin, check that it is actively maintained. Abandoned plugins are where unpatched CVEs live.

Disable protocols and endpoints you do not use — the legacy CLI and remoting protocols have a long history of pre-auth RCE.

6. Never build on the controller

Set the controller's executor count to 0 and run every build on an agent.

A build that runs on the controller has direct access to JENKINS_HOME — which contains the credential store, the master encryption key, and every job configuration. Once builds run on agents:

  • Use the Agent → Controller Access Control subsystem rather than disabling it.
  • Prefer ephemeral agents — Kubernetes pods, container agents, or cloud instances that are destroyed after the build — so a poisoned workspace does not persist to the next job.
  • Give each agent only the credentials its jobs need. An agent that builds a public repo should not hold production deploy keys.

7. Treat credentials as the crown jewels

  • Store secrets in the Credentials plugin (or better, an external manager like Vault or a cloud secret store via a plugin), never in a Jenkinsfile or a job's shell step.
  • Scope credentials to folders, not globally. Global credentials are visible to every job that can run.
  • Use withCredentials so secrets are bound for the shortest possible block and masked in logs.
  • Masking is best-effort, not a guarantee. A build that base64s a secret before printing it defeats it — which is another reason to restrict who can edit pipelines.
  • Rotate on a schedule, and immediately after any admin offboards.

8. Keep script security on

Leave the Groovy sandbox enabled for pipeline scripts and use In-process Script Approval deliberately — each approval is a permanent grant of that method to every future script. Blanket-approving to unblock a build is how sandbox escapes get handed out.

Keep CSRF protection enabled (it is the default), and require API tokens rather than passwords for scripted clients. Jenkins returns 403 rather than an authentication challenge when credentials are missing, so scripted clients should send authentication preemptively — see Jenkins' scripted client authentication guide.

9. Make changes reviewable and auditable

  • Install an audit-trail plugin so configuration changes, job runs and credential access are logged somewhere off the controller.
  • Manage configuration with Configuration as Code (JCasC) so the controller's security settings live in version control and drift is a diff rather than a discovery.
  • Ship logs off the box. If the controller is compromised, its local logs are not evidence.
  • Back up JENKINS_HOME — encrypted, because it contains your secrets — and test a restore.

How do you secure a CI/CD pipeline?

The controller is one part. The rest, briefly:

  1. Least privilege for the pipeline's cloud identity. Most real CI breaches are not "someone got into Jenkins" but "a build could assume a role that could do anything."
  2. Separate build and deploy. A build that produces an artifact should not also hold production deploy credentials. Split the jobs and the identities.
  3. Protect the branch, not just the server. If a pull request can change the Jenkinsfile and have it run with production credentials, your access control is in your SCM, not in Jenkins. Restrict which branches trigger privileged jobs.
  4. Pin and review dependencies. Pipelines pull plugins, base images and packages; each is an execution path into your build.
  5. Verify webhook signatures. Trigger endpoints are trigger endpoints even when they are not publicly routable — see the webhook signature verification guide.

The short version

Do thisWhy it matters
Take the controller off the public internetRemoves the largest class of attacks outright
Disable sign-up and anonymous readStops accidental public admin
Use SSO + a matrix/role authorization strategyOffboarding actually works
Set the Jenkins URL and serve over TLSAgents and webhooks stop using plaintext
Patch core and plugins; uninstall unused onesMost advisories are plugin CVEs
Zero executors on the controller; ephemeral agentsA poisoned build cannot read JENKINS_HOME
Folder-scoped credentials, short withCredentials blocksLimits blast radius
Keep the sandbox and CSRF protection onPrevents trivial escalation from job config
JCasC + audit logs shipped off-boxMakes drift and incidents visible

FAQ

Should Jenkins be exposed to the internet? No. The controller holds credentials and executes arbitrary code, and public instances are scanned continuously. If the only reason to expose it is inbound webhooks, an outbound-only relay removes that reason.

Can Jenkins receive webhooks without a public IP? Yes — run a relay agent beside Jenkins so it makes an outbound connection, and give your provider a public input URL that forwards inward. No port forward, no inbound firewall rule. The Jenkins webhook guide has the full setup.

How do I secure Jenkins with SSL? Terminate TLS at a reverse proxy or run Jenkins with --httpsPort, then set the Jenkins URL under Manage Jenkins > System to the https:// address so generated links and agent connections use it.

Is IP allow-listing enough to secure a webhook endpoint? It helps, but it is not sufficient on its own. Provider IP ranges change, and allow-listing one path still leaves the rest of the controller publicly reachable. Combine it with HMAC signature verification, or remove the public exposure entirely.

What is the single highest-impact change? Getting the controller off the public internet. Every other item on this list reduces the damage of a compromise; that one removes most of the opportunities.

If your Jenkins is public today only so a provider can reach /github-webhook/, that is the first thing to fix. Set up an inbound route in a few minutes and close the port.