feat(#3517): publish forgejo's metrics behind the gateway
Forgejo can serve prometheus metrics but nothing turned them on, and turning them on alone would have published them: forgejo serves `/metrics` on its normal listener and the gateway vhost proxies `/` to that listener, so the existing catch-all would have carried the endpoint to anyone. The option is therefore one switch for both halves, and the `= /metrics` location is an exact match so it outranks that prefix. Authentication is the gateway's rather than forgejo's own `[metrics] TOKEN`: a scraper presents an audience-scoped authelia token which nginx checks via auth_request, so the swarm keeps one identity system instead of gaining a static bearer per service. The subrequest deliberately omits the `error_page 401 =302` that swarm-ui uses. That redirect sends a browser to a login page; a scraper would follow it and parse HTML as metrics.
This commit is contained in:
parent
4af890f01d
commit
ae129835ae
1 changed files with 94 additions and 9 deletions
|
|
@ -305,6 +305,36 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
metricsEnable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
example = true;
|
||||||
|
description = ''
|
||||||
|
Serve forgejo's prometheus metrics and publish them, behind
|
||||||
|
authelia, at `https://<domain>/metrics`.
|
||||||
|
|
||||||
|
Off by default. Forgejo's metrics carry repository, user and
|
||||||
|
request counts for the whole hive, so turning them on is a
|
||||||
|
disclosure decision an operator should make deliberately rather
|
||||||
|
than inherit from a collector appearing elsewhere in the swarm.
|
||||||
|
|
||||||
|
::: {.warning}
|
||||||
|
This option is deliberately **one** switch for two changes that
|
||||||
|
must never be made separately. Forgejo serves `/metrics` on its
|
||||||
|
normal HTTP listener, and the gateway vhost proxies `/` to that
|
||||||
|
listener — so enabling the endpoint *without* the protected
|
||||||
|
location would publish it through the existing catch-all, to
|
||||||
|
anyone. The `= /metrics` location added alongside is an exact
|
||||||
|
match and therefore wins over the `/` prefix.
|
||||||
|
:::
|
||||||
|
|
||||||
|
Authentication is the gateway's, not forgejo's own `[metrics]
|
||||||
|
TOKEN`: a scraper presents an audience-scoped authelia token and
|
||||||
|
nginx checks it via `auth_request`, so the swarm keeps one
|
||||||
|
identity system instead of a static bearer per service.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
mirrors = lib.mkOption {
|
mirrors = lib.mkOption {
|
||||||
type = lib.types.listOf (
|
type = lib.types.listOf (
|
||||||
lib.types.submodule {
|
lib.types.submodule {
|
||||||
|
|
@ -450,15 +480,59 @@ in
|
||||||
"${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // {
|
"${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // {
|
||||||
listen = gatewayCfg.lib.listen;
|
listen = gatewayCfg.lib.listen;
|
||||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
extraConfig = gatewayCfg.lib.securityHeaders;
|
||||||
locations."/" = {
|
# ⚠️ The metrics locations merge into THIS attrset, not into the
|
||||||
proxyPass = "http://127.0.0.1:${toString cfg.httpPort}/";
|
# vhost above it. `//` is shallow: merging at the vhost level
|
||||||
proxyWebsockets = true;
|
# would replace `locations` wholesale and silently drop the `/`
|
||||||
extraConfig = ''
|
# proxy — a config that still renders and still starts.
|
||||||
proxy_buffering off;
|
locations = {
|
||||||
client_max_body_size 1G;
|
"/" = {
|
||||||
proxy_read_timeout 1h;
|
proxyPass = "http://127.0.0.1:${toString cfg.httpPort}/";
|
||||||
proxy_send_timeout 1h;
|
proxyWebsockets = true;
|
||||||
'';
|
extraConfig = ''
|
||||||
|
proxy_buffering off;
|
||||||
|
client_max_body_size 1G;
|
||||||
|
proxy_read_timeout 1h;
|
||||||
|
proxy_send_timeout 1h;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs cfg.metricsEnable {
|
||||||
|
# ⚠️ EXACT match, and that is what makes this safe. Forgejo
|
||||||
|
# serves `/metrics` on the same listener the `/` prefix above
|
||||||
|
# already proxies, so without a more specific location the
|
||||||
|
# endpoint would be public the moment it is enabled.
|
||||||
|
# `= /metrics` outranks the `/` prefix in nginx, so this
|
||||||
|
# location — and its auth — is the one that runs.
|
||||||
|
"= /metrics" = {
|
||||||
|
proxyPass = "http://127.0.0.1:${toString cfg.httpPort}/metrics";
|
||||||
|
extraConfig = ''
|
||||||
|
auth_request /__forge_metrics_authz;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# The subrequest. Same implementation and header set as
|
||||||
|
# `swarm-ui.nix` uses, for the same reason: `X-Original-URL`
|
||||||
|
# and `X-Original-Method` are what authelia's `auth-request`
|
||||||
|
# implementation reads.
|
||||||
|
#
|
||||||
|
# ⚠️ NO `error_page 401 =302` here, and its absence is the
|
||||||
|
# whole point. The swarm UI redirects an unauthenticated
|
||||||
|
# browser to a login page; a scraper handed that 302 would
|
||||||
|
# follow it and parse an HTML page as metrics. A machine-facing
|
||||||
|
# location lets the 401 reach the client unchanged.
|
||||||
|
"= /__forge_metrics_authz" = {
|
||||||
|
proxyPass = "http://127.0.0.1:${toString autheliaCfg.port}/api/authz/auth-request";
|
||||||
|
extraConfig = ''
|
||||||
|
internal;
|
||||||
|
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;
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -674,6 +748,17 @@ in
|
||||||
DEFAULT_BRANCH = "main";
|
DEFAULT_BRANCH = "main";
|
||||||
DEFAULT_PRIVATE = "private";
|
DEFAULT_PRIVATE = "private";
|
||||||
};
|
};
|
||||||
|
# No `TOKEN` here on purpose. Forgejo can guard this itself
|
||||||
|
# with a static bearer, but the swarm authenticates the
|
||||||
|
# scraper at the gateway with an audience-scoped authelia
|
||||||
|
# token, so a second credential system per service would buy
|
||||||
|
# nothing and would be the one that stops getting rotated.
|
||||||
|
#
|
||||||
|
# Reachability is not decided here: forgejo serves this on
|
||||||
|
# its normal listener, which is loopback-only. What publishes
|
||||||
|
# it is the `= /metrics` vhost location, and that location is
|
||||||
|
# what authenticates it.
|
||||||
|
metrics.ENABLED = cfg.metricsEnable;
|
||||||
# Repo migrations / pull-mirrors fetch from the source
|
# Repo migrations / pull-mirrors fetch from the source
|
||||||
# URL *inside* Forgejo. hyperhive code is synced from
|
# URL *inside* Forgejo. hyperhive code is synced from
|
||||||
# `localhost` (and the host LAN), which Forgejo's
|
# `localhost` (and the host LAN), which Forgejo's
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue