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.publicRootUrlcontrols:
- 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
| Problem | Cause | Fix |
|---|---|---|
Scanner: PKIX path building failed | CA not trusted in scanner JVM | Import CA into scanner’s cacerts |
Badges show http:// URL | sonar.web.publicRootUrl not updated | Set it in sonar.properties and restart |
| Analysis completes but quality gate never triggers | Webhook using http:// URL | Update webhook URL to https:// |
| 502 Bad Gateway from nginx | SonarQube not started or wrong port | Check systemctl status sonarqube and verify port 9000 |
| Login loop after HTTPS | URL mismatch between browser and publicRootUrl | Set publicRootUrl to the exact URL you use in the browser |
| Large projects fail to upload | client_max_body_size too small | Set 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.