qasim@wiki:~$

Jenkins Controller Setup

Install, harden, and configure a Jenkins LTS controller on Ubuntu 24.04 for a private-network CI environment.

Target state

OSUbuntu 24.04 LTS
Jenkins2.555.x LTS (Debian package, official repo)
JavaOpenJDK 17 — required by Jenkins 2.4xx+
Reverse proxyNginx, TLS terminated at the proxy
ExposurePrivate network only. No inbound from internet.
Home/var/lib/jenkins — owned by the jenkins user

Installation

Step 1 — Java runtime

Jenkins 2.4xx and later require Java 17 or 21. Install the headless JDK — no GUI libraries needed on a server.

# Update and install OpenJDK 17
sudo apt update
sudo apt install -y fontconfig openjdk-17-jre-headless

# Verify
java -version
# openjdk version "17.0.x" ...

fontconfig is not optional — Jenkins uses it for chart and badge rendering, and its absence produces confusing runtime errors later.

Step 2 — Jenkins repository and package
# Add the Jenkins signing key
sudo wget -O /usr/share/keyrings/jenkins-keyring.asc \
  https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key

# Add the LTS repository
echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc]" \
     "https://pkg.jenkins.io/debian-stable binary/" \
  | sudo tee /etc/apt/sources.list.d/jenkins.list > /dev/null

sudo apt update
sudo apt install -y jenkins

# Start and enable at boot
sudo systemctl enable --now jenkins
sudo systemctl status jenkins --no-pager

Use debian-stable (LTS), not debian (weekly). LTS gets security backports and is the only sensible choice for a system that gates production deploys.

Step 3 — Unlock and first login
# Initial admin password
sudo cat /var/lib/jenkins/secrets/initialAdminPassword

Browse to http://<host>:8080, paste the password, choose Select plugins to install rather than the suggested set — the suggested list installs a lot you won't use, and each plugin is attack surface plus an upgrade obligation.

Plugin set

The minimum needed for the pipeline in this series. Install via Manage Jenkins → Plugins → Available.

PluginWhy
Pipeline (workflow-aggregator)Declarative pipeline support. Everything else depends on it.
GitSCM checkout and polling.
NodeJSPer-job Node version provisioning via the tools block.
SonarQube ScannerProvides withSonarQubeEnv and waitForQualityGate.
SSH AgentInjects an SSH key into the build for deploy steps.
Email Extension (email-ext)emailext step — richer than the built-in mailer.
Credentials BindingwithCredentials for secrets in shell steps.
TimestamperTimestamps in console output. Invaluable when debugging slow stages.
FoldersFolder-scoped credentials and job organisation (UAT / Staging / Production).
Folder-scoped credentials. With the Folders plugin, a credential added inside the UAT folder is only resolvable by jobs in that folder. This is how you stop a staging job from being able to authenticate against production.

Hardening

Set executors on the controller to 0

By default Jenkins runs builds on the controller itself. That means build scripts execute with access to /var/lib/jenkins — including credentials.xml and the master key. Any pipeline can then read every secret Jenkins holds.

Manage Jenkins → Nodes → Built-In Node → Configure → Number of executors → 0

Consequence to plan for. With 0 executors and no agents configured, every build queues forever showing Waiting for next available executor. Set up an agent before setting this to 0, or you will block all builds. For a small internal setup, a Docker-based agent or a single SSH agent node is enough.

Security realm and authorization

Manage Jenkins → Security

Nginx reverse proxy with TLS

# /etc/nginx/sites-available/jenkins
upstream jenkins { server 127.0.0.1:8080 fail_timeout=0; }

server {
  listen 443 ssl http2;
  server_name jenkins.example.internal;

  ssl_certificate     /etc/ssl/certs/jenkins.crt;
  ssl_certificate_key /etc/ssl/private/jenkins.key;
  ssl_protocols TLSv1.2 TLSv1.3;

  add_header X-Content-Type-Options nosniff always;
  add_header X-Frame-Options SAMEORIGIN always;

  location / {
    proxy_pass         http://jenkins;
    proxy_set_header   Host              $host;
    proxy_set_header   X-Real-IP         $remote_addr;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_redirect     http:// https://;

    # Required for the Jenkins UI's live console streaming
    proxy_http_version 1.1;
    proxy_set_header   Upgrade    $http_upgrade;
    proxy_set_header   Connection $connection_upgrade;

    proxy_buffering off;
    proxy_request_buffering off;
  }
}
# /etc/nginx/nginx.conf — inside the http{} block
map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}
Real failure this fixes. Without the Upgrade / Connection map, WebSocket-dependent parts of the Jenkins UI silently break — plugin install progress bars hang, and some buttons never appear. The page loads fine, so it looks like a UI bug rather than a proxy config problem.

Then bind Jenkins to localhost only, so the proxy is the only path in:

# /etc/default/jenkins  (or systemd override on newer packages)
JENKINS_LISTEN_ADDRESS=127.0.0.1

# Tell Jenkins its external URL
# Manage Jenkins → System → Jenkins Location → Jenkins URL
#   https://jenkins.example.internal/

Firewall

sudo ufw allow from 10.10.0.0/16 to any port 443 proto tcp
sudo ufw deny 8080
sudo ufw enable
sudo ufw status verbose

Global tool configuration

Manage Jenkins → Tools → NodeJS installations → Add NodeJS

Add one entry per Node version your projects actually run. The Name is what the Jenkinsfile references in its tools block, so keep it predictable:

NameVersionUsed by
Node.js 24.11.x24.11.1Projects pinned to 24.11.1 in .nvmrc
Node.js 24.18.x24.18.0Most current-generation client APIs
Node.js 20.20.x20.20.2Legacy project (documented EOL exception)
Name drift is a real bug source. A Jenkinsfile referencing 'Node.js 24.18.x' fails with an unhelpful error if the tool is actually named 'Node.js 24.18'. Match strings exactly, and treat the tool name as an interface.

Build retention

Set on every job (or in the Jenkinsfile options block, covered in Part 4):

options {
  buildDiscarder(logRotator(numToKeepStr: '30'))
  disableConcurrentBuilds()
  timestamps()
}

Verification checklist

#CheckCommand / location
1Service running and enabledsystemctl is-enabled jenkins && systemctl is-active jenkins
2Java version correctjava -version → 17 or 21
3Not listening on public interfacess -lntp | grep 8080 → bound to 127.0.0.1
4TLS works via proxycurl -I https://jenkins.example.internal/login
5Anonymous access deniedOpen in a private window → redirected to login
6Node tools resolveCreate a throwaway pipeline job with tools { nodejs '...' } and sh 'node -v'
7Jenkins URL setManage Jenkins → System → Jenkins Location — must not be blank, or email links break