make caddy a L4 proxy to terminate TLS for HiveMQ
This commit is contained in:
+55
-10
@@ -3,7 +3,24 @@
|
||||
let
|
||||
cfg = config.services.gebos.caddy;
|
||||
|
||||
siteBlock = host: site:
|
||||
tcpProxySites = lib.filterAttrs (_: s: s ? tcpProxy) cfg.sites;
|
||||
httpSites = lib.filterAttrs (_: s: !(s ? tcpProxy)) cfg.sites;
|
||||
needsLayer4 = tcpProxySites != { };
|
||||
|
||||
# External MQTTS port the senders connect to (mqtt_port = 8883, SSL).
|
||||
tlsListenPort = 8883;
|
||||
|
||||
# Caddy built with the layer4 module (caddy-l4) — needed to TLS-terminate raw
|
||||
# MQTT and proxy it to HiveMQ, which Caddy's HTTP-only core can't do. Only
|
||||
# built when a host actually has a tcpProxy site; app-host's static/upstream
|
||||
# sites run on stock Caddy so they don't pay for the custom build.
|
||||
caddyWithL4 = pkgs.caddy.withPlugins {
|
||||
plugins = [ "github.com/mholt/caddy-l4@v0.1.1" ];
|
||||
hash = "sha256-O6GuC2q1mA/Fa0utb2Yg7ZE73iq13oVYhJI1IVyOvog=";
|
||||
};
|
||||
|
||||
# HTTP-style sites: static file server or reverse proxy.
|
||||
httpSiteBlock = host: site:
|
||||
if site ? staticRoot then ''
|
||||
${host} {
|
||||
root * ${site.staticRoot}
|
||||
@@ -18,14 +35,38 @@ let
|
||||
encode zstd gzip
|
||||
}
|
||||
''
|
||||
else if site ? tcpProxy then ''
|
||||
${host}:8883 {
|
||||
# TLS termination for MQTT — Caddy's `layer4` app would be ideal here;
|
||||
# for now, document that the upstream HiveMQ port is ${site.tcpProxy}.
|
||||
# TODO: switch to the caddy-l4 module or terminate TLS in HiveMQ directly.
|
||||
}
|
||||
''
|
||||
else throw "site `${host}` needs one of: staticRoot, upstream, tcpProxy";
|
||||
|
||||
# A tcpProxy site needs TWO Caddyfile pieces:
|
||||
# 1. a layer4 listener (in the global options block) that terminates TLS on
|
||||
# :8883 and proxies cleartext to the upstream (HiveMQ on 127.0.0.1:1883);
|
||||
# 2. a normal HTTPS site block for the host, purely so Caddy's automatic
|
||||
# HTTPS OBTAINS the ACME cert that the layer4 `tls` handler then serves by
|
||||
# SNI — caddy-l4's `tls` handler only terminates, it never provisions
|
||||
# certs. This is why ports 80/443 must stay open for the ACME challenge.
|
||||
layer4Listener = _host: site: ''
|
||||
:${toString tlsListenPort} {
|
||||
route {
|
||||
tls
|
||||
proxy ${site.tcpProxy}
|
||||
}
|
||||
}
|
||||
'';
|
||||
|
||||
certSiteBlock = host: _site: ''
|
||||
${host} {
|
||||
respond "gebos MQTT ingest — connect MQTTS on :${toString tlsListenPort}" 200
|
||||
}
|
||||
'';
|
||||
|
||||
# Caddyfile global options block must come first and exist once; we fold every
|
||||
# layer4 listener into a single `layer4 { ... }` inside it.
|
||||
globalOptions = lib.optionalString needsLayer4 ''
|
||||
{
|
||||
layer4 {
|
||||
${lib.concatStrings (lib.mapAttrsToList layer4Listener tcpProxySites)} }
|
||||
}
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.services.gebos.caddy = {
|
||||
@@ -39,9 +80,13 @@ in
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.caddy = {
|
||||
enable = true;
|
||||
package = if needsLayer4 then caddyWithL4 else pkgs.caddy;
|
||||
email = "ops@gebos.online"; # TODO: confirm ACME contact
|
||||
extraConfig = lib.concatStringsSep "\n"
|
||||
(lib.mapAttrsToList siteBlock cfg.sites);
|
||||
extraConfig = lib.concatStringsSep "\n" (
|
||||
[ globalOptions ]
|
||||
++ lib.mapAttrsToList httpSiteBlock httpSites
|
||||
++ lib.mapAttrsToList certSiteBlock tcpProxySites
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user