Grafana SSL/TLS Certificate Configuration: Enabling HTTPS on Your Monitoring Dashboard

Grafana ships with HTTP enabled by default. That is fine for a local lab, but the moment Grafana is reachable over any network you do not fully control, you need TLS. I learned that lesson when a penetration test caught session tokens traveling in the clear between an on-call engineer’s laptop and a Grafana instance that was “only on the internal network”. This article walks through every step of enabling TLS on Grafana, replacing an expiring certificate, and testing that the configuration actually works.

What changes when you enable TLS in Grafana

Grafana’s built-in web server (written in Go) handles TLS natively. You do not need nginx or Apache in front of it unless you want to. When TLS is enabled, Grafana reads a certificate and key file from disk and serves HTTPS directly on port 3000 (or whichever port you configured). The relevant settings all live in grafana.ini under the [server] section.

Prerequisites

  • Grafana 9.x or later installed on Linux (these steps also apply to Grafana 10/11)
  • A valid certificate and private key in PEM format
  • Root or sudo access to the Grafana host
  • A DNS record pointing your domain to the server

If you do not yet have a certificate, obtain one from Let’s Encrypt:

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

The certificate will be written to /etc/letsencrypt/live/grafana.example.com/.

Step 1: Locate and back up grafana.ini

On most Linux distributions Grafana’s configuration file is at /etc/grafana/grafana.ini. Before touching it:

sudo cp /etc/grafana/grafana.ini /etc/grafana/grafana.ini.bak-$(date +%Y%m%d)

Step 2: Edit the [server] section

Open the file with your preferred editor:

sudo nano /etc/grafana/grafana.ini

Find the [server] section and set the following values:

[server]
# The public-facing URL — must match your certificate's Common Name or SAN
root_url = https://grafana.example.com

# Switch the protocol to https
protocol = https

# Port Grafana listens on (keep 3000 or use 443 if running as root / with cap_net_bind_service)
http_port = 3000

# Full paths to your certificate and key
cert_file = /etc/letsencrypt/live/grafana.example.com/fullchain.pem
cert_key  = /etc/letsencrypt/live/grafana.example.com/privkey.pem

If you are using a certificate issued by your own internal CA, point cert_file at the leaf certificate (or the full chain including intermediates) and cert_key at the matching private key.

Step 3: Fix file permissions

Grafana runs as the grafana user. Let’s Encrypt private keys are mode 0600 owned by root, so you need to allow the Grafana user to read them without exposing the key to everyone else.

Option A — add the grafana user to the ssl-cert group (Debian/Ubuntu):

sudo usermod -aG ssl-cert grafana
sudo chown root:ssl-cert /etc/letsencrypt/live/grafana.example.com/privkey.pem
sudo chmod 640 /etc/letsencrypt/live/grafana.example.com/privkey.pem

Option B — copy the certificate and key to a directory Grafana owns:

sudo mkdir -p /etc/grafana/certs
sudo cp /etc/letsencrypt/live/grafana.example.com/fullchain.pem /etc/grafana/certs/cert.pem
sudo cp /etc/letsencrypt/live/grafana.example.com/privkey.pem  /etc/grafana/certs/key.pem
sudo chown -R grafana:grafana /etc/grafana/certs
sudo chmod 750 /etc/grafana/certs
sudo chmod 640 /etc/grafana/certs/key.pem

Then update grafana.ini to reference the copied paths. Option B is simpler to reason about and works well when combined with a post-renewal hook that copies the files.

Step 4: Restart Grafana and check the service log

sudo systemctl restart grafana-server
sudo systemctl status grafana-server
sudo journalctl -u grafana-server -n 50 --no-pager

Look for a line like:

logger=http.server t=... msg="HTTP Server Listen" address=[::]:3000 protocol=https subUrl= socket=

The word protocol=https confirms TLS is active. If you see an error such as open /etc/grafana/certs/key.pem: permission denied, fix the ownership from Step 3 and restart again.

Step 5: Enforce HTTPS — configure a redirect or firewall rule

If Grafana is still accepting plain HTTP connections you risk users (or monitoring tools) connecting without TLS by accident. There are two approaches:

Option A — nginx reverse proxy with HTTPS redirect (recommended for production)

Terminate TLS at nginx on port 443, redirect port 80 to 443, and proxy to Grafana on localhost:3000:

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

server {
    listen 443 ssl;
    server_name grafana.example.com;

    ssl_certificate     /etc/letsencrypt/live/grafana.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/grafana.example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

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

When using nginx as a reverse proxy, set Grafana back to protocol = http and bind it to 127.0.0.1 only so it is not directly reachable:

[server]
protocol  = http
http_addr = 127.0.0.1
http_port = 3000
root_url  = https://grafana.example.com

Option B — Grafana built-in TLS, block port 80 at the firewall

If you want Grafana to handle TLS itself (no nginx), simply block inbound HTTP:

sudo ufw deny 80/tcp
sudo ufw allow 3000/tcp comment "Grafana HTTPS"

Step 6: Verify the certificate in the browser and with openssl

From a workstation:

openssl s_client -connect grafana.example.com:3000 -servername grafana.example.com </dev/null 2>&1 \
  | openssl x509 -noout -dates -subject -issuer

You should see the certificate’s subject, issuer, and validity dates. If the connection fails you will see a detailed error (wrong hostname, expired cert, handshake failure, etc.) that points you toward the fix.

From a browser, navigate to https://grafana.example.com:3000 and click the padlock. Verify:

  • Certificate issued to the correct hostname
  • Valid dates
  • Correct issuer (Let’s Encrypt or your internal CA)
  • TLS 1.2 or 1.3 in use

Automating certificate renewal

Let’s Encrypt certificates expire every 90 days. Certbot’s systemd timer renews them automatically, but the new files must also reach Grafana. Create a post-renewal deploy hook:

sudo nano /etc/letsencrypt/renewal-hooks/deploy/grafana.sh
#!/bin/bash
# Copy renewed certificates to the Grafana certs directory and reload Grafana
CERT_DIR=/etc/letsencrypt/live/grafana.example.com
DEST=/etc/grafana/certs

cp "${CERT_DIR}/fullchain.pem" "${DEST}/cert.pem"
cp "${CERT_DIR}/privkey.pem"   "${DEST}/key.pem"
chown grafana:grafana "${DEST}/cert.pem" "${DEST}/key.pem"
chmod 640 "${DEST}/key.pem"

systemctl reload-or-restart grafana-server
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/grafana.sh

Test the hook manually:

sudo certbot renew --dry-run

Replacing an already-installed certificate

When your certificate expires or you switch from a self-signed cert to a CA-signed one, the process is:

  1. Copy the new fullchain.pem and privkey.pem to /etc/grafana/certs/.
  2. Verify ownership and permissions.
  3. Run sudo systemctl reload grafana-server — Grafana re-reads the certificate on reload, no full restart needed for most versions. If reload does not pick up the new cert, do a full restart.
  4. Confirm with openssl s_client that the new certificate is served.

Hardening TLS settings

Grafana’s built-in TLS server inherits Go’s defaults, which are already reasonable. If you need explicit control (for compliance or a security scanner), use nginx as the TLS terminator and set:

ssl_protocols       TLSv1.2 TLSv1.3;
ssl_ciphers         ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers on;
ssl_session_timeout 1d;
ssl_session_cache   shared:SSL:10m;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

Common errors and what they mean

ErrorLikely causeFix
open cert_file: no such file or directoryPath in grafana.ini is wrongDouble-check the path with ls -la
open key_file: permission deniedKey not readable by grafana userFix ownership/group membership
tls: failed to find any PEM dataFile is DER encoded or truncatedConvert with openssl x509 -inform DER -out cert.pem
Browser shows “Not Secure” despite HTTPSUsing plain HTTP port but visiting HTTPS URLEnable protocol=https in grafana.ini or use nginx
Certificate mismatch warningcert CN/SAN does not match hostnameIssue a new cert for the correct domain

Summary

Enabling TLS on Grafana is a ten-minute task once you have a certificate. The essential steps are: set protocol = https, point cert_file and cert_key at the correct PEM files, fix file permissions so the grafana user can read the key, and restart the service. Add a post-renewal hook so Let’s Encrypt replacements roll out automatically and you will not be woken up by an expired-certificate alert from your own monitoring system.

Scroll to Top