How to Configure Jenkins Behind NGINX with SSL

For a production server, I have been trying to get jenkins running behind NGINX a reverse proxy with SSL enabled. What I faced is that there are many configurations available in wiki.jenkins.io but mostly those configurations worked for the authors with different configurations according to their need.

I have found a couple of useful instructions but they led me nowhere. Because I got the server running behind nginx proxy and the whole css seems to be broken. Then I added some rules (again searching the internet) to route the static contents but it didn’t help me much while trying to keep the ssl up. Those rules work just fine without SSL.

So what was the catch? Most of the online instructions that worked was using a sub-directory to run jenkins, instruction uses --prefix=/jenkins directive that leads to a configuration where the jenkins service is running in a sub-directory. This will lead to an URL like below

https://your-super-awesome-domain.com/jenkins/

But my problem was, that my Jenkins instance was already up and running at the root of the sub-domain. e.g.

http://jenkins.my-awesome-domain.com

Here is the working Nginx configuration and explanation

Goal

https must be enabled to serve a secure instance of jenkins at the following FQDN

https://jenkins.domain.tld

Binding Jenkins to Loopback IP Address

Considering that jenkins service is running at 127.0.0.1:8080

to enable – httplistenAddress to 127.0.0.1 edit the following file(s)

  • /etc/default/jenkins for Ubuntu/Debian based system
  • /etc/sysconfig/jenkins for RedHat based system

if the default jenkins file is not found in one of these locations then please consult Jenkins’s installation guide.

change the file and add the following argument to JENKINS_ARGS, the line should read as below

HTTP_HOST=127.0.0.1
JENKINS_ARGS="--webroot=/var/cache/$NAME/war --httpPort=$HTTP_PORT --httpListenAddress=$HTTP_HOST"

Now, let’s first take a look at a running instance of jenkins behind nginx proxy service without ssl

Configuration for HTTP (Without SSL)

upstream jenkins {
  keepalive 64; # keepalive connections
  server 127.0.0.1:8080; # jenkins ip and port
}

server {
  listen          80;       # Listen on port 80 for IPv4 requests

  server_name     jenkins.domain.tld;

  #this is the jenkins web root directory (mentioned in the /etc/default/jenkins file)
  root            /var/cache/jenkins/war/;

  access_log      /var/log/nginx/jenkins/access.log;
  error_log       /var/log/nginx/jenkins/error.log;

  #pass through headers from Jenkins which are considered invalid by Nginx server.
  ignore_invalid_headers off;

  location ~ "^/static/[0-9a-fA-F]{8}\/(.*)$" {
    #rewrite all static files into requests to the root
    #E.g /static/12345678/css/something.css will become /css/something.css
    rewrite "^/static/[0-9a-fA-F]{8}\/(.*)" /$1 last;
  }

  location /userContent {
    #have nginx handle all the static requests to the userContent folder files
    #note : This is the $JENKINS_HOME dir
        root /var/lib/jenkins/;
    if (!-f $request_filename){
      #this file does not exist, might be a directory or a /**view** url
      rewrite (.*) /$1 last;
          break;
    }
        sendfile on;
  }

  # keep an eye on root location directive,
  # because the reverse proxy is being served from the root of the server block
  location / {
      sendfile off;
      proxy_pass         http://jenkins;
      proxy_redirect     default;  # For http, it's set to default
      proxy_http_version 1.1;

      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_max_temp_file_size 0;

      #this is the maximum upload size
      client_max_body_size       10m;
      client_body_buffer_size    128k;

      proxy_connect_timeout      90;
      proxy_send_timeout         90;
      proxy_read_timeout         90;
      proxy_buffering            off;
      proxy_request_buffering    off; # Required for HTTP CLI commands in Jenkins > 2.54
      proxy_set_header Connection ""; # Clear for keepalive
  }

}

Configuration for HTTPS

For the above configuration to work exactly as it should with SSL enabled, a few changes are required –

  • SSL key and certificates location has been added
  • The listening port has been changed
  • Change in location root (/) directive declaration

NOTE

A few configuration snippets is added with include directives in the next configuration. Those configuration snippets are also provided below.



IMPORTANT

Carefully consider the changes in the following code snippet. Especially the root location / {......} location directive declararion.


upstream jenkins {
  server 127.0.0.1:8080 fail_timeout=0;
}

server {
    listen 443 ssl;

    server_name jenkinsx.domain.tld;
    root /var/cache/jenkins/war/;

    # SSL
    ssl_certificate /etc/nginx/ssl/jenkins.domain.tld.crt;
    ssl_certificate_key /etc/nginx/ssl/jenkins.domain.tld.key;

    # security
    include nginxconfig/security.conf;

    # logging
    access_log /var/log/nginx/jenkins.domain.tld.access.log;
    error_log /var/log/nginx/jenkins.domain.tld.error.log warn;

    ignore_invalid_headers off;

    location ~ "^/static/[0-9a-fA-F]{8}\/(.*)$" {
        #rewrite all static files into requests to the root
        #E.g /static/12345678/css/something.css will become /css/something.css
        rewrite "^/static/[0-9a-fA-F]{8}\/(.*)" /$1 last;
    }

    location /userContent {
        #have nginx handle all the static requests to the userContent folder files
        #note : This is the $JENKINS_HOME dir
        root /var/lib/jenkins/;
        if (!-f $request_filename){
            #this file does not exist, might be a directory or a /**view** url
            rewrite (.*) /$1 last;
            break;
        }
        sendfile on;
    }

    # reverse proxy -> https://wiki.jenkins.io/display/JENKINS/Jenkins+behind+an+nginx+reverse+proxy
    # NOTE :: the regular expression is necessay to make sure that
    # jenkins is being served form the root of the server block

    location ~* ^/ {
        sendfile off;
        proxy_pass         http://jenkins;

        proxy_set_header   Host              $host:$server_port;
        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;
        # For `https` the value changed, it will prevent broken proxy errot message in jenkins GUI
        proxy_redirect     http:// https://;
        proxy_set_header   X-Forwarded-Host  $host;
        proxy_max_temp_file_size 0;

        #this is the maximum upload size
        client_max_body_size       10m;
        client_body_buffer_size    128k;

        proxy_connect_timeout      90;
        proxy_send_timeout         90;
        proxy_read_timeout         90;
        # New HTTP-based CLI
        proxy_http_version         1.1;
        proxy_buffering            off;
        proxy_request_buffering    off; # Required for HTTP CLI commands in Jenkins > 2.54
        proxy_set_header Connection ""; # Clear for keepalive
        # workaround for https://issues.jenkins-ci.org/browse/JENKINS-45651
        add_header 'X-SSH-Endpoint' 'jenkins.domain.tld:50022' always;
    }

    # additional config
    include nginxconfig/general.conf;
}

# subdomains redirect
server {
    listen 443 ssl;

    server_name *.jenkins.domain.tld;

    # SSL
    ssl_certificate /etc/nginx/ssl/jenkins.domain.tld.crt;
    ssl_certificate_key /etc/nginx/ssl/jenkins.domain.tld.key;

    return 301 https://jenkins.domain.tld$request_uri;
}

# HTTP redirect
server {
    listen 80;

    server_name .jenkins.domain.tld;

    return 301 https://jenkins.domain.tld$request_uri;
}

General Configuration Snippet

# favicon.ico
location = /favicon.ico {
    log_not_found off;
    access_log off;
}

# robots.txt
location = /robots.txt {
    log_not_found off;
    access_log off;
}

# assets, media
location ~* \.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
    expires 7d;
    access_log off;
}

# svg, fonts
location ~* \.(?:svgz?|ttf|ttc|otf|eot|woff2?)$ {
    add_header Access-Control-Allow-Origin "*";
    expires 7d;
    access_log off;
}

# gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

Security Configuration Snippet

# security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

# . files
location ~ /\.(?!well-known) {
    deny all;
}

The global nginx (e.g. /etc/nginx/nginx.conf) configuration is not provided here.

Now with these configurations, the SSL is enabled and CSS is working fine.

Enjoy!