[{"data":1,"prerenderedAt":1394},["ShallowReactive",2],{"content-query-sLpx2KZnLP":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"layout":10,"head":11,"author":12,"tags":13,"categories":19,"date":20,"faq":21,"excerpt":37,"body":87,"_type":1387,"_id":1388,"_source":1389,"_file":1390,"_stem":1391,"_extension":1392,"sitemap":1393},"/blog/securing-jenkins","blog",false,"","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.","post",{"title":8},"Karolis Rusenas",[14,15,16,17,18],"jenkins","security","ci-cd","devops","webhooks",[15],"2026-09-09 09:00:00",[22,25,28,31,34],{"q":23,"a":24},"How will you secure Jenkins?","Enable an authentication realm and a restrictive authorization strategy, turn off anonymous read and open sign-up, serve the UI over TLS, keep the controller and plugins patched against Jenkins security advisories, run builds on agents rather than the controller, scope credentials to folders, and keep the controller off the public internet. Receive webhooks through an outbound-only relay instead of opening an inbound port.",{"q":26,"a":27},"Should Jenkins be exposed to the internet?","No. A Jenkins controller stores credentials and executes arbitrary build code, so a public controller is a high-value target that is scanned continuously. If you only need inbound webhooks from GitHub, GitLab or Bitbucket, use an outbound-only relay agent so the controller keeps no public IP and no inbound firewall rule.",{"q":29,"a":30},"How do you secure a CI/CD pipeline?","Secure the four layers separately: the controller (authentication, authorization, patching, network exposure), the build environment (ephemeral agents, no builds on the controller), the secrets (scoped credentials, an external secret manager, no plaintext in Jenkinsfiles), and the supply chain (pinned plugins and dependencies, signed artifacts, reviewed pipeline changes).",{"q":32,"a":33},"How do I secure Jenkins with SSL?","Terminate TLS at a reverse proxy such as nginx, Caddy or a load balancer in front of Jenkins, or run Jenkins itself with --httpsPort and a certificate. Then set the Jenkins URL in Manage Jenkins > System to the https address so generated links, webhook endpoints and agent connections all use it.",{"q":35,"a":36},"Can Jenkins receive webhooks without a public IP?","Yes. Run a relay agent next to Jenkins, or anywhere on a network that can reach it. The agent opens an outbound connection to the relay, so provider webhooks arrive at a public HTTPS input URL and are forwarded inward. Jenkins needs no public IP, no port forward and no inbound firewall rule.",{"type":38,"children":39},"root",[40,63,82],{"type":41,"tag":42,"props":43,"children":44},"element","p",{},[45,48,54,56,61],{"type":46,"value":47},"text","A Jenkins controller is two dangerous things in one process: a ",{"type":41,"tag":49,"props":50,"children":51},"strong",{},[52],{"type":46,"value":53},"credential store",{"type":46,"value":55}," and a ",{"type":41,"tag":49,"props":57,"children":58},{},[59],{"type":46,"value":60},"remote code execution service you built on purpose",{"type":46,"value":62},". 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.",{"type":41,"tag":42,"props":64,"children":65},{},[66,68,73,75,80],{"type":46,"value":67},"That is why unauthenticated Jenkins instances get found and cryptomined within hours of being exposed. Securing Jenkins is mostly about shrinking two things: ",{"type":41,"tag":49,"props":69,"children":70},{},[71],{"type":46,"value":72},"who can reach it",{"type":46,"value":74}," and ",{"type":41,"tag":49,"props":76,"children":77},{},[78],{"type":46,"value":79},"what a compromised build can touch",{"type":46,"value":81},".",{"type":41,"tag":42,"props":83,"children":84},{},[85],{"type":46,"value":86},"This is a practical hardening checklist for a self-hosted controller, ordered roughly by how much risk each step removes.",{"type":38,"children":88,"toc":1371},[89,103,117,121,128,133,238,243,249,259,289,294,358,370,460,481,493,522,527,533,545,568,574,579,646,652,665,700,713,719,740,763,768,774,786,799,831,837,877,883,902,937,943,980,985,990,1051,1057,1208,1214,1223,1238,1266,1276,1286,1292,1344,1365],{"type":41,"tag":42,"props":90,"children":91},{},[92,93,97,98,102],{"type":46,"value":47},{"type":41,"tag":49,"props":94,"children":95},{},[96],{"type":46,"value":53},{"type":46,"value":55},{"type":41,"tag":49,"props":99,"children":100},{},[101],{"type":46,"value":60},{"type":46,"value":62},{"type":41,"tag":42,"props":104,"children":105},{},[106,107,111,112,116],{"type":46,"value":67},{"type":41,"tag":49,"props":108,"children":109},{},[110],{"type":46,"value":72},{"type":46,"value":74},{"type":41,"tag":49,"props":113,"children":114},{},[115],{"type":46,"value":79},{"type":46,"value":81},{"type":41,"tag":42,"props":118,"children":119},{},[120],{"type":46,"value":86},{"type":41,"tag":122,"props":123,"children":125},"h2",{"id":124},"what-jenkins-security-actually-covers",[126],{"type":46,"value":127},"What \"Jenkins security\" actually covers",{"type":41,"tag":42,"props":129,"children":130},{},[131],{"type":46,"value":132},"Jenkins security is not one setting. It spans four layers, and a gap in any one of them undoes the others:",{"type":41,"tag":134,"props":135,"children":136},"table",{},[137,161],{"type":41,"tag":138,"props":139,"children":140},"thead",{},[141],{"type":41,"tag":142,"props":143,"children":144},"tr",{},[145,151,156],{"type":41,"tag":146,"props":147,"children":148},"th",{},[149],{"type":46,"value":150},"Layer",{"type":41,"tag":146,"props":152,"children":153},{},[154],{"type":46,"value":155},"What you are protecting",{"type":41,"tag":146,"props":157,"children":158},{},[159],{"type":46,"value":160},"Main controls",{"type":41,"tag":162,"props":163,"children":164},"tbody",{},[165,184,202,220],{"type":41,"tag":142,"props":166,"children":167},{},[168,174,179],{"type":41,"tag":169,"props":170,"children":171},"td",{},[172],{"type":46,"value":173},"Network",{"type":41,"tag":169,"props":175,"children":176},{},[177],{"type":46,"value":178},"Reachability of the controller",{"type":41,"tag":169,"props":180,"children":181},{},[182],{"type":46,"value":183},"Private networking, TLS, no inbound ports",{"type":41,"tag":142,"props":185,"children":186},{},[187,192,197],{"type":41,"tag":169,"props":188,"children":189},{},[190],{"type":46,"value":191},"Access",{"type":41,"tag":169,"props":193,"children":194},{},[195],{"type":46,"value":196},"Who can log in and what they can do",{"type":41,"tag":169,"props":198,"children":199},{},[200],{"type":46,"value":201},"Security realm, authorization strategy, API tokens",{"type":41,"tag":142,"props":203,"children":204},{},[205,210,215],{"type":41,"tag":169,"props":206,"children":207},{},[208],{"type":46,"value":209},"Execution",{"type":41,"tag":169,"props":211,"children":212},{},[213],{"type":46,"value":214},"What a build can do to its host",{"type":41,"tag":169,"props":216,"children":217},{},[218],{"type":46,"value":219},"Agents, script security, ephemeral workspaces",{"type":41,"tag":142,"props":221,"children":222},{},[223,228,233],{"type":41,"tag":169,"props":224,"children":225},{},[226],{"type":46,"value":227},"Supply chain",{"type":41,"tag":169,"props":229,"children":230},{},[231],{"type":46,"value":232},"What code and plugins you run",{"type":41,"tag":169,"props":234,"children":235},{},[236],{"type":46,"value":237},"Patching, plugin review, pinned dependencies",{"type":41,"tag":42,"props":239,"children":240},{},[241],{"type":46,"value":242},"A controller with perfect RBAC that is publicly reachable and three plugin CVEs behind is not secure. Work the list top to bottom.",{"type":41,"tag":122,"props":244,"children":246},{"id":245},"_1-keep-the-controller-off-the-public-internet",[247],{"type":46,"value":248},"1. Keep the controller off the public internet",{"type":41,"tag":42,"props":250,"children":251},{},[252,254],{"type":46,"value":253},"The most common reason a Jenkins controller ends up public is boring: ",{"type":41,"tag":49,"props":255,"children":256},{},[257],{"type":46,"value":258},"someone needed GitHub to deliver a webhook.",{"type":41,"tag":42,"props":260,"children":261},{},[262,264,271,273,279,281,287],{"type":46,"value":263},"The usual fix is to open ",{"type":41,"tag":265,"props":266,"children":268},"code",{"className":267},[],[269],{"type":46,"value":270},":8080",{"type":46,"value":272}," — or put a reverse proxy on ",{"type":41,"tag":265,"props":274,"children":276},{"className":275},[],[277],{"type":46,"value":278},":443",{"type":46,"value":280}," — so that ",{"type":41,"tag":265,"props":282,"children":284},{"className":283},[],[285],{"type":46,"value":286},"https://jenkins.example.com/github-webhook/",{"type":46,"value":288}," 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.",{"type":41,"tag":42,"props":290,"children":291},{},[292],{"type":46,"value":293},"Ranked from weakest to strongest:",{"type":41,"tag":295,"props":296,"children":297},"ul",{},[298,328,338,348],{"type":41,"tag":299,"props":300,"children":301},"li",{},[302,307,309,318,320,326],{"type":41,"tag":49,"props":303,"children":304},{},[305],{"type":46,"value":306},"IP allow-listing the provider's hook ranges.",{"type":46,"value":308}," GitHub publishes its hook source ranges via the ",{"type":41,"tag":310,"props":311,"children":315},"a",{"href":312,"rel":313},"https://docs.github.com/en/rest/meta/meta",[314],"nofollow",[316],{"type":46,"value":317},"meta API",{"type":46,"value":319},", and you can restrict ",{"type":41,"tag":265,"props":321,"children":323},{"className":322},[],[324],{"type":46,"value":325},"/github-webhook/",{"type":46,"value":327}," 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.",{"type":41,"tag":299,"props":329,"children":330},{},[331,336],{"type":41,"tag":49,"props":332,"children":333},{},[334],{"type":46,"value":335},"A reverse proxy that only forwards the webhook path.",{"type":46,"value":337}," 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.",{"type":41,"tag":299,"props":339,"children":340},{},[341,346],{"type":41,"tag":49,"props":342,"children":343},{},[344],{"type":46,"value":345},"A VPN or private network.",{"type":46,"value":347}," Strong for humans, but SaaS providers are not on your VPN — it does not solve webhook delivery at all.",{"type":41,"tag":299,"props":349,"children":350},{},[351,356],{"type":41,"tag":49,"props":352,"children":353},{},[354],{"type":46,"value":355},"An outbound-only relay.",{"type":46,"value":357}," The controller keeps no public IP and no inbound firewall rule.",{"type":41,"tag":42,"props":359,"children":360},{},[361,363,368],{"type":46,"value":362},"That last option is the one that removes the problem rather than narrowing it. An agent running beside Jenkins opens an ",{"type":41,"tag":49,"props":364,"children":365},{},[366],{"type":46,"value":367},"outbound",{"type":46,"value":369}," 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:",{"type":41,"tag":371,"props":372,"children":375},"pre",{"className":373,"code":374,"language":46,"meta":7,"style":7},"language-text shiki shiki-themes github-dark","GitHub / GitLab / Bitbucket / Gitea\n                 |\n                 | HTTPS webhook\n                 v\n        public relay input URL\n                 |\n                 | outbound agent connection (no inbound rule)\n                 v\n     private Jenkins  http://localhost:8080/github-webhook/\n",[376],{"type":41,"tag":265,"props":377,"children":378},{"__ignoreMap":7},[379,390,399,408,417,426,434,443,451],{"type":41,"tag":380,"props":381,"children":384},"span",{"class":382,"line":383},"line",1,[385],{"type":41,"tag":380,"props":386,"children":387},{},[388],{"type":46,"value":389},"GitHub / GitLab / Bitbucket / Gitea\n",{"type":41,"tag":380,"props":391,"children":393},{"class":382,"line":392},2,[394],{"type":41,"tag":380,"props":395,"children":396},{},[397],{"type":46,"value":398},"                 |\n",{"type":41,"tag":380,"props":400,"children":402},{"class":382,"line":401},3,[403],{"type":41,"tag":380,"props":404,"children":405},{},[406],{"type":46,"value":407},"                 | HTTPS webhook\n",{"type":41,"tag":380,"props":409,"children":411},{"class":382,"line":410},4,[412],{"type":41,"tag":380,"props":413,"children":414},{},[415],{"type":46,"value":416},"                 v\n",{"type":41,"tag":380,"props":418,"children":420},{"class":382,"line":419},5,[421],{"type":41,"tag":380,"props":422,"children":423},{},[424],{"type":46,"value":425},"        public relay input URL\n",{"type":41,"tag":380,"props":427,"children":429},{"class":382,"line":428},6,[430],{"type":41,"tag":380,"props":431,"children":432},{},[433],{"type":46,"value":398},{"type":41,"tag":380,"props":435,"children":437},{"class":382,"line":436},7,[438],{"type":41,"tag":380,"props":439,"children":440},{},[441],{"type":46,"value":442},"                 | outbound agent connection (no inbound rule)\n",{"type":41,"tag":380,"props":444,"children":446},{"class":382,"line":445},8,[447],{"type":41,"tag":380,"props":448,"children":449},{},[450],{"type":46,"value":416},{"type":41,"tag":380,"props":452,"children":454},{"class":382,"line":453},9,[455],{"type":41,"tag":380,"props":456,"children":457},{},[458],{"type":46,"value":459},"     private Jenkins  http://localhost:8080/github-webhook/\n",{"type":41,"tag":42,"props":461,"children":462},{},[463,465,471,473,479],{"type":46,"value":464},"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 ",{"type":41,"tag":265,"props":466,"children":468},{"className":467},[],[469],{"type":46,"value":470},"localhost",{"type":46,"value":472},", an RFC1918 address, or a Kubernetes ",{"type":41,"tag":265,"props":474,"children":476},{"className":475},[],[477],{"type":46,"value":478},"ClusterIP",{"type":46,"value":480}," service with no ingress.",{"type":41,"tag":42,"props":482,"children":483},{},[484,486,491],{"type":46,"value":485},"Two things worth being clear about. First, this moves the public endpoint to a third party, so ",{"type":41,"tag":49,"props":487,"children":488},{},[489],{"type":46,"value":490},"keep verifying the provider's HMAC signature at Jenkins",{"type":46,"value":492}," — 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.",{"type":41,"tag":42,"props":494,"children":495},{},[496,498,504,506,512,514,520],{"type":46,"value":497},"Webhook Relay's ",{"type":41,"tag":310,"props":499,"children":501},{"href":500},"/blog/jenkins-webhooks/",[502],{"type":46,"value":503},"Jenkins webhook guide",{"type":46,"value":505}," walks through the GitHub, Bitbucket, GitLab and Gitea setups, and ",{"type":41,"tag":310,"props":507,"children":509},{"href":508},"/features/webhook-to-internal-server/",[510],{"type":46,"value":511},"forwarding to an internal server",{"type":46,"value":513}," covers the general pattern. There is also a ",{"type":41,"tag":310,"props":515,"children":517},{"href":516},"/docs/tutorials/cicd/jenkins-plugin/",[518],{"type":46,"value":519},"Jenkins plugin",{"type":46,"value":521}," if you would rather not run a separate agent process.",{"type":41,"tag":42,"props":523,"children":524},{},[525],{"type":46,"value":526},"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.",{"type":41,"tag":122,"props":528,"children":530},{"id":529},"_2-enable-a-real-security-realm",[531],{"type":46,"value":532},"2. Enable a real security realm",{"type":41,"tag":42,"props":534,"children":535},{},[536,538,543],{"type":46,"value":537},"Under ",{"type":41,"tag":49,"props":539,"children":540},{},[541],{"type":46,"value":542},"Manage Jenkins > Security",{"type":46,"value":544},":",{"type":41,"tag":295,"props":546,"children":547},{},[548,553,563],{"type":41,"tag":299,"props":549,"children":550},{},[551],{"type":46,"value":552},"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.",{"type":41,"tag":299,"props":554,"children":555},{},[556,561],{"type":41,"tag":49,"props":557,"children":558},{},[559],{"type":46,"value":560},"Turn off \"Allow users to sign up.\"",{"type":46,"value":562}," It is the single worst default anyone leaves on. It has repeatedly turned \"internal Jenkins\" into \"anyone with the URL is an admin.\"",{"type":41,"tag":299,"props":564,"children":565},{},[566],{"type":46,"value":567},"Enforce MFA at the identity provider. Jenkins does not do this well natively, which is another argument for SSO.",{"type":41,"tag":122,"props":569,"children":571},{"id":570},"_3-choose-an-authorization-strategy-that-is-not-anything",[572],{"type":46,"value":573},"3. Choose an authorization strategy that is not \"anything\"",{"type":41,"tag":42,"props":575,"children":576},{},[577],{"type":46,"value":578},"The default \"Logged-in users can do anything\" is a starting point, not a configuration.",{"type":41,"tag":295,"props":580,"children":581},{},[582,608,620,641],{"type":41,"tag":299,"props":583,"children":584},{},[585,587,592,594,599,601,606],{"type":46,"value":586},"Use ",{"type":41,"tag":49,"props":588,"children":589},{},[590],{"type":46,"value":591},"Matrix-based security",{"type":46,"value":593},", ",{"type":41,"tag":49,"props":595,"children":596},{},[597],{"type":46,"value":598},"Project-based Matrix Authorization",{"type":46,"value":600},", or the ",{"type":41,"tag":49,"props":602,"children":603},{},[604],{"type":46,"value":605},"Role-based Authorization Strategy",{"type":46,"value":607}," plugin.",{"type":41,"tag":299,"props":609,"children":610},{},[611,613,618],{"type":46,"value":612},"Remove ",{"type":41,"tag":49,"props":614,"children":615},{},[616],{"type":46,"value":617},"anonymous read",{"type":46,"value":619},". Anonymous read access leaks job names, build logs, and frequently the secrets people accidentally echo into build logs.",{"type":41,"tag":299,"props":621,"children":622},{},[623,625,631,633,639],{"type":46,"value":624},"Grant ",{"type":41,"tag":265,"props":626,"children":628},{"className":627},[],[629],{"type":46,"value":630},"Overall/Administer",{"type":46,"value":632}," to as few accounts as you can live with. ",{"type":41,"tag":265,"props":634,"children":636},{"className":635},[],[637],{"type":46,"value":638},"Job/Configure",{"type":46,"value":640}," is close to admin in practice — a user who can edit a Jenkinsfile can run code on an agent.",{"type":41,"tag":299,"props":642,"children":643},{},[644],{"type":46,"value":645},"Use folders to separate teams, and scope permissions per folder rather than globally.",{"type":41,"tag":122,"props":647,"children":649},{"id":648},"_4-serve-everything-over-tls",[650],{"type":46,"value":651},"4. Serve everything over TLS",{"type":41,"tag":42,"props":653,"children":654},{},[655,657,663],{"type":46,"value":656},"Terminate TLS at nginx, Caddy, or a load balancer in front of Jenkins, or run Jenkins with ",{"type":41,"tag":265,"props":658,"children":660},{"className":659},[],[661],{"type":46,"value":662},"--httpsPort",{"type":46,"value":664}," and a certificate directly.",{"type":41,"tag":42,"props":666,"children":667},{},[668,670,675,677,682,684,690,692,698],{"type":46,"value":669},"Then — and this is the step people miss — set the ",{"type":41,"tag":49,"props":671,"children":672},{},[673],{"type":46,"value":674},"Jenkins URL",{"type":46,"value":676}," in ",{"type":41,"tag":49,"props":678,"children":679},{},[680],{"type":46,"value":681},"Manage Jenkins > System",{"type":46,"value":683}," to the ",{"type":41,"tag":265,"props":685,"children":687},{"className":686},[],[688],{"type":46,"value":689},"https://",{"type":46,"value":691}," address. Jenkins uses that value to build the URLs it hands to agents, plugins and webhook endpoints. Leave it on ",{"type":41,"tag":265,"props":693,"children":695},{"className":694},[],[696],{"type":46,"value":697},"http://",{"type":46,"value":699}," or on a stale hostname and you will get agents connecting over plaintext and webhook endpoints advertising the wrong address.",{"type":41,"tag":42,"props":701,"children":702},{},[703,705,711],{"type":46,"value":704},"While you are there, keep the default CSP for served artifacts. Relaxing ",{"type":41,"tag":265,"props":706,"children":708},{"className":707},[],[709],{"type":46,"value":710},"hudson.model.DirectoryBrowserSupport.CSP",{"type":46,"value":712}," 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.",{"type":41,"tag":122,"props":714,"children":716},{"id":715},"_5-patch-the-controller-and-plugins-on-a-schedule",[717],{"type":46,"value":718},"5. Patch the controller and plugins on a schedule",{"type":41,"tag":42,"props":720,"children":721},{},[722,724,731,733,738],{"type":46,"value":723},"Jenkins publishes ",{"type":41,"tag":310,"props":725,"children":728},{"href":726,"rel":727},"https://www.jenkins.io/security/advisories/",[314],[729],{"type":46,"value":730},"security advisories",{"type":46,"value":732}," regularly, and the large majority concern ",{"type":41,"tag":49,"props":734,"children":735},{},[736],{"type":46,"value":737},"plugins",{"type":46,"value":739},", not core.",{"type":41,"tag":295,"props":741,"children":742},{},[743,748,753,758],{"type":41,"tag":299,"props":744,"children":745},{},[746],{"type":46,"value":747},"Track the LTS line and upgrade on a cadence you actually keep.",{"type":41,"tag":299,"props":749,"children":750},{},[751],{"type":46,"value":752},"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.",{"type":41,"tag":299,"props":754,"children":755},{},[756],{"type":46,"value":757},"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.",{"type":41,"tag":299,"props":759,"children":760},{},[761],{"type":46,"value":762},"Before installing a plugin, check that it is actively maintained. Abandoned plugins are where unpatched CVEs live.",{"type":41,"tag":42,"props":764,"children":765},{},[766],{"type":46,"value":767},"Disable protocols and endpoints you do not use — the legacy CLI and remoting protocols have a long history of pre-auth RCE.",{"type":41,"tag":122,"props":769,"children":771},{"id":770},"_6-never-build-on-the-controller",[772],{"type":46,"value":773},"6. Never build on the controller",{"type":41,"tag":42,"props":775,"children":776},{},[777,779,784],{"type":46,"value":778},"Set the controller's executor count to ",{"type":41,"tag":49,"props":780,"children":781},{},[782],{"type":46,"value":783},"0",{"type":46,"value":785}," and run every build on an agent.",{"type":41,"tag":42,"props":787,"children":788},{},[789,791,797],{"type":46,"value":790},"A build that runs on the controller has direct access to ",{"type":41,"tag":265,"props":792,"children":794},{"className":793},[],[795],{"type":46,"value":796},"JENKINS_HOME",{"type":46,"value":798}," — which contains the credential store, the master encryption key, and every job configuration. Once builds run on agents:",{"type":41,"tag":295,"props":800,"children":801},{},[802,814,826],{"type":41,"tag":299,"props":803,"children":804},{},[805,807,812],{"type":46,"value":806},"Use the ",{"type":41,"tag":49,"props":808,"children":809},{},[810],{"type":46,"value":811},"Agent → Controller Access Control",{"type":46,"value":813}," subsystem rather than disabling it.",{"type":41,"tag":299,"props":815,"children":816},{},[817,819,824],{"type":46,"value":818},"Prefer ",{"type":41,"tag":49,"props":820,"children":821},{},[822],{"type":46,"value":823},"ephemeral agents",{"type":46,"value":825}," — Kubernetes pods, container agents, or cloud instances that are destroyed after the build — so a poisoned workspace does not persist to the next job.",{"type":41,"tag":299,"props":827,"children":828},{},[829],{"type":46,"value":830},"Give each agent only the credentials its jobs need. An agent that builds a public repo should not hold production deploy keys.",{"type":41,"tag":122,"props":832,"children":834},{"id":833},"_7-treat-credentials-as-the-crown-jewels",[835],{"type":46,"value":836},"7. Treat credentials as the crown jewels",{"type":41,"tag":295,"props":838,"children":839},{},[840,845,855,867,872],{"type":41,"tag":299,"props":841,"children":842},{},[843],{"type":46,"value":844},"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.",{"type":41,"tag":299,"props":846,"children":847},{},[848,853],{"type":41,"tag":49,"props":849,"children":850},{},[851],{"type":46,"value":852},"Scope credentials to folders",{"type":46,"value":854},", not globally. Global credentials are visible to every job that can run.",{"type":41,"tag":299,"props":856,"children":857},{},[858,859,865],{"type":46,"value":586},{"type":41,"tag":265,"props":860,"children":862},{"className":861},[],[863],{"type":46,"value":864},"withCredentials",{"type":46,"value":866}," so secrets are bound for the shortest possible block and masked in logs.",{"type":41,"tag":299,"props":868,"children":869},{},[870],{"type":46,"value":871},"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.",{"type":41,"tag":299,"props":873,"children":874},{},[875],{"type":46,"value":876},"Rotate on a schedule, and immediately after any admin offboards.",{"type":41,"tag":122,"props":878,"children":880},{"id":879},"_8-keep-script-security-on",[881],{"type":46,"value":882},"8. Keep script security on",{"type":41,"tag":42,"props":884,"children":885},{},[886,888,893,895,900],{"type":46,"value":887},"Leave the ",{"type":41,"tag":49,"props":889,"children":890},{},[891],{"type":46,"value":892},"Groovy sandbox",{"type":46,"value":894}," enabled for pipeline scripts and use ",{"type":41,"tag":49,"props":896,"children":897},{},[898],{"type":46,"value":899},"In-process Script Approval",{"type":46,"value":901}," 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.",{"type":41,"tag":42,"props":903,"children":904},{},[905,907,912,914,919,921,927,929,936],{"type":46,"value":906},"Keep ",{"type":41,"tag":49,"props":908,"children":909},{},[910],{"type":46,"value":911},"CSRF protection",{"type":46,"value":913}," enabled (it is the default), and require ",{"type":41,"tag":49,"props":915,"children":916},{},[917],{"type":46,"value":918},"API tokens",{"type":46,"value":920}," rather than passwords for scripted clients. Jenkins returns ",{"type":41,"tag":265,"props":922,"children":924},{"className":923},[],[925],{"type":46,"value":926},"403",{"type":46,"value":928}," rather than an authentication challenge when credentials are missing, so scripted clients should send authentication preemptively — see Jenkins' ",{"type":41,"tag":310,"props":930,"children":933},{"href":931,"rel":932},"https://www.jenkins.io/doc/book/system-administration/authenticating-scripted-clients/",[314],[934],{"type":46,"value":935},"scripted client authentication guide",{"type":46,"value":81},{"type":41,"tag":122,"props":938,"children":940},{"id":939},"_9-make-changes-reviewable-and-auditable",[941],{"type":46,"value":942},"9. Make changes reviewable and auditable",{"type":41,"tag":295,"props":944,"children":945},{},[946,951,963,968],{"type":41,"tag":299,"props":947,"children":948},{},[949],{"type":46,"value":950},"Install an audit-trail plugin so configuration changes, job runs and credential access are logged somewhere off the controller.",{"type":41,"tag":299,"props":952,"children":953},{},[954,956,961],{"type":46,"value":955},"Manage configuration with ",{"type":41,"tag":49,"props":957,"children":958},{},[959],{"type":46,"value":960},"Configuration as Code (JCasC)",{"type":46,"value":962}," so the controller's security settings live in version control and drift is a diff rather than a discovery.",{"type":41,"tag":299,"props":964,"children":965},{},[966],{"type":46,"value":967},"Ship logs off the box. If the controller is compromised, its local logs are not evidence.",{"type":41,"tag":299,"props":969,"children":970},{},[971,973,978],{"type":46,"value":972},"Back up ",{"type":41,"tag":265,"props":974,"children":976},{"className":975},[],[977],{"type":46,"value":796},{"type":46,"value":979}," — encrypted, because it contains your secrets — and test a restore.",{"type":41,"tag":122,"props":981,"children":983},{"id":982},"how-do-you-secure-a-cicd-pipeline",[984],{"type":46,"value":29},{"type":41,"tag":42,"props":986,"children":987},{},[988],{"type":46,"value":989},"The controller is one part. The rest, briefly:",{"type":41,"tag":991,"props":992,"children":993},"ol",{},[994,1004,1014,1024,1034],{"type":41,"tag":299,"props":995,"children":996},{},[997,1002],{"type":41,"tag":49,"props":998,"children":999},{},[1000],{"type":46,"value":1001},"Least privilege for the pipeline's cloud identity.",{"type":46,"value":1003}," Most real CI breaches are not \"someone got into Jenkins\" but \"a build could assume a role that could do anything.\"",{"type":41,"tag":299,"props":1005,"children":1006},{},[1007,1012],{"type":41,"tag":49,"props":1008,"children":1009},{},[1010],{"type":46,"value":1011},"Separate build and deploy.",{"type":46,"value":1013}," A build that produces an artifact should not also hold production deploy credentials. Split the jobs and the identities.",{"type":41,"tag":299,"props":1015,"children":1016},{},[1017,1022],{"type":41,"tag":49,"props":1018,"children":1019},{},[1020],{"type":46,"value":1021},"Protect the branch, not just the server.",{"type":46,"value":1023}," 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.",{"type":41,"tag":299,"props":1025,"children":1026},{},[1027,1032],{"type":41,"tag":49,"props":1028,"children":1029},{},[1030],{"type":46,"value":1031},"Pin and review dependencies.",{"type":46,"value":1033}," Pipelines pull plugins, base images and packages; each is an execution path into your build.",{"type":41,"tag":299,"props":1035,"children":1036},{},[1037,1042,1044,1050],{"type":41,"tag":49,"props":1038,"children":1039},{},[1040],{"type":46,"value":1041},"Verify webhook signatures.",{"type":46,"value":1043}," Trigger endpoints are trigger endpoints even when they are not publicly routable — see the ",{"type":41,"tag":310,"props":1045,"children":1047},{"href":1046},"/blog/verify-webhook-signature/",[1048],{"type":46,"value":1049},"webhook signature verification guide",{"type":46,"value":81},{"type":41,"tag":122,"props":1052,"children":1054},{"id":1053},"the-short-version",[1055],{"type":46,"value":1056},"The short version",{"type":41,"tag":134,"props":1058,"children":1059},{},[1060,1076],{"type":41,"tag":138,"props":1061,"children":1062},{},[1063],{"type":41,"tag":142,"props":1064,"children":1065},{},[1066,1071],{"type":41,"tag":146,"props":1067,"children":1068},{},[1069],{"type":46,"value":1070},"Do this",{"type":41,"tag":146,"props":1072,"children":1073},{},[1074],{"type":46,"value":1075},"Why it matters",{"type":41,"tag":162,"props":1077,"children":1078},{},[1079,1092,1105,1118,1131,1144,1162,1182,1195],{"type":41,"tag":142,"props":1080,"children":1081},{},[1082,1087],{"type":41,"tag":169,"props":1083,"children":1084},{},[1085],{"type":46,"value":1086},"Take the controller off the public internet",{"type":41,"tag":169,"props":1088,"children":1089},{},[1090],{"type":46,"value":1091},"Removes the largest class of attacks outright",{"type":41,"tag":142,"props":1093,"children":1094},{},[1095,1100],{"type":41,"tag":169,"props":1096,"children":1097},{},[1098],{"type":46,"value":1099},"Disable sign-up and anonymous read",{"type":41,"tag":169,"props":1101,"children":1102},{},[1103],{"type":46,"value":1104},"Stops accidental public admin",{"type":41,"tag":142,"props":1106,"children":1107},{},[1108,1113],{"type":41,"tag":169,"props":1109,"children":1110},{},[1111],{"type":46,"value":1112},"Use SSO + a matrix/role authorization strategy",{"type":41,"tag":169,"props":1114,"children":1115},{},[1116],{"type":46,"value":1117},"Offboarding actually works",{"type":41,"tag":142,"props":1119,"children":1120},{},[1121,1126],{"type":41,"tag":169,"props":1122,"children":1123},{},[1124],{"type":46,"value":1125},"Set the Jenkins URL and serve over TLS",{"type":41,"tag":169,"props":1127,"children":1128},{},[1129],{"type":46,"value":1130},"Agents and webhooks stop using plaintext",{"type":41,"tag":142,"props":1132,"children":1133},{},[1134,1139],{"type":41,"tag":169,"props":1135,"children":1136},{},[1137],{"type":46,"value":1138},"Patch core and plugins; uninstall unused ones",{"type":41,"tag":169,"props":1140,"children":1141},{},[1142],{"type":46,"value":1143},"Most advisories are plugin CVEs",{"type":41,"tag":142,"props":1145,"children":1146},{},[1147,1152],{"type":41,"tag":169,"props":1148,"children":1149},{},[1150],{"type":46,"value":1151},"Zero executors on the controller; ephemeral agents",{"type":41,"tag":169,"props":1153,"children":1154},{},[1155,1157],{"type":46,"value":1156},"A poisoned build cannot read ",{"type":41,"tag":265,"props":1158,"children":1160},{"className":1159},[],[1161],{"type":46,"value":796},{"type":41,"tag":142,"props":1163,"children":1164},{},[1165,1177],{"type":41,"tag":169,"props":1166,"children":1167},{},[1168,1170,1175],{"type":46,"value":1169},"Folder-scoped credentials, short ",{"type":41,"tag":265,"props":1171,"children":1173},{"className":1172},[],[1174],{"type":46,"value":864},{"type":46,"value":1176}," blocks",{"type":41,"tag":169,"props":1178,"children":1179},{},[1180],{"type":46,"value":1181},"Limits blast radius",{"type":41,"tag":142,"props":1183,"children":1184},{},[1185,1190],{"type":41,"tag":169,"props":1186,"children":1187},{},[1188],{"type":46,"value":1189},"Keep the sandbox and CSRF protection on",{"type":41,"tag":169,"props":1191,"children":1192},{},[1193],{"type":46,"value":1194},"Prevents trivial escalation from job config",{"type":41,"tag":142,"props":1196,"children":1197},{},[1198,1203],{"type":41,"tag":169,"props":1199,"children":1200},{},[1201],{"type":46,"value":1202},"JCasC + audit logs shipped off-box",{"type":41,"tag":169,"props":1204,"children":1205},{},[1206],{"type":46,"value":1207},"Makes drift and incidents visible",{"type":41,"tag":122,"props":1209,"children":1211},{"id":1210},"faq",[1212],{"type":46,"value":1213},"FAQ",{"type":41,"tag":42,"props":1215,"children":1216},{},[1217,1221],{"type":41,"tag":49,"props":1218,"children":1219},{},[1220],{"type":46,"value":26},{"type":46,"value":1222}," 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.",{"type":41,"tag":42,"props":1224,"children":1225},{},[1226,1230,1232,1236],{"type":41,"tag":49,"props":1227,"children":1228},{},[1229],{"type":46,"value":35},{"type":46,"value":1231}," 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 ",{"type":41,"tag":310,"props":1233,"children":1234},{"href":500},[1235],{"type":46,"value":503},{"type":46,"value":1237}," has the full setup.",{"type":41,"tag":42,"props":1239,"children":1240},{},[1241,1245,1247,1252,1254,1258,1259,1264],{"type":41,"tag":49,"props":1242,"children":1243},{},[1244],{"type":46,"value":32},{"type":46,"value":1246}," Terminate TLS at a reverse proxy or run Jenkins with ",{"type":41,"tag":265,"props":1248,"children":1250},{"className":1249},[],[1251],{"type":46,"value":662},{"type":46,"value":1253},", then set the Jenkins URL under ",{"type":41,"tag":49,"props":1255,"children":1256},{},[1257],{"type":46,"value":681},{"type":46,"value":683},{"type":41,"tag":265,"props":1260,"children":1262},{"className":1261},[],[1263],{"type":46,"value":689},{"type":46,"value":1265}," address so generated links and agent connections use it.",{"type":41,"tag":42,"props":1267,"children":1268},{},[1269,1274],{"type":41,"tag":49,"props":1270,"children":1271},{},[1272],{"type":46,"value":1273},"Is IP allow-listing enough to secure a webhook endpoint?",{"type":46,"value":1275}," 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.",{"type":41,"tag":42,"props":1277,"children":1278},{},[1279,1284],{"type":41,"tag":49,"props":1280,"children":1281},{},[1282],{"type":46,"value":1283},"What is the single highest-impact change?",{"type":46,"value":1285}," 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.",{"type":41,"tag":122,"props":1287,"children":1289},{"id":1288},"related-reading",[1290],{"type":46,"value":1291},"Related reading",{"type":41,"tag":295,"props":1293,"children":1294},{},[1295,1303,1311,1320,1328],{"type":41,"tag":299,"props":1296,"children":1297},{},[1298],{"type":41,"tag":310,"props":1299,"children":1300},{"href":500},[1301],{"type":46,"value":1302},"Jenkins webhooks: the complete setup guide",{"type":41,"tag":299,"props":1304,"children":1305},{},[1306],{"type":41,"tag":310,"props":1307,"children":1308},{"href":508},[1309],{"type":46,"value":1310},"Forward webhooks to an internal server",{"type":41,"tag":299,"props":1312,"children":1313},{},[1314],{"type":41,"tag":310,"props":1315,"children":1317},{"href":1316},"/blog/webhook-security/",[1318],{"type":46,"value":1319},"Webhook security best practices",{"type":41,"tag":299,"props":1321,"children":1322},{},[1323],{"type":41,"tag":310,"props":1324,"children":1325},{"href":1046},[1326],{"type":46,"value":1327},"How to verify a webhook signature",{"type":41,"tag":299,"props":1329,"children":1330},{},[1331,1337,1338],{"type":41,"tag":310,"props":1332,"children":1334},{"href":1333},"/docs/tutorials/cicd/jenkins-github/",[1335],{"type":46,"value":1336},"Jenkins + GitHub setup",{"type":46,"value":74},{"type":41,"tag":310,"props":1339,"children":1341},{"href":1340},"/docs/tutorials/cicd/jenkins-bitbucket/",[1342],{"type":46,"value":1343},"Jenkins + Bitbucket setup",{"type":41,"tag":42,"props":1345,"children":1346},{},[1347,1349,1354,1356,1363],{"type":46,"value":1348},"If your Jenkins is public today only so a provider can reach ",{"type":41,"tag":265,"props":1350,"children":1352},{"className":1351},[],[1353],{"type":46,"value":325},{"type":46,"value":1355},", that is the first thing to fix. ",{"type":41,"tag":310,"props":1357,"children":1360},{"href":1358,"rel":1359},"https://my.webhookrelay.com/register",[314],[1361],{"type":46,"value":1362},"Set up an inbound route in a few minutes",{"type":46,"value":1364}," and close the port.",{"type":41,"tag":1366,"props":1367,"children":1368},"style",{},[1369],{"type":46,"value":1370},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":401,"depth":401,"links":1372},[1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386],{"id":124,"depth":392,"text":127},{"id":245,"depth":392,"text":248},{"id":529,"depth":392,"text":532},{"id":570,"depth":392,"text":573},{"id":648,"depth":392,"text":651},{"id":715,"depth":392,"text":718},{"id":770,"depth":392,"text":773},{"id":833,"depth":392,"text":836},{"id":879,"depth":392,"text":882},{"id":939,"depth":392,"text":942},{"id":982,"depth":392,"text":29},{"id":1053,"depth":392,"text":1056},{"id":1210,"depth":392,"text":1213},{"id":1288,"depth":392,"text":1291},"markdown","content:blog:securing-jenkins.md","content","blog/securing-jenkins.md","blog/securing-jenkins","md",{"loc":4},1788902387379]