Skip to content

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

@hugithordarson

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions