SonarQube HTTPS Configuration: Putting the Code Quality Platform Behind a TLS Proxy

SonarQube runs an Elasticsearch node internally and serves its web interface on port 9000. It does not handle TLS natively — it is designed to be deployed behind a reverse proxy. This article covers setting up nginx as a TLS reverse proxy in front of SonarQube, configuring the sonar.web.publicRootUrl property so that generated links are correct, and fixing the scanner configuration so CI pipelines can push analysis results over HTTPS.

Why SonarQube needs a reverse proxy for TLS

SonarQube’s built-in web server (based on Tomcat) does not include TLS termination in the default distribution. The recommended architecture from Sonar’s documentation explicitly places a reverse proxy in front. Additionally:

  • SonarQube generates badge URLs, webhook callback URLs, and scanner URLs. All of these use sonar.web.publicRootUrl, which must be the HTTPS URL.
  • The scanner (sonar-scanner, Maven/Gradle plugins) needs the HTTPS URL to push results.
  • SonarQube webhooks (used to notify CI systems of quality gate results) must use HTTPS to be received by most CI systems.

Step 1: Obtain a certificate

certbot certonly --standalone -d sonarqube.example.com

Step 2: Configure nginx

Create /etc/nginx/sites-available/sonarqube:

server {
    listen 80;
    server_name sonarqube.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name sonarqube.example.com;

    ssl_certificate     /etc/letsencrypt/live/sonarqube.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/sonarqube.example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Frame-Options SAMEORIGIN always;
    add_header X-Content-Type-Options nosniff always;

    # Increase timeouts for analysis uploads — analysis results can be large
    proxy_read_timeout    600s;
    proxy_connect_timeout  30s;
    proxy_send_timeout    600s;

    # Increase upload size for large projects
    client_max_body_size 100M;

    location / {
        proxy_pass         http://127.0.0.1:9000;
        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;

        # Required for proper redirect handling
        proxy_redirect     http:// https://;
    }
}

Enable the site:

sudo ln -s /etc/nginx/sites-available/sonarqube /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Step 3: Configure SonarQube’s public URL

Open /opt/sonarqube/conf/sonar.properties (or /etc/sonarqube/sonar.properties depending on installation):

sudo nano /opt/sonarqube/conf/sonar.properties

Set:

# The public URL — must match what clients use to connect
sonar.web.publicRootUrl=https://sonarqube.example.com

# Bind to localhost only (nginx handles external access)
sonar.web.host=127.0.0.1
sonar.web.port=9000

# Context path (leave empty unless running under a subpath)
sonar.web.context=

Important: sonar.web.publicRootUrl controls:

  • URLs in email notifications
  • Badge URLs
  • Webhook callback URLs
  • The URL shown in scanner output

If this is wrong, scanners will still work but all generated links will be broken.

Restart SonarQube:

sudo systemctl restart sonarqube
sudo systemctl status sonarqube
sudo tail -100 /opt/sonarqube/logs/web.log

Step 4: Verify the HTTPS interface

curl -I https://sonarqube.example.com

# Check the SSL certificate
openssl s_client -connect sonarqube.example.com:443 -servername sonarqube.example.com </dev/null 2>&1 \
  | openssl x509 -noout -subject -dates

Navigate to https://sonarqube.example.com in a browser and log in. Go to Administration → System → System Info and verify that Server Base URL shows https://sonarqube.example.com.

Step 5: Update sonar-scanner to use HTTPS

The sonar-scanner (or Maven/Gradle plugin) pushes analysis results to SonarQube. After enabling HTTPS, update the scanner configuration.

Global scanner configuration at ~/.sonar/sonar-scanner.properties (or per-project in sonar-project.properties):

sonar.host.url=https://sonarqube.example.com
sonar.login=your-sonarqube-token

Maven (pom.xml or command line):

mvn sonar:sonar \
  -Dsonar.host.url=https://sonarqube.example.com \
  -Dsonar.login=${SONAR_TOKEN}

Gradle (build.gradle):

sonarqube {
    properties {
        property "sonar.host.url", "https://sonarqube.example.com"
        property "sonar.login", System.env.SONAR_TOKEN
    }
}

GitHub Actions:

