fix(#3396): move the forge onto the shared trust-bundle helper

Last of the three modules that hand-rolled the same concat with wantedBy +
before and no requires, so a failed assembly left the consumer running against
a missing file and trusting nothing -- every outbound TLS call fails while the
unit looks healthy.

The forge is the one with two consumers: forgejo-sso-source fetches the
issuer's discovery document over the swarm CA and once shipped without the
trust its sibling had. It only exists when SSO is on, so the consumer list is
conditional -- naming an absent unit would define a serviceless one and order
nothing.

Removes the now-dead useSelfSigned and caContainerPath bindings (nix does not
warn) and retargets three comments the deletion orphaned, including the helper
header that still named this module as the per-call-site concat.
This commit is contained in:
atlas 2026-08-17 20:35:56 +02:00
commit 2eb74f3a74
2 changed files with 38 additions and 67 deletions

View file

@ -11,18 +11,6 @@ let
swarmDomain = config.services.hyperhive.swarm.domain;
tlsCfg = config.services.hyperhive.tls;
# Self-signed gateway TLS: forgejo (Go) validates outbound webhook
# deliveries (e.g. the config-PR webhook to https://<domain>/webhook/...)
# against its system cert store, which lacks the runtime-generated hive
# CA — so delivery fails with an x509 "unknown authority". Go has no
# additive trust env var (SSL_CERT_FILE *replaces* the default bundle),
# so bind the public CA in and hand forgejo a combined bundle (system
# CAs + hive CA) via SSL_CERT_FILE. Only active in self-signed mode;
# with an operator cert / ACME the public chain already validates and
# this whole block drops out. The bind-mount + `container@` ordering
# that make the CA reachable are shared with hive-ci via the
# `hive-ca-trust` helper; only the Go SSL_CERT_FILE concat below is
# hive-forge-specific.
# Forgejo's name for the login source. A constant, not an option: it
# is the key this module's own idempotency check looks up, so making
# it configurable would buy nothing and add a way for the lookup and
@ -72,9 +60,6 @@ let
ssoRedirectUri = "${effectiveRootUrl}user/oauth2/${ssoSourceName}/callback";
caTrust = import ../lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
useSelfSigned = caTrust.useSelfSigned;
caContainerPath = caTrust.caContainerPath;
forgeCaBundle = "/run/hive-forge-ca/ca-bundle.crt";
# ROOT_URL forgejo advertises in clone links + outbound URLs. When
# served behind the gateway, `cfg.domain` doubles as both the
@ -579,9 +564,9 @@ in
privateNetwork = false;
# Self-signed mode: bind the public hive CA cert read-only so forgejo
# can trust the gateway's self-signed leaf for outbound webhook
# delivery (combined bundle assembled at container start by
# hive-forge-ca-bundle below). Shared bind-mount + ordering come from
# the hive-ca-trust helper.
# delivery. The bind-mount, the `container@` ordering and the bundle
# the container's consumers read all come from the hive-ca-trust
# helper — see the container's `imports` for the consumption half.
bindMounts = caTrust.bindMount;
config =
{ pkgs, ... }:
@ -607,6 +592,26 @@ in
'';
in
{
imports = [
# Forgejo is Go, and `SSL_CERT_FILE` *replaces* the default store
# rather than adding to it — so it needs the system CAs and the
# hive CA concatenated, not the CA alone, or public mirror fetches
# lose every anchor they had.
#
# BOTH units that make an outbound HTTPS call are consumers, not
# just the obvious one: `forgejo-sso-source` fetches the issuer's
# `.well-known/openid-configuration` over the swarm CA, and it
# once shipped without the trust its sibling had. `optional`
# rather than a flat list because that unit only exists when SSO
# is on — naming an absent unit would order nothing and quietly
# define a serviceless one.
(caTrust.trustBundle {
inherit pkgs;
name = "hive-forge";
consumers = [ "forgejo" ] ++ lib.optional cfg.sso.enable "forgejo-sso-source";
})
];
system.stateVersion = "25.11";
# Shared host netns: this container's own firewall.service
@ -764,36 +769,6 @@ in
"d /var/lib/forgejo/data/actions_artifacts 0750 forgejo forgejo - -"
];
# Self-signed mode: assemble the combined TLS trust bundle
# (system CAs + the bind-mounted hive CA) forgejo's Go HTTP
# client validates outbound webhook deliveries against. Go's
# SSL_CERT_FILE *replaces* the default bundle, so we concatenate
# rather than point at the CA alone — otherwise mirror fetches
# from public hosts would lose their trust anchors. Runs before
# forgejo each boot; /run is tmpfs so the bundle is rebuilt from
# the current CA every start.
systemd.services.hive-forge-ca-bundle = lib.mkIf useSelfSigned {
description = "assemble forgejo TLS trust bundle (system CAs + hive CA)";
wantedBy = [ "forgejo.service" ];
before = [ "forgejo.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
SyslogIdentifier = "hive-forge-ca-bundle";
};
path = [ pkgs.coreutils ];
script = ''
set -euo pipefail
install -d -m 0755 /run/hive-forge-ca
cat /etc/ssl/certs/ca-certificates.crt ${caContainerPath} \
> ${forgeCaBundle}
chmod 0644 ${forgeCaBundle}
'';
};
# Point forgejo's Go TLS stack at the combined bundle so webhook
# delivery to the self-signed gateway validates.
systemd.services.forgejo.environment.SSL_CERT_FILE = lib.mkIf useSelfSigned forgeCaBundle;
# Ensure Forgejo has a usable GPG signing key so UI merges / CRUD
# commits are signed instead of erroring "does not have a signing
# key". This service (a) generates a key in forgejo's persistent
@ -888,23 +863,18 @@ in
# `FORGEJO_CUSTOM` (not `GITEA_CUSTOM` — forgejo renamed it)
# is how the CLI finds the app.ini upstream's module wrote.
#
# `SSL_CERT_FILE` for the same reason `forgejo.service` has it,
# and it was missing here: registering the login source makes an
# **outbound HTTPS call** — the CLI fetches
# `<issuer>/.well-known/openid-configuration` to validate the
# provider before writing the row. That URL is a swarm service
# name served under the swarm CA, which the default system store
# has never heard of, so without this the unit fails every single
# time with `x509: certificate signed by unknown authority` and no
# restart can help it.
#
# The trust belongs to every process that makes the call, not to
# the service that happens to be the obvious consumer. Same
# binary, same host, different unit — and only one of them had it.
# This unit is also a trust-bundle consumer (declared in the
# container's `imports` above, which is where `SSL_CERT_FILE`
# comes from): registering the login source makes an **outbound
# HTTPS call** to `<issuer>/.well-known/openid-configuration`,
# served under the swarm CA the default store has never heard of.
# It once shipped without the trust `forgejo.service` had, and
# failed every single time with `x509: certificate signed by
# unknown authority`. The trust belongs to every process that
# makes the call, not to the obvious consumer.
environment = {
FORGEJO_CUSTOM = "/var/lib/forgejo/custom";
}
// lib.optionalAttrs useSelfSigned { SSL_CERT_FILE = forgeCaBundle; };
};
path = [
cfg.package
pkgs.coreutils

View file

@ -6,10 +6,11 @@
# and orders its `container@<name>` unit after `hive-tls-ca.service` so the
# bind source exists before nspawn sets the mount up.
#
# This is the language-agnostic half (bind-mount + systemd ordering). The
# *consumption* differs per runtime and stays at each call site: Node's
# `NODE_EXTRA_CA_CERTS` is additive (hive-ci), Go's `SSL_CERT_FILE` replaces
# the bundle so it needs a system-CAs+hive-CA concat step (hive-forge).
# `bindMount` + `containerOrdering` are the language-agnostic half. The
# *consumption* differs per runtime: an additive variable (Node's
# `NODE_EXTRA_CA_CERTS`, hive-ci) points straight at `caContainerPath` from
# the call site, while a *replacing* one (Go's `SSL_CERT_FILE`, rustls) needs
# the system-CAs+hive-CA concat that `trustBundle` below does for it.
#
# Pure function — NOT a NixOS module (don't add it to the host-modules
# aggregator). Call it from a module's `let`: