How OpenResty Edge Routing Handles TLS Termination and Let's Encrypt in Openship

Openship delegates all TLS termination and automatic Let's Encrypt certificate management to its OpenResty edge container, which proxies HTTP-01 challenges to a standalone Certbot instance while rejecting unconfigured SNI headers by default.

Openship uses OpenResty as the core reverse-proxy within its edge container to handle ingress traffic. Unlike traditional setups that rely on external load balancers for TLS termination, Openship manages certificates entirely inside the edge container using a combination of OpenResty configuration and Certbot automation.

Default TLS Configuration and SNI Protection

The edge container initializes with a secure default configuration generated by ensureOpenRestyConfig in packages/adapters/src/infra/openresty-lua.ts. This function creates a catch-all server block on port 443 that explicitly rejects unknown Server Name Indication (SNI) headers.

The critical configuration directive is ssl_reject_handshake on, which prevents the "first-loaded vhost" fallback behavior that would otherwise expose a certificate for a different domain.

server {
    listen 443 ssl default_server;
    ssl_reject_handshake on;
}

This default block is defined at lines 44-53 of openresty-lua.ts, ensuring that any TLS connection attempt without a matching configured domain receives an immediate SSL handshake rejection rather than a mismatched certificate.

Certificate Provisioning Workflow

When you register a new site through the Openship dashboard or API, the system triggers a structured provisioning process that bridges OpenResty configuration with Certbot execution.

Route Registration and TLS Flagging

The registerRoute method in packages/adapters/src/system/proxy/api.ts handles new domain registration. When called with tls: true, it writes a site-specific vhost file to sites-enabled/ and invokes the certificate provisioning logic.

await nginx.registerRoute({
  domain: "example.com",
  tls: true,
  targetUrl: "http://127.0.0.1:3000"
});

This corresponds to lines 127-138 in api.ts.

HTTP-01 Challenge Proxying

Certbot runs in standalone HTTP-01 mode listening on the loopback port 49180 (ACME_HTTP01_PORT). OpenResty forwards all /.well-known/acme-challenge/ requests to this internal port, allowing Certbot to respond to validation requests while the edge maintains control of public ports 80 and 443.

The proxy location is defined in packages/adapters/src/infra/openresty-lua.ts at lines 21-25:

location /.well-known/acme-challenge/ {
    proxy_pass http://127.0.0.1:49180;
    proxy_set_header Host $host;
}

Executing Certbot Inside the Container

The provisionCert method executes Certbot commands via the container executor. Located at lines 141-152 of api.ts, this function runs:

certbot certonly --standalone -d <domain> --preferred-challenges http

The command executes inside the edge container through docker exec or SSH-based abstraction handled by ensure-container-edge.ts (lines 425-435). This ensures Certbot has direct access to the standalone authenticator on port 49180.

Certificate Persistence and Storage

Certificates survive container recreation through bind mounts. The EDGE_CONTAINER_MOUNTS constant defined in openresty-lua.ts (lines 104-108) maps the host's /etc/letsencrypt directory into the container.

When Certbot writes certificates to /etc/letsencrypt/live/<domain>/, they are immediately persisted on the host filesystem. The generated vhost configurations reference these host-backed paths directly, enabling instant TLS termination once certificates exist.

Safe Configuration Reloads

After certificate issuance or renewal, Openship reloads OpenResty using the buildReloadCommand function from openresty-lua.ts (lines 88-108). This generates a two-stage command:

/usr/local/openresty/bin/openresty -t && /usr/local/openresty/bin/openresty -s reload

The configuration test (-t) ensures that invalid certificates or broken nginx syntax never bring down the edge. When running in container mode, the reload command respects the containerEdge flag to avoid terminating the container's PID 1 process.

Automated Renewal Process

Openship periodically invokes certbot renew through the same container execution layer. Because certificates live in the host-mounted /etc/letsencrypt directory, renewed files are immediately visible to the running OpenResty process without requiring container restart. If a renewal updates a certificate, the same buildReloadCommand validation-reload sequence applies.

Implementation Examples

Registering a TLS-Enabled Domain

import { nginx } from "openship/adapters";

// Provision will automatically trigger for new domains
await nginx.registerRoute({
  domain: "api.example.com",
  tls: true,
  targetUrl: "http://internal-app:3000"
});

Manual Certificate Debugging

For debugging or manual certificate generation:


# Execute Certbot inside the edge container

docker exec openship-edge sh -c \
  "certbot certonly --standalone -d manual.example.com \
   --preferred-challenges http --non-interactive --agree-tos \
   -m admin@example.com"

# Validate and reload configuration

docker exec openship-edge sh -c \
  "/usr/local/openresty/bin/openresty -t && \
   /usr/local/openresty/bin/openresty -s reload"

Generating Reload Commands

import { buildReloadCommand } from "packages/adapters/src/infra/openresty-lua";

const reloadScript = buildReloadCommand(
  detectedPaths, 
  { containerEdge: true }
);
// Returns: validation command && reload command for containerized edge

Summary

  • TLS termination occurs entirely within the Openship edge container using OpenResty, eliminating external load balancer dependencies.
  • SNI protection via ssl_reject_handshake on prevents certificate leakage for unmatched domains.
  • Certbot integration uses standalone HTTP-01 challenges proxied through OpenResty on port 49180.
  • Certificate persistence relies on host bind mounts at /etc/letsencrypt to survive container recreation.
  • Safe reloads validate configuration before applying changes using buildReloadCommand.
  • Key files include packages/adapters/src/infra/openresty-lua.ts and packages/adapters/src/system/proxy/api.ts.

Frequently Asked Questions

How does Openship prevent exposing wrong certificates for unmatched domains?

Openship configures a default server block in nginx.conf that uses the ssl_reject_handshake on directive. This forces OpenResty to terminate the TLS handshake immediately for any SNI not explicitly configured, rather than falling back to the first available certificate.

Where are Let's Encrypt certificates stored?

Certificates are stored in the host's /etc/letsencrypt directory, which is mounted into the edge container via EDGE_CONTAINER_MOUNTS. This bind-mount ensures certificates persist across container updates and restarts while remaining accessible to both Certbot and OpenResty.

What happens if certificate renewal fails?

The renewal process runs certbot renew non-interactively. If renewal fails for a specific domain, Certbot skips that certificate and continues processing others. OpenResty only reloads if the renewal command exits successfully, ensuring that expired certificates remain in place rather than breaking the configuration with missing files.

Can I use external load balancers instead of OpenResty?

While Openship is designed to handle TLS termination internally, you can place the edge container behind an external load balancer. However, you must then configure the external system to handle TLS termination and certificate management, as Openship's automatic Let's Encrypt provisioning assumes OpenResty controls the public ports and proxy challenge handling.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →