Skip to content

Reshape ingress config around named modes (RFD-84)#799

Merged
phinze merged 7 commits into
mainfrom
phinze/mir-1115-run-miren-behind-a-reverse-proxy
May 12, 2026
Merged

Reshape ingress config around named modes (RFD-84)#799
phinze merged 7 commits into
mainfrom
phinze/mir-1115-run-miren-behind-a-reverse-proxy

Conversation

@phinze
Copy link
Copy Markdown
Contributor

@phinze phinze commented May 11, 2026

This work started because @jcasimir wanted to run Miren behind nginx in production, and Miren didn't quite know how to be run that way yet. He opened #789, #790, and #791 to make it work, extending the existing TLS configuration along the lines its surface seemed to invite. Reading the three PRs as a set, though, surfaced a deeper tension: the ingress and TLS sections together were trying to answer three separate questions (where to listen, whether to terminate TLS, where certs come from) through knobs that each spanned multiple of those answers, and the cross-axis coupling was forcing compensating validation helpers to keep things coherent. We landed on a mode-based redesign that names each deployment shape directly, which RFD-84 walks through in full.

This PR is the implementation. Three modes now: tls-autoprovision (default, behavior unchanged from today), behind-proxy-http (plain HTTP, defaults to 127.0.0.1:80), and behind-proxy-https (TLS terminated by Miren on a single listener, defaults to 127.0.0.1:443). An optional ingress.address field replaces any mode's default bind, so operators on multi-homed hosts, Tailscale interfaces, IPv6, or non-standard ports can bind wherever they need. Unix sockets are reserved as a future address value and rejected today with a clear "not yet supported" error.

One real behavior change comes with this. The autoprovision :80 handler used to have a Host-based exception that routed localhost, 127.0.0.1, ::1, and any raw-IP Host directly to the default route app over plain HTTP, bypassing the HTTPS redirect. That was a dev-convenience shortcut that became a small production smell, since operators didn't always realize that raw-IP access could reach apps without TLS. It's gone now. The autoprovision path does only what its name suggests, and operators who want plain-HTTP access for dev workflow pick behind-proxy-http explicitly.

One small ergonomic regression worth flagging in dev: curl http://localhost/ (no scheme, defaults to HTTP) used to hit the default route directly via that side door. Now it 302s to https://localhost/. The other dev shortcuts still work, though: curl -k https://localhost/ and curl -kL http://localhost/ both reach the default route as before. If the friction proves annoying, RFD-84 leaves room for a dev-only opt-in flag in a follow-up.

tls.standard_tls retires entirely. The dev environment already used --self-signed-tls rather than that field, so the dev loop is unchanged; I caught and updated a few stranded references in the systemd install path, the embedded systemd entrypoint test script, and the example server.toml.

Big thanks to @jcasimir for the use case and the implementation work; both set the stage for this change. His autotls helpers from #790 are the backbone of the new behind-proxy-https mode. The three PRs close out once this lands.

Closes MIR-1115.

phinze added 4 commits May 11, 2026 14:12
Introduces the [ingress] section with mode and address fields, and
retires tls.standard_tls. The mode is a three-value enum
(tls-autoprovision, behind-proxy-http, behind-proxy-https) that names
the supported deployment shapes directly so invalid combinations
become unrepresentable rather than rejected at load time.

The cross-field validation that the previous shape required (warning
when [tls] knobs become no-ops, the HTTP-01-cant-pair-with-custom-https
rule, the mutual-exclusion check between two address fields) collapses
to a small ValidateIngressCoherence method: enforce that [tls] is
empty under behind-proxy-http, reject the reserved-but-unsupported
unix: address prefix with a clear message, and require that
ingress.address remain empty under tls-autoprovision since the :443 +
:80 pair is structural for HTTP-01 ACME.

Constants in ingress_modes.go give callers and downstream code a
typed name to switch on rather than bare strings.

See mirendev/rfd#134 for the design and
rationale.
…FD-84)

Replaces the GetStandardTLS branching in server.go with a switch on
cfg.Ingress.GetMode(). Each non-autoprovision mode resolves its bind
address from cfg.Ingress.GetAddress() (with a localhost default), and
dispatches to the right serve function:

- tls-autoprovision: existing ServeTLSWithController or
  ServeTLSSelfSigned (binds :443 + :80 as before)
- behind-proxy-https: new ServeTLSWithControllerOnAddr or
  ServeTLSSelfSignedOnAddr (single TLS listener at the configured
  address; adopted from Jeff Casimir's PR #790 essentially unchanged)
- behind-proxy-http: plain http.ListenAndServe at the configured
  address

Also drops the Host-based exception in ServeTLSWithController's :80
handler that used to route localhost / 127.0.0.1 / ::1 / raw-IP Host
requests directly to the default route app over plain HTTP. That was
a dev-convenience hack with production security smell. Operators who
want plain-HTTP access for dev workflow now pick behind-proxy-http
explicitly; the autoprovision :80 listener now does only what it
says (redirect + ACME).

ValidateIngressCoherence runs right after Load() in both Server()
and ServerConfigValidate() so config errors surface before any
listener starts.

Thanks to Jeff Casimir (https://github.com/jcasimir) for surfacing
the use case in PRs #789/#790/#791. The autotls helpers in this
commit are the OnAddr variants from his PR #790 essentially
unchanged.
- server-config.md: new [ingress] section documenting mode + address,
  with the three-mode reference table. [tls] section reframed as
  cert-only and no longer lists the removed standard_tls field.
- tls.md: new "Ingress Modes and TLS" section that introduces the
  three modes and how cert sourcing varies across them. Old TLS
  reference table no longer mentions standard_tls.
- command/server.md: regenerated to pick up --ingress-mode and
  --ingress-address flags and drop --serve-tls.
…eshape

The systemd install path, the embedded systemd entrypoint test script,
and the example server.toml all still referenced the removed flag and
field. Drop the explicit --serve-tls since tls-autoprovision (the
default mode) is exactly what those callers want. Update the example
config to point at the new [ingress] section and link RFD-84.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request introduces a configurable ingress mode system to replace the boolean --serve-tls flag. The system defines three modes (tls-autoprovision, behind-proxy-https, behind-proxy-http) with different TLS termination and port-binding behavior. New configuration schema adds an [ingress] block with mode and optional address fields. Configuration validation enforces mode-to-address coherence and rejects TLS settings when using HTTP-only mode. TLS component helpers support address-specific HTTPS serving for reverse-proxy scenarios. The server startup path performs coherence validation early and uses a mode switch to initialize appropriate listeners. Documentation and examples are updated throughout.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/commands/server.go`:
- Around line 872-883: The behind-proxy-http branch currently spawns a detached
goroutine that calls http.ListenAndServe(addr, hs) and only logs errors; change
it to register the listener with the existing errgroup by calling eg.Go(func()
error { ctx.Log.Info("starting HTTP server", "addr", addr); return
http.ListenAndServe(addr, hs) }) so that startup failures are propagated through
eg.Wait(); keep the same logging context (addr and hs) but remove the
fire-and-forget go func and ensure the returned error is handled by the
surrounding code that calls eg.Wait().

In `@configs/server.toml.example`:
- Around line 49-52: The comment for ingress.address incorrectly states the
value is "ignored" under tls-autoprovision; update the docs to reflect actual
behavior: state that setting ingress.address is invalid in tls-autoprovision and
will cause configuration validation to fail (rather than being ignored).
Reference the ingress.address config key and tls-autoprovision mode in the
comment so users know the validator will reject this value; alternatively, if
you prefer to change behavior instead of docs, adjust the validation logic that
checks mode == "tls-autoprovision" to accept/ignore ingress.address (edit the
validator function that enforces ingress.address rules).

In `@pkg/serverconfig/cli.gen.go`:
- Around line 25-26: The CLI help for the ingress flags is stale: update the
descriptions for the IngressConfigAddress field/flag (--ingress-address) to
reflect that it is validated (not ignored) for tls-autoprovision and clarify the
behavior/constraints, and correct the IngressConfigMode description to
accurately describe the current TLS model (replace the outdated
"behind-proxy-https" wording with the actual mode semantics used by the TLS
configuration); make these changes at the schema/source level where
IngressConfigAddress and IngressConfigMode are defined and then regenerate
pkg/serverconfig/cli.gen.go so the compiled help text matches runtime validation
and current TLS behavior.

In `@pkg/serverconfig/schema.yml`:
- Around line 197-200: Update the description string for the ingress-mode CLI
option (cli.long: ingress-mode, env: MIREN_INGRESS_MODE) so the
"behind-proxy-https" mode no longer incorrectly claims "no ACME provisioning";
specifically edit the text for the behind-proxy-https entry to state that TLS is
terminated by Miren with externally-provided certificates and remove or soften
the absolute "no ACME provisioning" claim (e.g., mention that ACME is not
performed by Miren for externally-provided certs or omit ACME detail entirely).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0d36a27-21bc-4300-b275-d7b535be59d4

📥 Commits

Reviewing files that changed from the base of the PR and between a994b74 and b0645bd.

📒 Files selected for processing (20)
  • cli/commands/server.go
  • cli/commands/server_config_cmds.go
  • cli/commands/server_install.go
  • components/autotls/autotls.go
  • components/autotls/selfsigned.go
  • configs/server.toml.example
  • docs/docs/command/server.md
  • docs/docs/server-config.md
  • docs/docs/tls.md
  • hack/systemd/entrypoint.sh
  • pkg/serverconfig/cli.gen.go
  • pkg/serverconfig/config.gen.go
  • pkg/serverconfig/defaults.gen.go
  • pkg/serverconfig/env.gen.go
  • pkg/serverconfig/ingress_modes.go
  • pkg/serverconfig/loader.gen.go
  • pkg/serverconfig/schema.yml
  • pkg/serverconfig/validate.go
  • pkg/serverconfig/validate_test.go
  • pkg/serverconfig/validation.gen.go

Comment thread cli/commands/server.go Outdated
Comment thread configs/server.toml.example
Comment thread pkg/serverconfig/cli.gen.go Outdated
Comment thread pkg/serverconfig/schema.yml
phinze added 2 commits May 12, 2026 10:19
Four findings, all real:

- behind-proxy-http listener now goes through the existing errgroup
  (eg.Go) for both serve and graceful shutdown, so startup failures
  propagate via eg.Wait() instead of getting logged and ignored.
  The fire-and-forget pattern is still present in the autotls
  helpers; that's a separate cleanup to do across all the ingress
  paths in a follow-up.
- Help text and config example said 'ignored' under tls-autoprovision
  for ingress.address, but the validator hard-errors. Fixed both
  the schema description and the example comment to say 'rejected
  by validation' so users know what to expect.
- behind-proxy-https mode description claimed 'no ACME provisioning',
  which conflicts with DNS-01 support called out in the RFD and in
  ValidateIngressCoherence. Softened to 'certs from self-signed or
  DNS-01 ACME, since :80 isn't bound for HTTP-01'.

cli.gen.go regenerated from the schema changes.
The previous commit updated the schema descriptions but rebuilt with
a stale binary, so the generated docs/docs/command/server.md still
had the old text. CI's go-generate-check caught it.
Copy link
Copy Markdown
Contributor

@evanphx evanphx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, but we need to accept --start-tls as a noop (maybe mark it hidden so it's not normally used), so that upgrades don't break.

TLSConfigAdditionalIPs []string `long:"ips" description:"Additional IPs assigned to the server cert"`
TLSConfigAdditionalNames []string `long:"dns-names" description:"Additional DNS names assigned to the server cert"`
TLSConfigSelfSigned *bool `long:"self-signed-tls" description:"Use self-signed certificates for TLS (for development/testing only)"`
TLSConfigStandardTLS *bool `long:"serve-tls" description:"Expose the http ingress on standard TLS ports"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should still include this option but ignore it so that upgrades don't break.

Copy link
Copy Markdown
Contributor Author

phinze commented May 12, 2026

Yeah fair call I was waffling on the hard-line removal but better to be nicer

…afety

Per Evan's review on #799: existing systemd unit files from pre-RFD-84
installs pass --serve-tls in ExecStart, and operators may have
standard_tls = true in their config.toml or MIREN_TLS_STANDARD_TLS
set in their environment. The previous commit removed the field
outright, which would break those upgrades by either rejecting the
unknown flag or (for env/TOML cases) silently dropping a value the
operator expected to take effect.

Re-adding the field as a real (non-cli_only) bool on TLSConfig so:

- TOML standard_tls = true parses cleanly (go-toml/v2 is lenient
  about unknown fields anyway, but the explicit field is clearer)
- MIREN_TLS_STANDARD_TLS env var is read into the field
- --serve-tls CLI flag is accepted

The field is never read by any code path. ingress.mode is the source
of truth for the deployment shape.

A deprecation warning fires at startup when the field is explicitly
set (i.e. StandardTLS != nil), pointing operators at RFD-84.

Also extended configgen with a cli.hidden schema knob and used it
here. Note: mflags (Miren's CLI library) doesn't currently read the
hidden tag, so --serve-tls still appears in --help. The 'Deprecated
and ignored' description is loud enough on its own; if we want true
hiding later, that's a small mflags PR.
@phinze
Copy link
Copy Markdown
Contributor Author

phinze commented May 12, 2026

Addressed in 1145ee7c. The flag, env var, and TOML field all parse again. The field's never read by any code path so behavior matches the new tls-autoprovision default; a startup WARN fires when it's explicitly set so operators get nudged toward ingress.mode. Tried to mark it hidden in --help via a new cli.hidden schema knob, but mflags doesn't read the hidden tag yet so the flag still shows in help with a "Deprecated and ignored" description. Hiding it for real is a small follow-up against mflags if we want it.

@phinze phinze requested a review from evanphx May 12, 2026 18:09
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/serverconfig/validate.go (1)

69-73: ⚡ Quick win

Guard WarnDeprecatedConfig against nil logger.

log.Warn(...) is unconditional; a nil logger would panic. A small defaulting guard makes this safer.

Suggested patch
 func (c *Config) WarnDeprecatedConfig(log *slog.Logger) {
+	if log == nil {
+		log = slog.Default()
+	}
 	if c.TLS.StandardTLS != nil {
 		log.Warn("tls.standard_tls (also --serve-tls / MIREN_TLS_STANDARD_TLS) is deprecated and ignored; use ingress.mode to pick the deployment shape. See RFD-84 at rfd.miren.garden/rfd/84.",
 			"value", *c.TLS.StandardTLS)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/serverconfig/validate.go` around lines 69 - 73, The WarnDeprecatedConfig
method calls log.Warn unconditionally and will panic if the passed slog.Logger
is nil; update WarnDeprecatedConfig to guard against a nil logger (e.g., if log
== nil { return } or initialize a no-op/default logger) before using log.Warn,
keeping the existing conditional on c.TLS.StandardTLS and the deprecated message
text (referencing WarnDeprecatedConfig, the log parameter, and
c.TLS.StandardTLS).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/serverconfig/validate.go`:
- Around line 69-73: The WarnDeprecatedConfig method calls log.Warn
unconditionally and will panic if the passed slog.Logger is nil; update
WarnDeprecatedConfig to guard against a nil logger (e.g., if log == nil { return
} or initialize a no-op/default logger) before using log.Warn, keeping the
existing conditional on c.TLS.StandardTLS and the deprecated message text
(referencing WarnDeprecatedConfig, the log parameter, and c.TLS.StandardTLS).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84716c44-3e01-4a8b-a460-7e7a1fc8e607

📥 Commits

Reviewing files that changed from the base of the PR and between 07d65f4 and 1145ee7.

📒 Files selected for processing (12)
  • cli/commands/server.go
  • cli/commands/server_config_cmds.go
  • docs/docs/command/server.md
  • pkg/serverconfig/cli.gen.go
  • pkg/serverconfig/cmd/configgen/main.go
  • pkg/serverconfig/config.gen.go
  • pkg/serverconfig/defaults.gen.go
  • pkg/serverconfig/env.gen.go
  • pkg/serverconfig/loader.gen.go
  • pkg/serverconfig/schema.yml
  • pkg/serverconfig/toml_strict_test.go
  • pkg/serverconfig/validate.go
✅ Files skipped from review due to trivial changes (5)
  • pkg/serverconfig/defaults.gen.go
  • pkg/serverconfig/loader.gen.go
  • pkg/serverconfig/cli.gen.go
  • pkg/serverconfig/config.gen.go
  • pkg/serverconfig/env.gen.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • cli/commands/server.go

@phinze phinze merged commit f9eec65 into main May 12, 2026
19 checks passed
@phinze phinze deleted the phinze/mir-1115-run-miren-behind-a-reverse-proxy branch May 12, 2026 18:21
@phinze
Copy link
Copy Markdown
Contributor Author

phinze commented May 12, 2026

@jcasimir I clicked merge but still very open to any feedback you have at any point! Definitely want to make sure this works for your use case 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants