VanitySSL is a TLS proxy designed for SaaS platforms that want to offer "vanity" or custom domains with HTTPS support for their customers. It handles certificate management using the ACME protocol and proxies requests to a backend service while injecting headers that identify the customer.
A SaaS provider often wants customers to access the service via a custom domain, for example status.customer.com. VanitySSL acts as an entry point that:
- Terminates TLS for each custom domain using SNI to determine which certificate to present.
- Obtains and renews TLS certificates from an ACME provider such as Let's Encrypt.
- Proxies requests to the SaaS backend, adding headers so the backend knows which customer the request belongs to. Requests are signed with
X-Vanity-Signatureusing thePROXY_SECRETenvironment variable so the backend can verify they came through VanitySSL. - Provides an internal API for managing customers and domain mappings.
Customers create a CNAME from their chosen domain to the SaaS endpoint (e.g., app.saas.com). VanitySSL sees the incoming SNI and serves the correct certificate for that domain, then forwards the request to the backend service (e.g., backend.saas.com).
Client --> VanitySSL --> Backend Service
|
|-- ACME (Let's Encrypt) for certificates
`-- Internal API for managing customers
- TLS Termination: Uses Go's TLS stack with SNI support. Certificates are retrieved from storage based on domain and automatically renewed.
- ACME Client: The project will use
golang.org/x/crypto/acme/autocertfor automatic certificate management via ACME. It is part of the Go standard library ecosystem, well-maintained, and widely used for Let's Encrypt integrations. - Reverse Proxy: Requests are forwarded to the backend using
httputil.ReverseProxyfrom the standard library. It supports modifying requests and responses, making it a good fit for injecting custom headers. - Database Interface: Certificate data and customer domain mappings are stored through an abstract interface so that different key-value stores can be used. The MVP will rely on BadgerDB for local storage. Future implementations may use Consul, Etcd, or a Raft-based store without changing business logic.
- Internal API: Exposes CRUD endpoints for customers:
{customerId, domain}. This allows the SaaS platform to add or remove customer domains at runtime. Configuration (backend address, database directory, etc.) is provided via environment variables.
- ACME Autocert: Choosing
autocertreduces the amount of code needed for certificate management. It handles the ACME challenge/response workflow and certificate renewal out of the box. - Standard Library Reverse Proxy:
httputil.ReverseProxyis simple, reliable, and already integrated with Go's HTTP stack. It allows request/response modification, which is enough for adding identifying headers. - Database Abstraction: Defining a
Storeinterface decouples VanitySSL from a specific database. Implementations for BadgerDB and other KV stores can be swapped in as needed. - Minimal Dependencies: Relying primarily on the Go standard library keeps the code lightweight. External dependencies are chosen carefully for functionality not provided by the standard library (Badger, autocert).
-
Project Structure
- Initialize a Go module.
- Define the
Storeinterface for certificate and domain storage. - Implement a BadgerDB-backed store as the default option.
-
Certificate Management
- Integrate
autocert.Managerto automatically request and renew certificates based on incoming SNI values. - Store certificates in the configured
Storeimplementation.
- Integrate
-
Reverse Proxy
- Create a proxy handler that looks up the customer via SNI, adds identifying headers (e.g.,
X-Customer-IDandX-Customer-Domain), and forwards the request to the configured backend.
-
Internal API
- Implement HTTP endpoints for creating, reading, updating, and deleting customer records.
- The API should authenticate requests (e.g., via token or IP whitelist) since it modifies domain mappings.
-
Configuration and Launch
- Read environment variables for backend address, database location, ACME email, and other settings.
- Start the proxy server with HTTPS enabled. Requests with the hostname specified in
VANITY_API_HOSTNAMEare served by the internal API; all others are proxied to the backend.
- Testing and Logging
- Add unit tests for the store interface and API logic.
- Provide logging around certificate renewal and proxy events.
- Implement additional
Storebackends (Consul, Etcd, etc.). - Support clustering by sharing certificates and domain mappings across nodes via the store.
- Provide metrics (e.g., Prometheus) for monitoring certificate renewals and proxy traffic.
- Expand authentication and security features for the internal API.
VanitySSL's goal is to simplify managing custom domains with TLS so SaaS providers can offer branded endpoints to their customers with minimal operational overhead.
Build and run the container:
docker build -t vanityssl .
docker run -p 80:80 -p 443:443 \
-e BACKEND_URL=https://backend.internal \
-e ACME_EMAIL=admin@example.com \
-e PROXY_SECRET=changeme \
vanitysslEnvironment variables configure the backend address, ACME email, optional API token (API_TOKEN), database path (DB_PATH), the proxy signing secret (PROXY_SECRET), the API hostname (VANITY_API_HOSTNAME), and the LRU cache size (CACHE_SIZE, default 1000). Requests for the configured hostname are served by the internal API on port 443. Port 80 must be reachable for the ACME HTTP-01 challenge. Certificates are stored in the configured database.
A simple test backend is available in cmd/dummybackend. It prints request information and verifies the signature when PROXY_SECRET is set.