TLS / HTTPS
Why HTTPS Is Required
The Dart SDK (dart pub) will not send authentication tokens over unencrypted HTTP connections to non-localhost URLs. This is a security feature built into the Dart tooling, not a club limitation.
If you try to use club over plain HTTP with a non-localhost URL:
dart pub token add http://packages.example.comwill refuse to store the tokendart pub getwill not send theAuthorizationheaderdart pub publishwill fail with a 401 error
The recommended approach is to run club behind a reverse proxy that handles TLS termination. The club server itself listens on plain HTTP (port LISTEN_PORT, default 8080) and the reverse proxy sits in front, providing HTTPS to clients.
Option A: Caddy (Recommended)
Caddy is the easiest option. It automatically provisions and renews Let’s Encrypt TLS certificates with zero configuration.
Prerequisites
- A domain name (e.g.,
packages.example.com) pointing to your server’s IP - Ports 80 and 443 open in your firewall
Installation
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curlcurl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpgcurl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.listsudo apt updatesudo apt install caddySee the reverse proxy guide for a complete Docker Compose setup with Caddy as a sidecar container.
Configuration
Create or edit /etc/caddy/Caddyfile:
packages.example.com { reverse_proxy localhost:8080 { header_up X-Forwarded-Proto {scheme} header_up X-Real-IP {remote_host} } request_body { max_size 100MB }}That is the entire configuration. Caddy will:
- Listen on ports 80 and 443
- Obtain a Let’s Encrypt certificate for
packages.example.com - Redirect HTTP to HTTPS
- Proxy all traffic to club on port 8080
- Automatically renew the certificate before it expires
Start Caddy
sudo systemctl enable caddysudo systemctl restart caddyVerify
curl -I https://packages.example.com/api/v1/healthYou should see a valid HTTPS response with a Let’s Encrypt certificate.
Option B: nginx + Let’s Encrypt
Use this approach if you already have nginx deployed or prefer more control over TLS configuration.
-
Install nginx and Certbot
Terminal window sudo apt install nginx certbot python3-certbot-nginx -
Create the nginx configuration
Create
/etc/nginx/sites-available/club:server {listen 80;server_name packages.example.com;location / {return 301 https://$host$request_uri;}}server {listen 443 ssl http2;server_name packages.example.com;ssl_certificate /etc/letsencrypt/live/packages.example.com/fullchain.pem;ssl_certificate_key /etc/letsencrypt/live/packages.example.com/privkey.pem;ssl_protocols TLSv1.2 TLSv1.3;ssl_ciphers HIGH:!aNULL:!MD5;client_max_body_size 100m;location / {proxy_pass http://127.0.0.1:8080;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_set_header Connection "";proxy_read_timeout 120s;}} -
Enable the site
Terminal window sudo ln -s /etc/nginx/sites-available/club /etc/nginx/sites-enabled/sudo rm -f /etc/nginx/sites-enabled/default -
Obtain a certificate
Terminal window sudo certbot --nginx -d packages.example.comCertbot will update the nginx configuration with the certificate paths and set up automatic renewal.
-
Test and reload
Terminal window sudo nginx -tsudo systemctl reload nginx -
Verify auto-renewal
Terminal window sudo certbot renew --dry-run
Certificate renewal
Certbot installs a systemd timer that checks for renewal twice daily. Verify it is active:
sudo systemctl status certbot.timerOption C: Self-Signed Certificates (Testing Only)
For local testing or internal networks where Let’s Encrypt is not an option, you can use self-signed certificates.
Generate a self-signed certificate
sudo mkdir -p /etc/ssl/club
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/ssl/club/key.pem \ -out /etc/ssl/club/cert.pem \ -subj "/CN=packages.example.com"Use with Caddy
packages.example.com { tls /etc/ssl/club/cert.pem /etc/ssl/club/key.pem reverse_proxy localhost:8080 { header_up X-Forwarded-Proto {scheme} header_up X-Real-IP {remote_host} } request_body { max_size 100MB }}Use with nginx
Replace the ssl_certificate and ssl_certificate_key paths in the nginx configuration with your self-signed certificate paths:
ssl_certificate /etc/ssl/club/cert.pem;ssl_certificate_key /etc/ssl/club/key.pem;Trusting the certificate on client machines
For dart pub to accept a self-signed certificate, you need to add it to the system’s trust store on each client machine:
sudo security add-trusted-cert -d -r trustRoot \ -k /Library/Keychains/System.keychain cert.pemsudo cp cert.pem /usr/local/share/ca-certificates/club.crtsudo update-ca-certificatesImport-Certificate -FilePath cert.pem -CertStoreLocation Cert:\LocalMachine\RootAfter adding the certificate, restart any running Dart processes.
DNS Configuration
Before any of these TLS options will work, your domain must resolve to your server’s IP address:
packages.example.com A <your-server-ip>packages.example.com AAAA <your-server-ipv6> (optional)Verify DNS resolution:
nslookup packages.example.comDNS propagation typically takes 5 to 60 minutes after you create or update the record.
Verifying TLS
After setting up HTTPS, verify the full chain:
# Check the certificateopenssl s_client -connect packages.example.com:443 -brief
# Check the health endpoint over HTTPScurl -v https://packages.example.com/api/v1/health
# Verify dart pub can communicatedart pub token add https://packages.example.comAll three should succeed without TLS errors.