How to terminate TLS directly

Piko can serve HTTPS on the main application port without a reverse proxy. This guide covers basic TLS, certificate hot reload, mutual TLS, and an independently configured TLS listener for the health probe. For every TLS option see internal/bootstrap/options_tls.go.

Basic TLS

Point Piko at a certificate and key file:

ssr := piko.New(
    piko.WithTLS(
        piko.WithTLSCertFile("/certs/server.pem"),
        piko.WithTLSKeyFile("/certs/server.key"),
    ),
)

With TLS on, Piko sets HSTS headers automatically, switches CORS to secure origins, and negotiates HTTP/2 natively via ALPN (skipping the h2c cleartext wrapper).

Certificate hot reload

Rotate certificates without restarting the server:

ssr := piko.New(
    piko.WithTLS(
        piko.WithTLSCertFile("/certs/server.pem"),
        piko.WithTLSKeyFile("/certs/server.key"),
        piko.WithTLSHotReload(true),
    ),
)

The loader watches the directories containing the certificate and key (not the files directly) so it catches symlink swaps. That matters for Kubernetes and cert-manager, which renew mounted Secrets by replacing symlinks.

The reload flow:

  1. The filesystem watcher detects a change.
  2. A 500 ms debounce waits for both files to settle.
  3. The new certificate loads and validates.
  4. An atomic swap happens: the next TLS handshake uses the new certificate.
  5. If the new certificate fails validation, the old one stays in place and a warning logs.

Kubernetes volume mount

Mount the certificate from a Secret:

apiVersion: v1
kind: Pod
spec:
  containers:
    - name: app
      volumeMounts:
        - name: tls-certs
          mountPath: /certs
          readOnly: true
  volumes:
    - name: tls-certs
      secret:
        secretName: app-tls

With hotReload: true, cert-manager renewals propagate without a rolling restart.

Mutual TLS

Require every client to present a certificate signed by a trusted CA:

ssr := piko.New(
    piko.WithTLS(
        piko.WithTLSCertFile("/certs/server.pem"),
        piko.WithTLSKeyFile("/certs/server.key"),
        piko.WithTLSClientCA("/certs/ca.pem"),
        piko.WithTLSClientAuth("require_and_verify"),
    ),
)

Client authentication modes:

ModeBehaviour
noneNo client certificate requested. Default.
requestRequested but not required.
requireRequired but not verified against the CA.
verifyVerified against the CA if provided.
require_and_verifyRequired and verified against the CA.

Rejected clients fail at the TLS layer. No HTTP handler runs.

Minimum TLS version

Piko defaults to TLS 1.2. To require TLS 1.3:

piko.WithTLS(
    piko.WithTLSCertFile("/certs/server.pem"),
    piko.WithTLSKeyFile("/certs/server.key"),
    piko.WithTLSMinVersion("1.3"),
)

TLS for the health probe

The health probe runs on a separate port. Its TLS configuration is independent:

ssr := piko.New(
    piko.WithHealthTLS(
        piko.WithHealthTLSCertFile("/certs/health.pem"),
        piko.WithHealthTLSKeyFile("/certs/health.key"),
    ),
)

Useful when the health probe is reachable on a different trust boundary than the main application, or when the orchestrator requires HTTPS for probes.

Redirect HTTP to HTTPS

Run an HTTP-to-HTTPS redirector on a second port:

piko.WithTLS(
    piko.WithTLSCertFile("/certs/server.pem"),
    piko.WithTLSKeyFile("/certs/server.key"),
    piko.WithTLSRedirectHTTP(80),
)

Requests to port 80 receive a 301 redirect to the HTTPS equivalent.

TLS options reference

Configure every TLS setting through With* options inside piko.WithTLS(...):

OptionPurpose
piko.WithTLSCertFile(path)Certificate path.
piko.WithTLSKeyFile(path)Private key path.
piko.WithTLSClientCA(path)Client CA bundle for mTLS.
piko.WithTLSClientAuth(mode)Client auth mode ("none", "request", "require", "verify", "require_and_verify").
piko.WithTLSMinVersion(version)Minimum TLS version ("1.2" or "1.3").
piko.WithTLSHotReload(enabled)Watch cert files and reload on change.
piko.WithTLSRedirectHTTP(port)Run an HTTP-to-HTTPS redirector on the given port.

If you need to source any of these from environment variables, read them in your own func main and pass the values to the corresponding option. This is common when paths differ per environment.

ssr := piko.New(
    piko.WithTLS(
        piko.WithTLSCertFile(os.Getenv("APP_TLS_CERT")),
        piko.WithTLSKeyFile(os.Getenv("APP_TLS_KEY")),
    ),
)

Direct TLS or a reverse proxy

Use direct TLS when:

  • Running in Kubernetes with cert-manager.
  • Deploying into a service mesh that expects TLS between services.
  • You need mTLS for service-to-service authentication.
  • You want fewer moving parts in the deployment.

Use a reverse proxy (nginx, Caddy, Cloudflare) when:

  • You need advanced load balancing or path-based routing.
  • Multiple services share a single domain.
  • You want centralised certificate management outside the binary.
  • You need buffering, WAF, or other proxy-layer features.

See also