fleet/docs/Contributing/guides/mtls-reverse-proxy-setup.md
Victor Lyuboslavsky e274738b9d
Instructions to create a public mTLS reverse proxy (#33906)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #33165

Doc updates only.
2025-10-08 14:46:33 -05:00

209 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Create a public mTLS reverse proxy for testing mTLS
```mermaid
flowchart LR
subgraph Internet
Client["Client (with client cert)"]
end
subgraph Cloud_proxy
Proxy["Caddy / mTLS Proxy on VM"]
end
subgraph Tunnel_path
ngrok["ngrok public URL"]
LocalApp["Your Fleet server"]
end
Client -->|TLS + Client Cert| Proxy
Proxy -->|HTTPS + forwarded headers| ngrok
ngrok -->|tunnel traffic| LocalApp
```
## Assumptions & prerequisites
* You own a domain, e.g. `example.site`, or some domain.
* You will use a subdomain, e.g. `mtls.example.site` or whatever you choose.
* You have a **client CA certificate** (root or intermediate) that signs all valid client certificates (e.g. `client-ca.crt`).
* You have a backend (ngrok URL) that you want to forward to (i.e. `https://my-fleet-server.ngrok.io`).
* You have access to the DigitalOcean or another cloud provider.
* Replace `example.site` and `my-fleet-server.ngrok.io` in this guide with your own values.
---
## Step 1: Create a Droplet (VM) on DigitalOcean (or other cloud provider)
1. Log in to your DigitalOcean account.
2. In the dashboard, click **Create → Droplet**.
3. Choose an OS image (e.g. Ubuntu 24.04 LTS is a good default).
4. Choose a plan (for testing, the cheapest is fine, e.g. “Basic” with 1 vCPU / 1 GB RAM).
5. Choose a datacenter region (ideally near your users or your dev location).
6. Add SSH keys (recommended) so you can SSH in securely. If you dont have an SSH key, you can generate one (`ssh-keygen`) and paste the public key.
7. Finalize settings (hostname, backups, etc.), then click “Create Droplet”.
After some moments, youll have a Droplet with a public IP address (say `203.0.113.45` as an example).
---
## Step 2: Set up DNS so `mtls.yourdomain` points to the Droplet
You need to make your subdomain resolve to that Droplets IP.
1. In DigitalOcean dashboard → **Networking****Domains** (or **DNS**)
2. In your DNS records page, add a record:
* Type: **A**
* Hostname: `mtls` (so the full hostname is `mtls.example.site`) (or whatever you choose)
* Value / Points to: your Droplets IP (e.g. `203.0.113.45`)
* TTL: the default (or lower if you like)
3. Wait for DNS propagation (may take some minutes). You can test with `dig mtls.example.site` or `ping mtls.example.site` to see it resolve to your Droplet IP.
Once that is active, when clients connect to `mtls.example.site`, they reach your Droplet.
---
## Step 3: SSH into the droplet, install Caddy
1. SSH in:
```bash
ssh root@203.0.113.45
```
2. Update packages:
```bash
apt update
apt upgrade -y
```
3. Install Caddy (the recommended way is via their official install script or package). On Ubuntu, a reliable method:
```bash
# Install required packages
apt install -y debian-keyring debian-archive-keyring apt-transport-https
# Add Caddy repo
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | apt-key add -
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update
apt install -y caddy
```
This gives you a system service `caddy` which can manage and auto-reload configuration.
---
## Step 4: Place your mTLS CA cert on the server
You need to put the CA certificate (the public certificate of the CA that signs client certs) where Caddy can read it.
1. On your local machine, copy your CA cert file (say `client-ca.crt`) over to the Droplet:
```bash
scp client-ca.crt root@203.0.113.45:/etc/caddy/client-ca.crt
```
2. On the Droplet, ensure it's readable by Caddy (e.g. permissions):
```bash
chmod 644 /etc/caddy/client-ca.crt
chown root:root /etc/caddy/client-ca.crt
```
---
## Step 5: Configure Caddy for TLS + mTLS + reverse proxy + header forwarding
You need a `Caddyfile` (or JSON config) that:
* Listens for `mtls.example.site`
* Uses TLS (Caddy will obtain a certificate automatically via Lets Encrypt)
* Requires client certificate authentication, trusting your CA
* Proxies incoming requests to your backend URL (ngrok)
* Injects headers to your backend with the clients leaf cert and serial
Heres an example `Caddyfile` you can use (put at `/etc/caddy/Caddyfile`):
```caddyfile
mtls.example.site {
# Enable TLS (Caddy does automatic HTTPS)
tls {
# mTLS (client auth) config
client_auth {
mode require_and_verify
trusted_ca_cert_file /etc/caddy/client-ca.crt
}
}
# Reverse proxy to your ngrok backend
reverse_proxy https://my-fleet-server.ngrok.io {
# Make ngrok happy:
header_up Host my-fleet-server.ngrok.io
# Forward headers for client certificate information
# Use DER base64 encoding (PEM has newlines which break HTTP headers). Note: AWS ALB sends URL-encoded PEM format in X-Amzn-Mtls-Clientcert-Leaf.
header_up X-Client-Cert {http.request.tls.client.certificate_der_base64}
header_up X-Client-Cert-Serial {http.request.tls.client.serial}
header_up X-Client-Cert-Subject {http.request.tls.client.subject}
header_up X-Client-Cert-Issuer {http.request.tls.client.issuer}
}
}
```
Save that `Caddyfile`.
---
## Step 6: Reload / restart Caddy
After updating `/etc/caddy/Caddyfile`, reload Caddy to pick up the new config:
```bash
systemctl reload caddy
```
Watch logs to check for TLS / client auth errors:
```bash
journalctl -u caddy -f
```
If things go well, Caddy should:
* Obtain a TLS certificate for `mtls.example.site` from Lets Encrypt automatically
* Accept HTTPS connections at that hostname
* Require that connecting clients present a valid certificate signed by your CA
* If the clients certificate is valid, proxy requests to `https://my-fleet-server.ngrok.io`
* Forward `X-Client-Cert` header with the PEM of the clients certificate and `X-Client-Cert-Serial` header with the serial number to the backend
You can test with `curl` from a client that has a valid certificate:
```bash
curl --cert client-cert.pem --key client-key.pem https://mtls.example.site/healthz
```
You should see a response (the backend's response via ngrok), and your backend should receive headers:
```
X-Client-Cert: MIIDXTCCAkWgAwIBAgIJAK... (base64-encoded DER certificate)
X-Client-Cert-Serial: 542242443644849078027064623851697342324729218861
X-Client-Cert-Subject: CN=victor@dev
X-Client-Cert-Issuer: CN=My CA
```
If you use a certificate not signed by your CA (or no certificate), the handshake should fail (client is rejected).
---
## Step 7: Verify & troubleshoot
* Test DNS resolves
* Use `curl` with correct / incorrect cert
* Look at Caddy logs for TLS handshake errors
* If the backend (ngrok) rejects certain headers or sees unexpected hostnames, you may need to adjust `header_up Host` or other headers
---