gateway: service modules declare their vhost under gateway.vhosts
Ten service modules (forge, matrix chat, authelia, grafana, otel, nats, swarm-ui, victorialogs, victoriametrics, bao UI) wrote services.nginx.virtualHosts themselves, each applying the gateway's listen set, per-name TLS attrs and security headers by hand. They now declare services.hyperhive.gateway.vhosts.<name> with only their own locations and options; ./declared-vhosts.nix renders it into the nginx vhost with listen, TLS and headers added. An eval assertion refuses a declared vhost that sets listen, addSSL, onlySSL, enableACME, useACMEHost, sslCertificate or sslCertificateKey, and one declared while the gateway is off. nats' own leaf is the new `certificate` field. The rendered nginx vhosts are unchanged.
This commit is contained in:
parent
7b50ae57e4
commit
ddb583cc22
17 changed files with 652 additions and 513 deletions
|
|
@ -313,157 +313,153 @@ in
|
|||
# Authenticated front door onto the loopback-only store — see the
|
||||
# file-top comment for why this is safe to add without touching the
|
||||
# store's own (still unauthenticated, still loopback) listener at all.
|
||||
# `removeAttrs`/`forceSSL`: same asymmetry `swarm-ui.nix` documents —
|
||||
# `forceSSL`: same asymmetry `swarm-ui.nix` documents —
|
||||
# authelia answers a plain-http auth subrequest with 400, which
|
||||
# `auth_request` cannot read as anything but a broken check, so this
|
||||
# vhost needs `forceSSL` rather than the `addSSL` every unauthenticated
|
||||
# sibling vhost uses.
|
||||
services.nginx.virtualHosts."${cfg.domain}" =
|
||||
(builtins.removeAttrs (gatewayCfg.lib.tlsFor cfg.domain) [ "addSSL" ])
|
||||
// {
|
||||
forceSSL = true;
|
||||
listen = gatewayCfg.lib.listen;
|
||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
||||
locations = {
|
||||
"/" = {
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.port}/";
|
||||
extraConfig = swarmAuthRequest;
|
||||
};
|
||||
services.hyperhive.gateway.vhosts."${cfg.domain}" = {
|
||||
forceSSL = true;
|
||||
locations = {
|
||||
"/" = {
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.port}/";
|
||||
extraConfig = swarmAuthRequest;
|
||||
};
|
||||
|
||||
# The swarm collector's ingest route. `=` so it outranks the `/`
|
||||
# prefix above — without a more specific location it would ride that
|
||||
# catch-all, and that is the whole hazard here.
|
||||
#
|
||||
# ⚠️ Deliberately NOT `swarmAuthRequest`. That block ends in
|
||||
# `error_page 401 =302`, which is right for a browser and wrong for a
|
||||
# pusher: handed a redirect it follows the redirect and POSTs its
|
||||
# batch at a login page, which answers 200. Ingest then reports
|
||||
# healthy while storing nothing. A machine route lets the 401 reach
|
||||
# the client unchanged.
|
||||
#
|
||||
# `auth_request` does not inherit across sibling locations (see the
|
||||
# note on `swarmAuthRequest` above), so naming it here is required
|
||||
# rather than redundant.
|
||||
"= /insert/opentelemetry/v1/logs" = {
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.port}/insert/opentelemetry/v1/logs";
|
||||
extraConfig = ''
|
||||
auth_request /__hive_authelia;
|
||||
'';
|
||||
};
|
||||
# The swarm collector's ingest route. `=` so it outranks the `/`
|
||||
# prefix above — without a more specific location it would ride that
|
||||
# catch-all, and that is the whole hazard here.
|
||||
#
|
||||
# ⚠️ Deliberately NOT `swarmAuthRequest`. That block ends in
|
||||
# `error_page 401 =302`, which is right for a browser and wrong for a
|
||||
# pusher: handed a redirect it follows the redirect and POSTs its
|
||||
# batch at a login page, which answers 200. Ingest then reports
|
||||
# healthy while storing nothing. A machine route lets the 401 reach
|
||||
# the client unchanged.
|
||||
#
|
||||
# `auth_request` does not inherit across sibling locations (see the
|
||||
# note on `swarmAuthRequest` above), so naming it here is required
|
||||
# rather than redundant.
|
||||
"= /insert/opentelemetry/v1/logs" = {
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.port}/insert/opentelemetry/v1/logs";
|
||||
extraConfig = ''
|
||||
auth_request /__hive_authelia;
|
||||
'';
|
||||
};
|
||||
|
||||
# The read side of that same split: the route a caller holding a
|
||||
# bearer token queries, as opposed to the `/` above which is the
|
||||
# operator's browser. Bare `auth_request` for the ingest route's
|
||||
# exact reason — `error_page 401 =302` hands an unauthenticated
|
||||
# machine caller authelia's login page as a **200 with an HTML
|
||||
# body**, so a client that reads the status code records a
|
||||
# successful query that returned no rows. 401 is the only answer
|
||||
# here that a caller cannot mistake for an empty result.
|
||||
#
|
||||
# `^~` so it outranks the `/` catch-all and stays ahead of any
|
||||
# regex location a later change adds. The prefix is the whole
|
||||
# LogsQL query API and nothing else — `query`, `tail`, `hits`,
|
||||
# `facets`, the `stats_query*` and the `field_*`/`stream_*`
|
||||
# routes — while the browser UI sits beside it under
|
||||
# `/select/vmui`. Both read off the pinned build, not the docs.
|
||||
#
|
||||
# The query is forwarded unmodified — no scoping parameter is
|
||||
# injected, so any authenticated caller reads the whole swarm's
|
||||
# logs. That is the rule in force, not an omission: read
|
||||
# permissions are a later thing, and this location is where one
|
||||
# attaches when it exists.
|
||||
#
|
||||
# Three checks: an agent's own `swarm-agent.` bearer goes to
|
||||
# `swarm-logs-auth`, Grafana's 64-hex bearer to `swarm-metrics-auth`,
|
||||
# anything else to authelia (`appendHttpConfig` above picks).
|
||||
"^~ /select/logsql/" = {
|
||||
# No URI part, so the request path and its query string reach
|
||||
# the store as the caller sent them.
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.port}";
|
||||
extraConfig = ''
|
||||
auth_request /__swarm_logs_auth;
|
||||
'';
|
||||
};
|
||||
# The read side of that same split: the route a caller holding a
|
||||
# bearer token queries, as opposed to the `/` above which is the
|
||||
# operator's browser. Bare `auth_request` for the ingest route's
|
||||
# exact reason — `error_page 401 =302` hands an unauthenticated
|
||||
# machine caller authelia's login page as a **200 with an HTML
|
||||
# body**, so a client that reads the status code records a
|
||||
# successful query that returned no rows. 401 is the only answer
|
||||
# here that a caller cannot mistake for an empty result.
|
||||
#
|
||||
# `^~` so it outranks the `/` catch-all and stays ahead of any
|
||||
# regex location a later change adds. The prefix is the whole
|
||||
# LogsQL query API and nothing else — `query`, `tail`, `hits`,
|
||||
# `facets`, the `stats_query*` and the `field_*`/`stream_*`
|
||||
# routes — while the browser UI sits beside it under
|
||||
# `/select/vmui`. Both read off the pinned build, not the docs.
|
||||
#
|
||||
# The query is forwarded unmodified — no scoping parameter is
|
||||
# injected, so any authenticated caller reads the whole swarm's
|
||||
# logs. That is the rule in force, not an omission: read
|
||||
# permissions are a later thing, and this location is where one
|
||||
# attaches when it exists.
|
||||
#
|
||||
# Three checks: an agent's own `swarm-agent.` bearer goes to
|
||||
# `swarm-logs-auth`, Grafana's 64-hex bearer to `swarm-metrics-auth`,
|
||||
# anything else to authelia (`appendHttpConfig` above picks).
|
||||
"^~ /select/logsql/" = {
|
||||
# No URI part, so the request path and its query string reach
|
||||
# the store as the caller sent them.
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.port}";
|
||||
extraConfig = ''
|
||||
auth_request /__swarm_logs_auth;
|
||||
'';
|
||||
};
|
||||
|
||||
# The store's own health, which it publishes as prometheus metrics
|
||||
# on the listener it serves queries on. The swarm collector requests
|
||||
# exactly this URL, declared as `publishedScrapeTargets.victorialogs`
|
||||
# in ./swarm-otel-service.nix; the two must agree, since that string
|
||||
# is also the token's audience.
|
||||
#
|
||||
# Bare `auth_request` to authelia, as on the ingest route above, and
|
||||
# never `/__swarm_logs_auth`, which admits an agent's bearer:
|
||||
# authelia's rules for this URL admit the collector's client and
|
||||
# deny every other subject (see `metricsRules` in ./swarm-authelia.nix).
|
||||
"= /metrics" = {
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.port}/metrics";
|
||||
extraConfig = ''
|
||||
auth_request /__hive_authelia;
|
||||
'';
|
||||
};
|
||||
# The store's own health, which it publishes as prometheus metrics
|
||||
# on the listener it serves queries on. The swarm collector requests
|
||||
# exactly this URL, declared as `publishedScrapeTargets.victorialogs`
|
||||
# in ./swarm-otel-service.nix; the two must agree, since that string
|
||||
# is also the token's audience.
|
||||
#
|
||||
# Bare `auth_request` to authelia, as on the ingest route above, and
|
||||
# never `/__swarm_logs_auth`, which admits an agent's bearer:
|
||||
# authelia's rules for this URL admit the collector's client and
|
||||
# deny every other subject (see `metricsRules` in ./swarm-authelia.nix).
|
||||
"= /metrics" = {
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.port}/metrics";
|
||||
extraConfig = ''
|
||||
auth_request /__hive_authelia;
|
||||
'';
|
||||
};
|
||||
|
||||
# `rewrite … last` re-matches locations, so the subrequest lands in
|
||||
# one of the two internal locations named by the map.
|
||||
"= /__swarm_logs_auth" = {
|
||||
extraConfig = ''
|
||||
internal;
|
||||
rewrite ^ $swarm_logs_auth_target last;
|
||||
'';
|
||||
};
|
||||
# `rewrite … last` re-matches locations, so the subrequest lands in
|
||||
# one of the two internal locations named by the map.
|
||||
"= /__swarm_logs_auth" = {
|
||||
extraConfig = ''
|
||||
internal;
|
||||
rewrite ^ $swarm_logs_auth_target last;
|
||||
'';
|
||||
};
|
||||
|
||||
# The `Authorization` header rides the subrequest unchanged.
|
||||
#
|
||||
# Any well-formed agent bearer the responder has not recently granted
|
||||
# costs a secret store login and read, so the rate per client address
|
||||
# is capped here, ahead of the responder. 403 when over: the only
|
||||
# statuses `auth_request` passes through are 401 and 403, and
|
||||
# `swarm-logs` retries a 401.
|
||||
"= /__swarm_logs_agent_auth" = {
|
||||
proxyPass = "http://unix:${logsAuthSocket}:/";
|
||||
extraConfig = ''
|
||||
internal;
|
||||
limit_req zone=swarm_logs_agent_auth burst=${toString deployCfg.victorialogs.authBurst} nodelay;
|
||||
limit_req_status 403;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
'';
|
||||
};
|
||||
# The `Authorization` header rides the subrequest unchanged.
|
||||
#
|
||||
# Any well-formed agent bearer the responder has not recently granted
|
||||
# costs a secret store login and read, so the rate per client address
|
||||
# is capped here, ahead of the responder. 403 when over: the only
|
||||
# statuses `auth_request` passes through are 401 and 403, and
|
||||
# `swarm-logs` retries a 401.
|
||||
"= /__swarm_logs_agent_auth" = {
|
||||
proxyPass = "http://unix:${logsAuthSocket}:/";
|
||||
extraConfig = ''
|
||||
internal;
|
||||
limit_req zone=swarm_logs_agent_auth burst=${toString deployCfg.victorialogs.authBurst} nodelay;
|
||||
limit_req_status 403;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
'';
|
||||
};
|
||||
|
||||
# Grafana's bearer, 64 hex digits (`openssl rand -hex 32`), checked by
|
||||
# `swarm-metrics-auth`. The responder reads the store at most once per
|
||||
# its cache TTL whatever the bearers, so no rate limit sits in front.
|
||||
"= /__swarm_logs_grafana_auth" = {
|
||||
proxyPass = "http://unix:${grafanaAuthSocket}:/";
|
||||
extraConfig = ''
|
||||
internal;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
'';
|
||||
};
|
||||
# Grafana's bearer, 64 hex digits (`openssl rand -hex 32`), checked by
|
||||
# `swarm-metrics-auth`. The responder reads the store at most once per
|
||||
# its cache TTL whatever the bearers, so no rate limit sits in front.
|
||||
"= /__swarm_logs_grafana_auth" = {
|
||||
proxyPass = "http://unix:${grafanaAuthSocket}:/";
|
||||
extraConfig = ''
|
||||
internal;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
'';
|
||||
};
|
||||
|
||||
# The subrequest itself — same target, same header set, same
|
||||
# reasoning as `swarm-ui.nix`'s own copy (measured against the
|
||||
# pinned authelia binary, not copied from an example).
|
||||
"= /__hive_authelia" = {
|
||||
proxyPass = "https://${hyperhiveCfg.swarm.authelia.domain}/api/authz/auth-request";
|
||||
# nixpkgs appends its OWN `Host $host` after extraConfig,
|
||||
# which would override verifiedProxyTo's — see the comment
|
||||
# on verifiedProxyTo in hive-gateway/vhost-lib.nix.
|
||||
recommendedProxySettings = false;
|
||||
extraConfig = ''
|
||||
internal;
|
||||
${gatewayCfg.lib.verifiedProxyTo hyperhiveCfg.swarm.authelia.domain}
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header X-Original-Method $request_method;
|
||||
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
'';
|
||||
};
|
||||
# The subrequest itself — same target, same header set, same
|
||||
# reasoning as `swarm-ui.nix`'s own copy (measured against the
|
||||
# pinned authelia binary, not copied from an example).
|
||||
"= /__hive_authelia" = {
|
||||
proxyPass = "https://${hyperhiveCfg.swarm.authelia.domain}/api/authz/auth-request";
|
||||
# nixpkgs appends its OWN `Host $host` after extraConfig,
|
||||
# which would override verifiedProxyTo's — see the comment
|
||||
# on verifiedProxyTo in hive-gateway/vhost-lib.nix.
|
||||
recommendedProxySettings = false;
|
||||
extraConfig = ''
|
||||
internal;
|
||||
${gatewayCfg.lib.verifiedProxyTo hyperhiveCfg.swarm.authelia.domain}
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header X-Original-Method $request_method;
|
||||
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# The agents' check on `/select/logsql/`. On the host rather than in the
|
||||
# store's container: it needs nothing of the store, only nginx's reach to
|
||||
|
|
|
|||
Loading…
Reference in a new issue