- name: SonarQube Scan
  env:
    SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
    SONAR_HOST_URL: https://sonarqube.example.com
  run: |
    sonar-scanner \
      -Dsonar.projectKey=my-project \
      -Dsonar.sources=.

GitLab CI:

sonarqube:
  stage: analysis
  script:
    - sonar-scanner
      -Dsonar.projectKey=my-project
      -Dsonar.host.url=https://sonarqube.example.com
      -Dsonar.login=${SONAR_TOKEN}

Trusting a private CA in the scanner’s JVM

If SonarQube is using a certificate signed by an internal CA, the scanner’s JVM must trust it. The sonar-scanner bundles its own JRE.

Add the CA to the scanner’s trust store:

# Find the scanner's JRE
SCANNER_JRE=$(find /opt/sonar-scanner -name "jre" -type d 2>/dev/null | head -1)

# Import the CA
${SCANNER_JRE}/bin/keytool -import -alias internal-ca \
  -keystore ${SCANNER_JRE}/lib/security/cacerts \
  -file /etc/ssl/certs/internal-ca.crt \
  -storepass changeit -noprompt

Alternatively, set the JVM trust store as an environment variable:

export SONAR_SCANNER_OPTS="-Djavax.net.ssl.trustStore=/path/to/truststore.jks -Djavax.net.ssl.trustStorePassword=changeit"

Setting up HTTPS webhooks for CI quality gates

SonarQube webhooks notify CI systems (like Jenkins or GitHub Actions) when analysis is complete. For the webhook to work, SonarQube must be able to reach the CI system via HTTPS.

Configure webhooks in Administration → Configuration → Webhooks (global) or Project Settings → Webhooks (per project):

  • URL: https://jenkins.example.com/sonarqube-webhook/ (or equivalent)
  • Secret: a shared secret for webhook signature verification

If SonarQube uses a private CA and the CI system’s certificate is signed by it, you may need to add the CI system’s CA to SonarQube’s JVM trust store as well:

${JAVA_HOME}/bin/keytool -import -alias ci-ca \
  -keystore ${JAVA_HOME}/lib/security/cacerts \
  -file /etc/ssl/certs/ci-ca.crt \
  -storepass changeit -noprompt

sudo systemctl restart sonarqube

SonarQube Docker deployment with TLS

For Docker deployments, use an environment variable for the public URL:

version: '3.8'
services:
  sonarqube:
    image: sonarqube:community
    environment:
      SONAR_WEB_PUBLICROOTURL: https://sonarqube.example.com
      SONAR_WEB_HOST: 0.0.0.0
      SONAR_WEB_PORT: "9000"
    ports:
      - "127.0.0.1:9000:9000"
    volumes:
      - sonarqube_data:/opt/sonarqube/data
      - sonarqube_logs:/opt/sonarqube/logs

Certificate renewal

sudo nano /etc/letsencrypt/renewal-hooks/deploy/sonarqube.sh
#!/bin/bash
systemctl reload nginx
# SonarQube itself does not need restarting — nginx handles TLS
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/sonarqube.sh

Troubleshooting

ProblemCauseFix
Scanner: PKIX path building failedCA not trusted in scanner JVMImport CA into scanner’s cacerts
Badges show http:// URLsonar.web.publicRootUrl not updatedSet it in sonar.properties and restart
Analysis completes but quality gate never triggersWebhook using http:// URLUpdate webhook URL to https://
502 Bad Gateway from nginxSonarQube not started or wrong portCheck systemctl status sonarqube and verify port 9000
Login loop after HTTPSURL mismatch between browser and publicRootUrlSet publicRootUrl to the exact URL you use in the browser
Large projects fail to uploadclient_max_body_size too smallSet client_max_body_size 100M in nginx

Summary

SonarQube TLS is a two-step process: configure nginx to terminate TLS and proxy to 127.0.0.1:9000, then set sonar.web.publicRootUrl=https://sonarqube.example.com in sonar.properties so that all generated URLs use HTTPS. Update all scanners, CI pipeline configurations, and webhooks to the HTTPS URL. If you use a private CA, import it into both the SonarQube JVM and any scanner JREs that run on separate build agents.

Scroll to Top