Skip to content

Iteration 1: TLS termination via Apache-config import (drop-in Apache replacement) #3

Description

@hugithordarson

Goal

Replace Apache as the front-facing TLS terminator on a real server (one of @hugi's), with the smallest possible first cut. Goal is to start dogfooding modulo-as-front-end immediately, then evolve from there as real requirements surface.

Parent: #2

Approach

Rather than design a config format up front, read existing Apache vhost files as the configuration source for iteration one. The operator already has working Apache configs with valid cert paths and hostname lists — repurpose that data. The Apache reader is one of several future config sources (manual modulo config, ACME-managed certs, etc.) — not a permanent coupling.

Rollback is trivial: stop modulo, start Apache, no files moved.

What iteration 1 includes

  • TLS termination on port 443 with SNI (multiple certs, one per Site).
  • Plain HTTP on port 80 that 301-redirects to HTTPS.
  • HTTP/1.1 + HTTP/2. (HTTP/3 deferred — separate dependency, debug surface.)
  • An Apache vhost reader (deliberately dumb regex-based) that extracts per <VirtualHost *:443> block:
    • ServerName → primary hostname
    • ServerAlias → aliases
    • SSLCertificateFile → cert path
    • SSLCertificateKeyFile → key path
  • Cert hot reload (file watcher / mtime poll) so certbot renewals don't require restart.
  • HTTP-01 challenge passthrough: modulo serves /.well-known/acme-challenge/* from certbot's webroot directory, so existing certbot renewal keeps working without code-side ACME yet.
  • Frontend is opt-in via a flag like -Dmodulo.frontend.apache-config-dir=/etc/apache2/sites-enabled. Without it, modulo behaves exactly as today (plain HTTP reverse proxy behind another web server). No behavior change for existing deployments.

What iteration 1 does NOT include

  • ACME client (acme4j). Certbot keeps renewing the certs on disk; modulo just reads them.
  • Access logging. Apache's TransferLog is ignored. Modulo logs continue as today.
  • Compression (Brotli/gzip). Ignored.
  • Apache RewriteRule semantics. The canonical-hostname redirect (e.g. lidamot.iswww.lidamot.is) is modeled as an explicit per-Site policy field, not derived from Apache rewrites.
  • DocumentRoot / static file serving. For this iteration the server's apps will handle their own static content.
  • HTTP/3.
  • Native modulo config format. The Apache reader is the only config source for now.
  • Per-site HTTP→HTTPS toggle. Behavior is always-on for known hostnames in iteration 1 — but the Site model carries it as a field with true default, so future Sites can override.

Conceptual model (iteration 1)

Minimal Site object — designed to grow, but tiny for now:

Site
├── primaryHostname     (from ServerName)
├── aliases             (from ServerAlias)
├── certPath            (SSLCertificateFile)
├── keyPath             (SSLCertificateKeyFile)
├── canonicalRedirect   (default true; redirects aliases to primaryHostname)
└── httpsRedirect       (default true; per-site override available)

Routing to the actual app upstream comes from modulo's existing logic (hostname → app via the current domainToAppMap, which is itself slated for replacement in #2). The Apache config's ProxyPass line is ignored — in the new topology modulo talks to the app directly rather than to another modulo.

Code shape

A new package modulo.frontend containing iteration-1 code, with no dependencies on ModuloProxy or AdaptorConfig. This keeps it cleanly extractable later (per undur/wo-adaptor-jetty#2) without designing the library API in a vacuum.

  • modulo.frontend.site.Site — the model.
  • modulo.frontend.apache.ApacheConfigReader — produces List<Site> from a directory of Apache vhost files.
  • modulo.frontend.tls.CertStore — loads PEM cert+key pairs into an in-memory KeyStore, supports reload on file change.
  • modulo.frontend.JettyFrontend — builds the Jetty Server with 80+443 connectors, SNI via the CertStore, canonical-hostname redirect handler, HTTP→HTTPS redirect handler, ACME challenge passthrough handler, then hands off to the existing ModuloProxy.
  • Modulo (existing) — when the frontend flag is set, builds via JettyFrontend instead of the current bare connector path.

Testing plan on the live server

  1. Run modulo on alternate ports first (8080/8443) with the live Apache config loaded; verify with curl --resolve <host>:8443:<ip> before touching real ports or DNS.
  2. Copy Apache configs to a parallel directory that modulo reads from, so Apache isn't fighting modulo for files during the swap.
  3. Confirm certbot HTTP-01 renewal works with modulo serving /.well-known/acme-challenge/* (test by forcing a renewal with certbot renew --dry-run).
  4. Rehearse the rollback: systemctl stop modulo && systemctl start apache2, with both unit files ready, before the real cutover.

Out-of-scope adjacent issues to keep in mind

These will surface as iteration 1 lands and we'll spin them off then:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions