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.is → www.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
- 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.
- Copy Apache configs to a parallel directory that modulo reads from, so Apache isn't fighting modulo for files during the swap.
- Confirm certbot HTTP-01 renewal works with modulo serving
/.well-known/acme-challenge/* (test by forcing a renewal with certbot renew --dry-run).
- 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:
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
<VirtualHost *:443>block:ServerName→ primary hostnameServerAlias→ aliasesSSLCertificateFile→ cert pathSSLCertificateKeyFile→ key path/.well-known/acme-challenge/*from certbot's webroot directory, so existing certbot renewal keeps working without code-side ACME yet.-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
TransferLogis ignored. Modulo logs continue as today.RewriteRulesemantics. The canonical-hostname redirect (e.g.lidamot.is→www.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.truedefault, so future Sites can override.Conceptual model (iteration 1)
Minimal Site object — designed to grow, but tiny for now:
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'sProxyPassline is ignored — in the new topology modulo talks to the app directly rather than to another modulo.Code shape
A new package
modulo.frontendcontaining iteration-1 code, with no dependencies onModuloProxyorAdaptorConfig. 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— producesList<Site>from a directory of Apache vhost files.modulo.frontend.tls.CertStore— loads PEM cert+key pairs into an in-memoryKeyStore, supports reload on file change.modulo.frontend.JettyFrontend— builds the JettyServerwith 80+443 connectors, SNI via theCertStore, canonical-hostname redirect handler, HTTP→HTTPS redirect handler, ACME challenge passthrough handler, then hands off to the existingModuloProxy.Modulo(existing) — when the frontend flag is set, builds viaJettyFrontendinstead of the current bare connector path.Testing plan on the live server
curl --resolve <host>:8443:<ip>before touching real ports or DNS./.well-known/acme-challenge/*(test by forcing a renewal withcertbot renew --dry-run).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:
DocumentRoot/ static file serving.domainToAppMap— part of Modulo as front-facing server: Site abstraction, native TLS, native logging #2.