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 {
|
||||
type = lib.types.listOf (
|
||||
lib.types.submodule {
|
||||
|
|
@ -450,15 +480,59 @@ in
|
|||
"${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // {
|
||||
listen = gatewayCfg.lib.listen;
|
||||
extraConfig = gatewayCfg.lib.securityHeaders;
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.httpPort}/";
|
||||
proxyWebsockets = true;
|
||||
extraConfig = ''
|
||||
proxy_buffering off;
|
||||
client_max_body_size 1G;
|
||||
proxy_read_timeout 1h;
|
||||
proxy_send_timeout 1h;
|
||||
'';
|
||||
# ⚠️ The metrics locations merge into THIS attrset, not into the
|
||||
# vhost above it. `//` is shallow: merging at the vhost level
|
||||
# would replace `locations` wholesale and silently drop the `/`
|
||||
# proxy — a config that still renders and still starts.
|
||||
locations = {
|
||||
"/" = {
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.httpPort}/";
|
||||
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_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
|
||||
# URL *inside* Forgejo. hyperhive code is synced from
|
||||
# `localhost` (and the host LAN), which Forgejo's
|
||||
|
|
|
|||
Loading…
Reference in a new issue