le-cert-server
GitHub

Usage over Tailscale

le-cert-server can run directly on your Tailscale network using an embedded tsnet node, instead of listening on a regular TCP socket. This is a convenient way to deploy it on a private network without exposing it publicly.

Benefits:

  • The API is only reachable from machines on your tailnet—no public exposure required
  • HTTPS is served automatically using Tailscale-provided certificates, so there’s nothing to configure or renew for the API listener itself
  • Clients authenticate using their Tailscale node identity—no pre-shared keys or tokens to distribute
  • Fine-grained, per-domain authorization is controlled centrally from your Tailscale ACL policy

Configuration#

Set server.listener to tsnet and configure the server.tsnet section. When the tsnet listener is used:

  • The server listens on a Tailscale netstack and always serves HTTPS using Tailscale-provided certificates
  • server.bind and server.tls are ignored
  • The default port is 443
# config.yaml
server:
  listener: "tsnet"
  tsnet:
    # Hostname for this node on your tailnet
    hostname: "le-cert-server"
    # Auth key used only on first startup to join the tailnet
    # If empty, tsnet relies on existing state in the state directory
    authKey: "tskey-auth-xxxxx"
    # Directory where tsnet stores its state
    # Defaults to a folder next to the loaded config file
    #stateDir: ""
    # If true, the node is ephemeral (not persisted in the tailnet)
    #ephemeral: false

# Use the built-in "tsnet" auth method, where client are authenticated using the node identity in the tailnet
# Permissions are configured using the standard Tailscale ACL policy
auth:
  method: "tsnet"

# Configure Let's Encrypt and the database as per usual
letsEncrypt:
  email: "admin@example.com"
  dnsProvider: "cloudflare"
  dnsCredentials:
    CF_DNS_API_TOKEN: "your-cloudflare-token"
  renewalDays: 30

database:
  path: "le-cert-server.db"

The authKey is only used on first startup to join the tailnet (or when the node key expires). Generate one in the Tailscale admin console . Once the node has joined, its state is persisted and the key is no longer needed.

Authentication with Tailscale identity#

When running over tsnet, you can authenticate callers by their Tailscale node identity by setting auth.method to tsnet. This method is only available when server.listener is also set to tsnet.

With Tailscale identity authentication:

  • Callers do not send an Authorization header. le-cert-server determines the caller’s identity from the tailnet connection itself, so the identity cannot be forged.
  • Authorization to request certificates for specific domains is granted in your Tailscale ACL policy, using the app capability italypaleale.me/le-cert-server.
  • A node that has not been granted any domains is rejected.

In the Tailscale ACL editor, grant nodes the italypaleale.me/le-cert-server capability with the list of allowed domains:

{
  "grants": [
    {
      "src": ["*"],
      "dst": ["le-cert-server"],
      "app": {
        "italypaleale.me/le-cert-server": [
          { "domains": ["example.com"] },
          { "domains": ["*.example2.com"] }
        ]
      }
    }
  ]
}
  • src controls which machines may request certificates (use * for any node, or restrict to specific tags such as tag:traefik)
  • dst is the le-cert-server node
  • Each entry under the capability lists the domains those nodes are allowed to request, including wildcards such as *.example2.com

Requesting a certificate#

From any authorized node on your tailnet, call the API over HTTPS using the node’s hostname. No Authorization header is needed—the request is authenticated by your Tailscale identity:

curl -X POST https://le-cert-server.your-tailnet.ts.net/api/certificate \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'

The domain must be covered by the grants assigned to the calling node, otherwise the request is rejected.

Edit this page on GitHub