Wire Forge-native release infrastructure
Some checks failed
Cache: Publish Nix / Publish Nix Cache (push) Waiting to run
Build Rust / Cargo Test (push) Successful in 4m14s
Build Site / Next.js Build (push) Failing after 6s
Infra: OpenTofu / OpenTofu (grafana) (push) Successful in 5s
Lint Governance / BEP Metadata (push) Successful in 0s
Build: Android / Android Rust Core Stub (push) Failing after 23s

This commit is contained in:
Conrad Kramer 2026-06-07 05:51:12 -07:00
parent 97c569fb35
commit 002bd382e9
199 changed files with 14268 additions and 185 deletions

View file

@ -2,7 +2,7 @@
This directory contains the Burrow forge host definition and the Hetzner bootstrap shape for `burrow-forge`.
Mail hosting is intentionally not part of this NixOS host in the current plan. Burrow's first mail path is Forward Email with Burrow-owned custom S3 backups; see [`docs/FORWARDEMAIL.md`](../docs/FORWARDEMAIL.md).
Mail hosting is intentionally not part of this NixOS host in the current plan. Burrow's first mail path is Forward Email with Burrow-owned custom S3 backups; see [`docs/FORWARDEMAIL.md`](../docs/FORWARDEMAIL.md). `inbox.burrow.net` remains the planned webmail surface for the later Stalwart phase.
## Files
@ -20,6 +20,7 @@ Mail hosting is intentionally not part of this NixOS host in the current plan. B
- `../Scripts/nsc-build-and-upload-image.sh`: temporary Namespace builder -> raw image -> Hetzner snapshot
- `../Scripts/bootstrap-forge-intake.sh`: copy the Forgejo bootstrap password and agent SSH key into `/var/lib/burrow/intake/`
- `../Scripts/check-forge-host.sh`: verify Forgejo, Caddy, the local runner, optional NSC services, and optional Tailnet services after boot
- `../Scripts/grafana-tofu.sh`: manage Grafana API-created folders and dashboards with OpenTofu
- `../Scripts/cloudflare-upsert-a-record.sh`: upsert DNS-only Cloudflare `A` records for Burrow host cutovers
- `../Scripts/forge-deploy.sh`: remote `nixos-rebuild` entrypoint for the forge host
- `../Scripts/provision-forgejo-nsc.sh`: render Burrow Namespace dispatcher/autoscaler runtime inputs and ensure the default Forgejo scope exists
@ -34,18 +35,21 @@ Mail hosting is intentionally not part of this NixOS host in the current plan. B
5. Let `burrow-forgejo-runner-bootstrap.service` register the self-hosted Forgejo runner and seed Git identity as `agent <agent@burrow.net>`.
6. Run `Scripts/provision-forgejo-nsc.sh` locally to refresh `intake/forgejo_nsc_token.txt`, `intake/forgejo_nsc_dispatcher.yaml`, and `intake/forgejo_nsc_autoscaler.yaml`.
7. Run `Scripts/seal-forgejo-nsc-secrets.sh` to encrypt those runtime inputs into the agenix secrets used by `burrow-forge`.
8. Ensure `/var/lib/agenix/agenix.key` exists on the host, encrypt `secrets/infra/authentik.env.age`, `secrets/infra/authentik-google-client-id.age`, `secrets/infra/authentik-google-client-secret.age`, `secrets/infra/forgejo-oidc-client-secret.age`, `secrets/infra/headscale-oidc-client-secret.age`, `secrets/infra/forgejo-nsc-token.age`, `secrets/infra/forgejo-nsc-dispatcher-config.age`, and `secrets/infra/forgejo-nsc-autoscaler-config.age`, and let agenix materialize them under `/run/agenix/`.
9. Use `Scripts/cloudflare-upsert-a-record.sh` to point `git.burrow.net`, `burrow.net`, `auth.burrow.net`, `ts.burrow.net`, and `nsc-autoscaler.burrow.net` at the host with Cloudflare proxying disabled for ACME.
8. Ensure `/var/lib/agenix/agenix.key` exists on the host, encrypt `secrets/infra/authentik.env.age`, `secrets/infra/authentik-google-client-id.age`, `secrets/infra/authentik-google-client-secret.age`, `secrets/infra/forgejo-oidc-client-secret.age`, `secrets/infra/grafana-admin-password.age`, `secrets/infra/grafana-oidc-client-secret.age`, `secrets/infra/headscale-oidc-client-secret.age`, `secrets/infra/forgejo-nsc-token.age`, `secrets/infra/forgejo-nsc-dispatcher-config.age`, and `secrets/infra/forgejo-nsc-autoscaler-config.age`, and let agenix materialize them under `/run/agenix/`.
9. Use `Scripts/cloudflare-upsert-a-record.sh` to point `git.burrow.net`, `burrow.net`, `auth.burrow.net`, `ts.burrow.net`, `graphs.burrow.net`, and `nsc-autoscaler.burrow.net` at the host with Cloudflare proxying disabled for ACME.
10. Use `Scripts/forge-deploy.sh --allow-dirty` for subsequent remote `nixos-rebuild` runs from the live workspace.
11. Configure Forward Email custom S3 backups for `burrow.net` and `burrow.rs` out-of-band with `Tools/forwardemail-custom-s3.sh`.
11. Rotate `secrets/infra/grafana-oidc-client-secret.age` from its pending placeholder, deploy, and let `burrow-authentik-grafana-oidc.service` reconcile the Authentik client.
12. Validate Grafana API-managed dashboards with `Scripts/grafana-tofu.sh init -backend=false` and `Scripts/grafana-tofu.sh validate`; use `.forgejo/workflows/infra-opentofu.yml` for planned dashboard apply/import.
13. Configure Forward Email custom S3 backups for `burrow.net` and `burrow.rs` out-of-band with `Tools/forwardemail-custom-s3.sh`.
14. Keep `services.burrow.openbao` and `services.burrow.jitsi` disabled until their DNS, secret, rollback, and live-service checks in BEP-0010 are complete.
## Current Constraints
- `burrow-forge` is live on NixOS in `hel1` at `89.167.47.21`.
- `services.forgejo-nsc` now expects agenix-backed runtime inputs at `/run/agenix/burrowForgejoNscToken`, `/run/agenix/burrowForgejoNscDispatcherConfig`, and `/run/agenix/burrowForgejoNscAutoscalerConfig`.
- Authentik and Headscale secrets now live in tracked agenix blobs under `secrets/infra/` and decrypt to `/run/agenix/` on the forge host.
- Authentik, Grafana, and Headscale secrets now live in tracked agenix blobs under `secrets/infra/` and decrypt to `/run/agenix/` on the forge host.
- Public Burrow forge cutover completed on March 15, 2026:
- `burrow.net`, `git.burrow.net`, and `nsc-autoscaler.burrow.net` now publish public `A` records to `89.167.47.21`
- `burrow.net`, `git.burrow.net`, and `nsc-autoscaler.burrow.net` now publish public `A` records to `89.167.47.21`; `graphs.burrow.net` should be pointed there before Grafana public cutover
- HTTP redirects to HTTPS on all three names
- `https://burrow.net` returns the root forge landing response
- `https://git.burrow.net` returns the live Forgejo front door

View file

@ -60,8 +60,13 @@ in
self.nixosModules.burrow-forge-runner
self.nixosModules.burrow-forgejo-nsc
self.nixosModules.burrow-authentik
self.nixosModules.burrow-garage
self.nixosModules.burrow-headscale
self.nixosModules.burrow-nix-cache
self.nixosModules.burrow-observability
self.nixosModules.burrow-zulip
self.nixosModules.burrow-openbao
self.nixosModules.burrow-jitsi
];
system.stateVersion = "24.11";
@ -108,6 +113,24 @@ in
group = "forgejo";
mode = "0440";
};
age.secrets.burrowGrafanaAdminPassword = {
file = ../../../secrets/infra/grafana-admin-password.age;
owner = "grafana";
group = "grafana";
mode = "0400";
};
age.secrets.burrowGrafanaSecretKey = {
file = ../../../secrets/infra/grafana-secret-key.age;
owner = "grafana";
group = "grafana";
mode = "0400";
};
age.secrets.burrowGrafanaOidcClientSecret = {
file = ../../../secrets/infra/grafana-oidc-client-secret.age;
owner = "grafana";
group = "grafana";
mode = "0400";
};
age.secrets.burrowTailscaleOidcClientSecret = {
file = ../../../secrets/infra/tailscale-oidc-client-secret.age;
owner = "root";
@ -138,6 +161,12 @@ in
group = "root";
mode = "0400";
};
age.secrets.burrowAuthentikGoogleWifClientSecret = {
file = ../../../secrets/infra/authentik-google-wif-client-secret.age;
owner = "root";
group = "root";
mode = "0400";
};
age.secrets.burrowAuthentikUiTestPassword = {
file = ../../../secrets/infra/authentik-ui-test-password.age;
owner = "root";
@ -192,8 +221,8 @@ in
};
networking.extraHosts = ''
127.0.0.1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net chat.burrow.net nsc-autoscaler.burrow.net
::1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net chat.burrow.net nsc-autoscaler.burrow.net
127.0.0.1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net chat.burrow.net graphs.burrow.net vault.burrow.net meet.burrow.net inbox.burrow.net nsc-autoscaler.burrow.net
::1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net chat.burrow.net graphs.burrow.net vault.burrow.net meet.burrow.net inbox.burrow.net nsc-autoscaler.burrow.net
'';
services.burrow.forge = {
@ -235,15 +264,20 @@ in
enable = true;
envFile = config.age.secrets.burrowAuthentikEnv.path;
forgejoClientSecretFile = config.age.secrets.burrowForgejoOidcClientSecret.path;
grafanaClientSecretFile = config.age.secrets.burrowGrafanaOidcClientSecret.path;
grafanaAccessGroupName = contributors.groups.users;
headscaleClientSecretFile = config.age.secrets.burrowHeadscaleOidcClientSecret.path;
tailscaleClientSecretFile = config.age.secrets.burrowTailscaleOidcClientSecret.path;
defaultExternalApplicationSlug = "tailscale";
googleClientIDFile = config.age.secrets.burrowAuthentikGoogleClientId.path;
googleClientSecretFile = config.age.secrets.burrowAuthentikGoogleClientSecret.path;
googleAccountMapFile = config.age.secrets.burrowAuthentikGoogleAccountMap.path;
googleWifClientSecretFile = config.age.secrets.burrowAuthentikGoogleWifClientSecret.path;
googleWifAccessGroupName = contributors.groups.automation;
googleLoginMode = "redirect";
userGroupName = contributors.groups.users;
adminGroupName = contributors.groups.admins;
extraGroupNames = [ contributors.groups.automation ];
tailscaleAccessGroupName = contributors.groups.users;
bootstrapUsers = bootstrapUsers;
linearAcsUrl = "https://api.linear.app/auth/sso/d0ca13dc-ac41-4824-8aab-e0ca352fc3de/acs";
@ -258,12 +292,75 @@ in
zulipAccessGroupName = contributors.groups.users;
};
services.grafana = {
enable = true;
settings = {
server = {
domain = "graphs.burrow.net";
root_url = "https://graphs.burrow.net/";
http_addr = "127.0.0.1";
http_port = 3001;
enforce_domain = true;
};
analytics = {
reporting_enabled = false;
check_for_updates = false;
check_for_plugin_updates = false;
};
security = {
admin_user = "admin";
admin_password = "$__file{${config.age.secrets.burrowGrafanaAdminPassword.path}}";
secret_key = "$__file{${config.age.secrets.burrowGrafanaSecretKey.path}}";
cookie_secure = true;
cookie_samesite = "strict";
disable_gravatar = true;
};
users = {
allow_sign_up = false;
auto_assign_org = true;
auto_assign_org_role = "Viewer";
};
auth = {
disable_login_form = false;
oauth_auto_login = false;
};
"auth.generic_oauth" = {
enabled = true;
name = "burrow.net";
allow_sign_up = true;
auto_login = true;
client_id = "graphs.burrow.net";
client_secret = "$__file{${config.age.secrets.burrowGrafanaOidcClientSecret.path}}";
scopes = "openid profile email groups";
auth_url = "https://auth.burrow.net/application/o/grafana/authorize/";
token_url = "https://auth.burrow.net/application/o/grafana/token/";
api_url = "https://auth.burrow.net/application/o/grafana/userinfo/";
role_attribute_path = "contains(groups[*], 'burrow-admins') && 'Admin' || 'Viewer'";
};
};
};
services.caddy.virtualHosts."graphs.burrow.net".extraConfig = ''
encode gzip zstd
reverse_proxy 127.0.0.1:3001
'';
services.burrow.headscale = {
enable = true;
oidcClientSecretFile = config.age.secrets.burrowHeadscaleOidcClientSecret.path;
bootstrapUsers = headscaleBootstrapUsers;
};
services.burrow.garage = {
enable = true;
enableApiProxy = true;
enableWebProxy = true;
};
services.burrow.nixCache.enable = true;
services.burrow.observability.enable = true;
services.burrow.zulip = {
enable = true;
administratorEmail = identities.contact.canonicalEmail;

View file

@ -10,6 +10,8 @@ let
dataVolume = "burrow-authentik-data:/data";
directorySyncScript = ../../Scripts/authentik-sync-burrow-directory.sh;
forgejoOidcSyncScript = ../../Scripts/authentik-sync-forgejo-oidc.sh;
grafanaOidcSyncScript = ../../Scripts/authentik-sync-grafana-oidc.sh;
googleWifOidcSyncScript = ../../Scripts/authentik-sync-google-wif-oidc.sh;
tailscaleOidcSyncScript = ../../Scripts/authentik-sync-tailscale-oidc.sh;
onePasswordOidcSyncScript = ../../Scripts/authentik-sync-1password-oidc.sh;
zulipSamlSyncScript = ../../Scripts/authentik-sync-zulip-saml.sh;
@ -136,6 +138,60 @@ in
description = "Authentik application slug for Forgejo.";
};
grafanaDomain = lib.mkOption {
type = lib.types.str;
default = "graphs.burrow.net";
description = "Grafana public domain used for the bundled OIDC client.";
};
grafanaProviderSlug = lib.mkOption {
type = lib.types.str;
default = "grafana";
description = "Authentik application slug for Grafana.";
};
grafanaClientId = lib.mkOption {
type = lib.types.str;
default = "graphs.burrow.net";
description = "Client ID Authentik should present to Grafana.";
};
grafanaClientSecretFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Host-local file containing the Authentik Grafana OIDC client secret.";
};
grafanaAccessGroupName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Authentik group allowed to launch Grafana.";
};
googleWifProviderSlug = lib.mkOption {
type = lib.types.str;
default = "google-cloud";
description = "Authentik application slug for Google Cloud Workload Identity Federation.";
};
googleWifClientId = lib.mkOption {
type = lib.types.str;
default = "google-wif.burrow.net";
description = "Client ID Authentik should present to Google Cloud WIF consumers.";
};
googleWifClientSecretFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Host-local file containing the Authentik Google WIF OIDC client secret.";
};
googleWifAccessGroupName = lib.mkOption {
type = lib.types.str;
default = "burrow-automation";
description = "Authentik group allowed to mint Google WIF tokens.";
};
tailscaleProviderSlug = lib.mkOption {
type = lib.types.str;
default = "tailscale";
@ -388,6 +444,12 @@ in
description = "Authentik group granted Burrow administrator access.";
};
extraGroupNames = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Additional Authentik groups that should exist even if no bootstrap user currently belongs to them.";
};
bootstrapUsers = lib.mkOption {
type = with lib.types; listOf (submodule {
options = {
@ -490,6 +552,13 @@ in
fi
''}
${lib.optionalString (cfg.googleWifClientSecretFile != null) ''
if [ ! -s ${lib.escapeShellArg cfg.googleWifClientSecretFile} ]; then
echo "Google WIF client secret missing: ${cfg.googleWifClientSecretFile}" >&2
exit 1
fi
''}
install -d -m 0750 -o root -g root ${runtimeDir} ${blueprintDir}
install -m 0644 -o root -g root ${authentikBlueprint} ${blueprintFile}
@ -517,6 +586,7 @@ AUTHENTIK_BOOTSTRAP_PASSWORD=$AUTHENTIK_BOOTSTRAP_PASSWORD
AUTHENTIK_BOOTSTRAP_TOKEN=$AUTHENTIK_BOOTSTRAP_TOKEN
AUTHENTIK_BURROW_TS_CLIENT_SECRET=$(read_secret ${lib.escapeShellArg cfg.headscaleClientSecretFile})
${lib.optionalString (cfg.forgejoClientSecretFile != null) "AUTHENTIK_BURROW_FORGEJO_CLIENT_SECRET=$(read_secret ${lib.escapeShellArg cfg.forgejoClientSecretFile})"}
${lib.optionalString (cfg.googleWifClientSecretFile != null) "AUTHENTIK_BURROW_GOOGLE_WIF_CLIENT_SECRET=$(read_secret ${lib.escapeShellArg cfg.googleWifClientSecretFile})"}
EOF
chown root:root ${envFile}
chmod 0600 ${envFile}
@ -667,7 +737,7 @@ EOF
'';
};
systemd.services.burrow-authentik-directory = lib.mkIf (cfg.bootstrapUsers != [ ]) {
systemd.services.burrow-authentik-directory = lib.mkIf (cfg.bootstrapUsers != [ ] || cfg.extraGroupNames != [ ]) {
description = "Reconcile Burrow Authentik users and groups";
after =
[
@ -706,6 +776,7 @@ EOF
export AUTHENTIK_URL=https://${cfg.domain}
export AUTHENTIK_BURROW_USERS_GROUP=${lib.escapeShellArg cfg.userGroupName}
export AUTHENTIK_BURROW_ADMINS_GROUP=${lib.escapeShellArg cfg.adminGroupName}
export AUTHENTIK_BURROW_EXTRA_GROUPS_JSON='${builtins.toJSON cfg.extraGroupNames}'
export AUTHENTIK_FORGEJO_APPLICATION_SLUG=${lib.escapeShellArg cfg.forgejoProviderSlug}
export AUTHENTIK_BURROW_DIRECTORY_JSON='${builtins.toJSON (map (user: {
inherit (user) username name email isAdmin passwordFile;
@ -810,12 +881,110 @@ EOF
export AUTHENTIK_FORGEJO_CLIENT_ID=${lib.escapeShellArg cfg.forgejoClientId}
export AUTHENTIK_FORGEJO_CLIENT_SECRET="$(tr -d '\r\n' < ${lib.escapeShellArg cfg.forgejoClientSecretFile})"
export AUTHENTIK_FORGEJO_LAUNCH_URL=https://${cfg.forgejoDomain}/
export AUTHENTIK_FORGEJO_REDIRECT_URIS_JSON='["https://${cfg.forgejoDomain}/user/oauth2/burrow.net/callback","https://${cfg.forgejoDomain}/user/oauth2/authentik/callback","https://${cfg.forgejoDomain}/user/oauth2/GitHub/callback"]'
export AUTHENTIK_FORGEJO_REDIRECT_URIS_JSON='["https://${cfg.forgejoDomain}/user/oauth2/burrow.net/callback","https://${cfg.forgejoDomain}/user/oauth2/authentik/callback"]'
${pkgs.bash}/bin/bash ${forgejoOidcSyncScript}
'';
};
systemd.services.burrow-authentik-grafana-oidc = lib.mkIf (cfg.grafanaClientSecretFile != null) {
description = "Reconcile the Burrow Authentik Grafana OIDC application";
after = [
"burrow-authentik-ready.service"
"network-online.target"
];
wants = [
"burrow-authentik-ready.service"
"network-online.target"
];
wantedBy = [ "multi-user.target" ];
restartTriggers = [
grafanaOidcSyncScript
cfg.envFile
cfg.grafanaClientSecretFile
];
path = [
pkgs.bash
pkgs.coreutils
pkgs.curl
pkgs.jq
];
serviceConfig = {
Type = "oneshot";
User = "root";
Group = "root";
};
script = ''
set -euo pipefail
set -a
source ${lib.escapeShellArg cfg.envFile}
set +a
export AUTHENTIK_URL=https://${cfg.domain}
export AUTHENTIK_GRAFANA_APPLICATION_SLUG=${lib.escapeShellArg cfg.grafanaProviderSlug}
export AUTHENTIK_GRAFANA_APPLICATION_NAME="Burrow Grafana"
export AUTHENTIK_GRAFANA_PROVIDER_NAME="Burrow Grafana"
export AUTHENTIK_GRAFANA_TEMPLATE_SLUG=${lib.escapeShellArg cfg.headscaleProviderSlug}
export AUTHENTIK_GRAFANA_CLIENT_ID=${lib.escapeShellArg cfg.grafanaClientId}
export AUTHENTIK_GRAFANA_CLIENT_SECRET="$(tr -d '\r\n' < ${lib.escapeShellArg cfg.grafanaClientSecretFile})"
export AUTHENTIK_GRAFANA_LAUNCH_URL=https://${cfg.grafanaDomain}/
export AUTHENTIK_GRAFANA_REDIRECT_URIS_JSON='["https://${cfg.grafanaDomain}/login/generic_oauth"]'
${lib.optionalString (cfg.grafanaAccessGroupName != null) ''
export AUTHENTIK_GRAFANA_ACCESS_GROUP=${lib.escapeShellArg cfg.grafanaAccessGroupName}
''}
${pkgs.bash}/bin/bash ${grafanaOidcSyncScript}
'';
};
systemd.services.burrow-authentik-google-wif-oidc = lib.mkIf (cfg.googleWifClientSecretFile != null) {
description = "Reconcile the Burrow Authentik Google WIF OIDC application";
after = [
"burrow-authentik-ready.service"
"network-online.target"
];
wants = [
"burrow-authentik-ready.service"
"network-online.target"
];
wantedBy = [ "multi-user.target" ];
restartTriggers = [
googleWifOidcSyncScript
cfg.envFile
cfg.googleWifClientSecretFile
];
path = [
pkgs.bash
pkgs.coreutils
pkgs.curl
pkgs.jq
];
serviceConfig = {
Type = "oneshot";
User = "root";
Group = "root";
};
script = ''
set -euo pipefail
set -a
source ${lib.escapeShellArg cfg.envFile}
set +a
export AUTHENTIK_URL=https://${cfg.domain}
export AUTHENTIK_GOOGLE_WIF_APPLICATION_SLUG=${lib.escapeShellArg cfg.googleWifProviderSlug}
export AUTHENTIK_GOOGLE_WIF_APPLICATION_NAME="Google Cloud WIF"
export AUTHENTIK_GOOGLE_WIF_PROVIDER_NAME="Google Cloud WIF"
export AUTHENTIK_GOOGLE_WIF_TEMPLATE_SLUG=${lib.escapeShellArg cfg.headscaleProviderSlug}
export AUTHENTIK_GOOGLE_WIF_CLIENT_ID=${lib.escapeShellArg cfg.googleWifClientId}
export AUTHENTIK_GOOGLE_WIF_CLIENT_SECRET="$(tr -d '\r\n' < ${lib.escapeShellArg cfg.googleWifClientSecretFile})"
export AUTHENTIK_GOOGLE_WIF_LAUNCH_URL=https://console.cloud.google.com/
export AUTHENTIK_GOOGLE_WIF_REDIRECT_URIS_JSON='[]'
export AUTHENTIK_GOOGLE_WIF_ACCESS_GROUP=${lib.escapeShellArg cfg.googleWifAccessGroupName}
${pkgs.bash}/bin/bash ${googleWifOidcSyncScript}
'';
};
systemd.services.burrow-authentik-tailscale-oidc = lib.mkIf (cfg.tailscaleClientSecretFile != null) {
description = "Reconcile the Burrow Authentik Tailscale OIDC application";
after = [

View file

@ -7,6 +7,7 @@ let
runnerFile = "${stateDir}/.runner";
registrationFingerprintFile = "${stateDir}/.runner-registration-fingerprint";
configFile = "${stateDir}/runner.yaml";
ageIdentityFile = "${stateDir}/age_keystore";
labelsCsv = lib.concatStringsSep "," (map (label: "${label}:host") cfg.labels);
registrationFingerprint = builtins.hashString "sha256" "${cfg.instanceUrl}\n${cfg.name}\n${labelsCsv}";
sshPrivateKeyFile = cfg.sshPrivateKeyFile or "";
@ -164,6 +165,9 @@ EOF
install -m 0600 -o ${cfg.user} -g ${cfg.group} \
${lib.escapeShellArg sshPrivateKeyFile} \
${stateDir}/.ssh/id_ed25519
install -m 0600 -o ${cfg.user} -g ${cfg.group} \
${lib.escapeShellArg sshPrivateKeyFile} \
${ageIdentityFile}
cat > ${stateDir}/.ssh/config <<EOF
Host *
IdentityFile ${stateDir}/.ssh/id_ed25519
@ -208,7 +212,13 @@ EOF
User = cfg.user;
Group = cfg.group;
WorkingDirectory = stateDir;
Environment = [ "BURROW_RUNNER_REGISTRATION_FINGERPRINT=${registrationFingerprint}" ];
Environment = [
"BURROW_RUNNER_REGISTRATION_FINGERPRINT=${registrationFingerprint}"
"BURROW_RUNNER_AGE_IDENTITY_PATH=${ageIdentityFile}"
"FORGEJO_RUNNER_HOME=${stateDir}"
"RUNNER_HOME=${stateDir}"
"HOME=${stateDir}"
];
Restart = "on-failure";
RestartSec = 2;
ExecStart = pkgs.writeShellScript "burrow-forgejo-runner" ''

View file

@ -10,6 +10,7 @@ let
forgejoAdminArgs = "--config ${lib.escapeShellArg forgejoConfigFile} --work-path ${lib.escapeShellArg forgejoWorkPath} --custom-path ${lib.escapeShellArg forgejoCustomPath}";
homeRepoPath = "/${cfg.homeOwner}/${cfg.homeRepo}";
homeRepoUrl = "https://${cfg.gitDomain}${homeRepoPath}";
sqlLiteral = value: "'${lib.replaceStrings [ "'" ] [ "''" ] value}'";
in
{
options.services.burrow.forge = {
@ -236,6 +237,7 @@ in
repository = {
DEFAULT_BRANCH = "main";
ENABLE_PUSH_CREATE_USER = false;
DISABLE_MIRRORS = true;
};
ui = {
@ -334,6 +336,84 @@ in
'';
};
systemd.services.burrow-forgejo-repository-policy = {
description = "Apply Burrow Forgejo repository authority policy";
after = [
"forgejo.service"
"postgresql.service"
];
requires = [
"forgejo.service"
"postgresql.service"
];
wantedBy = [ "multi-user.target" ];
path = [
pkgs.postgresql
];
serviceConfig = {
Type = "oneshot";
User = forgejoCfg.user;
Group = forgejoCfg.group;
WorkingDirectory = forgejoCfg.stateDir;
};
script = ''
set -euo pipefail
ready=0
for attempt in $(seq 1 60); do
if ${pkgs.postgresql}/bin/psql -h /run/postgresql -U forgejo forgejo -tAc \
"SELECT 1 FROM pg_tables WHERE schemaname='public' AND tablename='repository';" \
| grep -q 1; then
ready=1
break
fi
sleep 1
done
if [ "$ready" -ne 1 ]; then
echo "Forgejo repository table did not become ready" >&2
exit 1
fi
${pkgs.postgresql}/bin/psql -v ON_ERROR_STOP=1 \
-h /run/postgresql -U forgejo forgejo <<'SQL'
DO $$
DECLARE
repo_ids BIGINT[];
BEGIN
SELECT array_agg(r.id)
INTO repo_ids
FROM repository r
JOIN "user" u ON u.id = r.owner_id
WHERE u.lower_name = lower(${sqlLiteral cfg.homeOwner})
AND r.lower_name = lower(${sqlLiteral cfg.homeRepo});
IF repo_ids IS NULL THEN
RETURN;
END IF;
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'repository'
AND column_name = 'is_mirror'
) THEN
UPDATE repository SET is_mirror = FALSE WHERE id = ANY(repo_ids);
END IF;
IF to_regclass('public.mirror') IS NOT NULL THEN
DELETE FROM mirror WHERE repo_id = ANY(repo_ids);
END IF;
IF to_regclass('public.push_mirror') IS NOT NULL THEN
DELETE FROM push_mirror WHERE repo_id = ANY(repo_ids);
END IF;
END $$;
SQL
'';
};
systemd.services.burrow-forgejo-oidc-bootstrap = lib.mkIf (cfg.oidcClientSecretFile != null) {
description = "Seed the Burrow Forgejo OIDC login source";
after = [

View file

@ -0,0 +1,284 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.burrow.garage;
in
{
options.services.burrow.garage = {
enable = lib.mkEnableOption "Burrow Garage S3-compatible object storage";
environmentFile = lib.mkOption {
type = lib.types.path;
default = "/var/lib/burrow/garage/env";
description = "Host-local environment file containing Garage and S3 bootstrap secrets.";
};
region = lib.mkOption {
type = lib.types.str;
default = "garage";
description = "S3 region name advertised by Garage.";
};
apiListenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address used by the Garage S3 API listener.";
};
apiPort = lib.mkOption {
type = lib.types.port;
default = 3900;
description = "Port used by the Garage S3 API listener.";
};
webListenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address used by the Garage static-web listener.";
};
webPort = lib.mkOption {
type = lib.types.port;
default = 3902;
description = "Port used by the Garage static-web listener.";
};
adminListenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address used by the Garage admin listener.";
};
adminPort = lib.mkOption {
type = lib.types.port;
default = 3903;
description = "Port used by the Garage admin listener.";
};
zone = lib.mkOption {
type = lib.types.str;
default = "burrow-forge";
description = "Garage layout zone assigned to the forge host.";
};
layoutCapacity = lib.mkOption {
type = lib.types.str;
default = "100GB";
description = "Initial Garage layout capacity assigned to the forge host.";
};
s3RootDomain = lib.mkOption {
type = lib.types.str;
default = "s3.burrow.net";
description = "Wildcard root domain used for bucket-style S3 API requests.";
};
webRootDomain = lib.mkOption {
type = lib.types.str;
default = "web.burrow.net";
description = "Wildcard root domain used for bucket static website requests.";
};
apiDomain = lib.mkOption {
type = lib.types.str;
default = "objects.burrow.net";
description = "Public S3 API reverse-proxy domain.";
};
enableApiProxy = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Expose the Garage S3 API through Caddy.";
};
enableWebProxy = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Expose the Garage static-web listener through Caddy wildcard routes.";
};
atticBucket = lib.mkOption {
type = lib.types.str;
default = "attic";
description = "Garage bucket used by the Burrow Nix cache.";
};
releaseBucket = lib.mkOption {
type = lib.types.str;
default = "burrow-releases";
description = "Garage bucket used for release artifacts.";
};
packageBucket = lib.mkOption {
type = lib.types.str;
default = "burrow-packages";
description = "Garage bucket used for signed package repositories.";
};
};
config = lib.mkIf cfg.enable {
services.garage = {
enable = true;
package = pkgs.garage;
environmentFile = cfg.environmentFile;
settings = {
metadata_dir = "/var/lib/garage/meta";
data_dir = "/var/lib/garage/data";
rpc_bind_addr = "127.0.0.1:3901";
rpc_public_addr = "127.0.0.1:3901";
replication_factor = 1;
s3_api = {
s3_region = cfg.region;
api_bind_addr = "${cfg.apiListenAddress}:${toString cfg.apiPort}";
root_domain = ".${cfg.s3RootDomain}";
};
s3_web = {
bind_addr = "${cfg.webListenAddress}:${toString cfg.webPort}";
root_domain = ".${cfg.webRootDomain}";
index = "index.html";
};
admin.api_bind_addr = "${cfg.adminListenAddress}:${toString cfg.adminPort}";
};
};
systemd.services.burrow-garage-env = {
description = "Create Burrow Garage bootstrap environment";
before = [ "garage.service" ];
wantedBy = [ "garage.service" ];
requiredBy = [ "garage.service" ];
path = [
pkgs.coreutils
pkgs.gnugrep
pkgs.openssl
];
serviceConfig = {
Type = "oneshot";
User = "root";
Group = "root";
RemainAfterExit = true;
};
script = ''
set -euo pipefail
install -d -m 0700 "$(dirname ${lib.escapeShellArg cfg.environmentFile})"
touch ${lib.escapeShellArg cfg.environmentFile}
chmod 0600 ${lib.escapeShellArg cfg.environmentFile}
chown root:root ${lib.escapeShellArg cfg.environmentFile}
has_var() {
grep -q "^$1=" ${lib.escapeShellArg cfg.environmentFile}
}
add_var() {
local name="$1"
local value="$2"
if ! has_var "$name"; then
printf '%s=%s\n' "$name" "$value" >> ${lib.escapeShellArg cfg.environmentFile}
fi
}
add_var GARAGE_RPC_SECRET "$(${pkgs.openssl}/bin/openssl rand -hex 32)"
add_var GARAGE_ATTIC_ACCESS_KEY_ID "GK$(${pkgs.openssl}/bin/openssl rand -hex 12)"
add_var GARAGE_ATTIC_SECRET_ACCESS_KEY "$(${pkgs.openssl}/bin/openssl rand -hex 32)"
add_var GARAGE_RELEASE_ACCESS_KEY_ID "GK$(${pkgs.openssl}/bin/openssl rand -hex 12)"
add_var GARAGE_RELEASE_SECRET_ACCESS_KEY "$(${pkgs.openssl}/bin/openssl rand -hex 32)"
add_var GARAGE_PACKAGE_ACCESS_KEY_ID "GK$(${pkgs.openssl}/bin/openssl rand -hex 12)"
add_var GARAGE_PACKAGE_SECRET_ACCESS_KEY "$(${pkgs.openssl}/bin/openssl rand -hex 32)"
add_var GARAGE_BACKUP_ACCESS_KEY_ID "GK$(${pkgs.openssl}/bin/openssl rand -hex 12)"
add_var GARAGE_BACKUP_SECRET_ACCESS_KEY "$(${pkgs.openssl}/bin/openssl rand -hex 32)"
add_var ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64 "$(${pkgs.openssl}/bin/openssl genrsa -traditional 4096 | ${pkgs.coreutils}/bin/base64 -w0)"
. ${lib.escapeShellArg cfg.environmentFile}
add_var AWS_ACCESS_KEY_ID "$GARAGE_ATTIC_ACCESS_KEY_ID"
add_var AWS_SECRET_ACCESS_KEY "$GARAGE_ATTIC_SECRET_ACCESS_KEY"
'';
};
systemd.services.burrow-garage-bootstrap = {
description = "Bootstrap Burrow Garage layout, keys, and buckets";
after = [ "garage.service" ];
requires = [ "garage.service" ];
wantedBy = [ "multi-user.target" ];
path = [
config.services.garage.package
pkgs.coreutils
pkgs.gawk
pkgs.gnugrep
];
serviceConfig = {
Type = "oneshot";
User = "root";
Group = "root";
};
script = ''
set -euo pipefail
set -a
. ${lib.escapeShellArg cfg.environmentFile}
set +a
for _ in $(seq 1 60); do
if garage status >/dev/null 2>&1; then
break
fi
sleep 1
done
node_id="$(garage status | tail -n1 | awk '{ print $1 }')"
if [ -n "$node_id" ]; then
garage layout assign -c ${lib.escapeShellArg cfg.layoutCapacity} -z ${lib.escapeShellArg cfg.zone} "$node_id" >/dev/null 2>&1 || true
garage layout apply --version 1 >/dev/null 2>&1 || true
fi
ensure_key() {
local key_id="$1"
local secret="$2"
if ! garage key info "$key_id" >/dev/null 2>&1; then
garage key import "$key_id" "$secret" --yes >/dev/null
fi
}
ensure_bucket() {
local bucket="$1"
local key_id="$2"
if ! garage bucket info "$bucket" >/dev/null 2>&1; then
garage bucket create "$bucket" >/dev/null
fi
garage bucket allow --read --write --owner "$bucket" --key "$key_id" >/dev/null
}
ensure_bucket_reader() {
local bucket="$1"
local key_id="$2"
garage bucket allow --read "$bucket" --key "$key_id" >/dev/null
}
ensure_key "$GARAGE_ATTIC_ACCESS_KEY_ID" "$GARAGE_ATTIC_SECRET_ACCESS_KEY"
ensure_key "$GARAGE_RELEASE_ACCESS_KEY_ID" "$GARAGE_RELEASE_SECRET_ACCESS_KEY"
ensure_key "$GARAGE_PACKAGE_ACCESS_KEY_ID" "$GARAGE_PACKAGE_SECRET_ACCESS_KEY"
ensure_key "$GARAGE_BACKUP_ACCESS_KEY_ID" "$GARAGE_BACKUP_SECRET_ACCESS_KEY"
ensure_bucket ${lib.escapeShellArg cfg.atticBucket} "$GARAGE_ATTIC_ACCESS_KEY_ID"
ensure_bucket ${lib.escapeShellArg cfg.releaseBucket} "$GARAGE_RELEASE_ACCESS_KEY_ID"
ensure_bucket ${lib.escapeShellArg cfg.packageBucket} "$GARAGE_PACKAGE_ACCESS_KEY_ID"
ensure_bucket_reader ${lib.escapeShellArg cfg.atticBucket} "$GARAGE_BACKUP_ACCESS_KEY_ID"
ensure_bucket_reader ${lib.escapeShellArg cfg.releaseBucket} "$GARAGE_BACKUP_ACCESS_KEY_ID"
ensure_bucket_reader ${lib.escapeShellArg cfg.packageBucket} "$GARAGE_BACKUP_ACCESS_KEY_ID"
'';
};
services.caddy.virtualHosts = lib.mkMerge [
(lib.mkIf cfg.enableApiProxy {
"${cfg.apiDomain}".extraConfig = ''
encode gzip zstd
reverse_proxy ${cfg.apiListenAddress}:${toString cfg.apiPort}
'';
})
(lib.mkIf cfg.enableWebProxy {
"*.${cfg.webRootDomain}".extraConfig = ''
encode gzip zstd
reverse_proxy ${cfg.webListenAddress}:${toString cfg.webPort}
'';
})
];
};
}

View file

@ -26,6 +26,24 @@ in
description = "Local Headscale listen port.";
};
enableMetrics = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Expose Headscale Prometheus metrics on the local host.";
};
metricsListenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address used for the Headscale metrics listener.";
};
metricsPort = lib.mkOption {
type = lib.types.port;
default = 9098;
description = "Port used for the Headscale metrics listener.";
};
oidcIssuer = lib.mkOption {
type = lib.types.str;
default = "https://${config.services.burrow.authentik.domain}/application/o/${config.services.burrow.authentik.headscaleProviderSlug}/";
@ -154,6 +172,8 @@ in
method = "S256";
};
};
} // lib.optionalAttrs cfg.enableMetrics {
metrics_listen_addr = "${cfg.metricsListenAddress}:${toString cfg.metricsPort}";
};
};

View file

@ -0,0 +1,45 @@
{ config, lib, ... }:
let
cfg = config.services.burrow.jitsi;
in
{
options.services.burrow.jitsi = {
enable = lib.mkEnableOption "the Burrow Jitsi Meet deployment";
domain = lib.mkOption {
type = lib.types.str;
default = "meet.burrow.net";
description = "Public Jitsi Meet domain.";
};
videobridgeUdpPort = lib.mkOption {
type = lib.types.port;
default = 10000;
description = "Public UDP port used by Jitsi Videobridge media traffic.";
};
enableHttpProxy = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Expose Jitsi through the repo-owned Caddy reverse proxy.";
};
};
config = lib.mkIf cfg.enable {
services.jitsi-meet = {
enable = true;
hostName = cfg.domain;
nginx.enable = false;
};
services.jitsi-videobridge.openFirewall = true;
networking.firewall.allowedUDPPorts = [ cfg.videobridgeUdpPort ];
services.caddy.virtualHosts."${cfg.domain}".extraConfig = lib.mkIf cfg.enableHttpProxy ''
encode gzip zstd
reverse_proxy 127.0.0.1:5280
'';
};
}

View file

@ -0,0 +1,162 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.burrow.nixCache;
garageCfg = config.services.burrow.garage;
in
{
options.services.burrow.nixCache = {
enable = lib.mkEnableOption "Burrow Nix binary cache";
domain = lib.mkOption {
type = lib.types.str;
default = "nix.burrow.net";
description = "Public domain for the Burrow Nix binary cache.";
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Attic listen address.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Attic listen port.";
};
cacheName = lib.mkOption {
type = lib.types.str;
default = "burrow";
description = "Default public Attic cache name.";
};
environmentFile = lib.mkOption {
type = lib.types.path;
default = garageCfg.environmentFile;
description = "Environment file containing Attic JWT and Garage S3 credentials.";
};
adminTokenFile = lib.mkOption {
type = lib.types.path;
default = "/var/lib/burrow/nix-cache/admin-token";
description = "Host-local file containing the generated Attic admin token.";
};
ciPushTokenFile = lib.mkOption {
type = lib.types.path;
default = "/var/lib/burrow/nix-cache/ci-push-token";
description = "Host-local file containing the generated Attic CI push token.";
};
ciPushTokenValidity = lib.mkOption {
type = lib.types.str;
default = "1y";
description = "Validity duration for the generated Attic CI push token.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = garageCfg.enable;
message = "services.burrow.nixCache requires services.burrow.garage.enable = true.";
}
];
services.atticd = {
enable = true;
environmentFile = cfg.environmentFile;
settings = {
listen = "${cfg.listenAddress}:${toString cfg.port}";
allowed-hosts = [ cfg.domain ];
api-endpoint = "https://${cfg.domain}/";
require-proof-of-possession = true;
storage = {
type = "s3";
bucket = garageCfg.atticBucket;
region = garageCfg.region;
endpoint = "http://${garageCfg.apiListenAddress}:${toString garageCfg.apiPort}";
};
};
};
environment.systemPackages = [
pkgs.attic-client
];
services.caddy.virtualHosts."${cfg.domain}".extraConfig = ''
encode gzip zstd
reverse_proxy ${cfg.listenAddress}:${toString cfg.port}
'';
systemd.services.atticd = {
after = [ "burrow-garage-bootstrap.service" ];
wants = [ "burrow-garage-bootstrap.service" ];
requires = [ "burrow-garage-bootstrap.service" ];
};
systemd.services.burrow-nix-cache-bootstrap = {
description = "Bootstrap Burrow Nix binary cache";
after = [ "atticd.service" ];
requires = [ "atticd.service" ];
wantedBy = [ "multi-user.target" ];
path = [
pkgs.attic-client
pkgs.coreutils
];
serviceConfig = {
Type = "oneshot";
User = "root";
Group = "root";
};
script = ''
set -euo pipefail
install -d -m 0700 "$(dirname ${lib.escapeShellArg cfg.adminTokenFile})"
install -d -m 0700 "$(dirname ${lib.escapeShellArg cfg.ciPushTokenFile})"
for _ in $(seq 1 60); do
if (: > /dev/tcp/${cfg.listenAddress}/${toString cfg.port}) >/dev/null 2>&1; then
break
fi
sleep 1
done
if [ ! -s ${lib.escapeShellArg cfg.adminTokenFile} ]; then
umask 077
atticd-atticadm make-token \
--sub burrow-nix-cache-bootstrap \
--validity 10y \
--create-cache '*' \
--pull '*' \
--push '*' \
--delete '*' \
--configure-cache '*' \
--configure-cache-retention '*' \
> ${lib.escapeShellArg cfg.adminTokenFile}
fi
token="$(cat ${lib.escapeShellArg cfg.adminTokenFile})"
export HOME=/var/lib/burrow/nix-cache
install -d -m 0700 "$HOME"
attic login local http://${cfg.listenAddress}:${toString cfg.port} "$token" --set-default
if ! attic cache info ${lib.escapeShellArg "local:${cfg.cacheName}"} >/dev/null 2>&1; then
attic cache create --public --priority 39 ${lib.escapeShellArg "local:${cfg.cacheName}"}
fi
if [ ! -s ${lib.escapeShellArg cfg.ciPushTokenFile} ]; then
umask 077
atticd-atticadm make-token \
--sub burrow-ci-cache-publisher \
--validity ${lib.escapeShellArg cfg.ciPushTokenValidity} \
--pull ${lib.escapeShellArg cfg.cacheName} \
--push ${lib.escapeShellArg cfg.cacheName} \
> ${lib.escapeShellArg cfg.ciPushTokenFile}
fi
'';
};
};
}

View file

@ -0,0 +1,302 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.burrow.observability;
otelCollectorPackage =
if builtins.hasAttr "opentelemetry-collector-contrib" pkgs then
pkgs."opentelemetry-collector-contrib"
else
pkgs.opentelemetry-collector;
garageCfg = config.services.burrow.garage;
headscaleCfg = config.services.burrow.headscale;
grafanaHttpPort = config.services.grafana.settings.server.http_port or 3000;
grafanaHttpAddress = config.services.grafana.settings.server.http_addr or "127.0.0.1";
tailscaleExporterEnabled = cfg.tailscaleExporterEnvironmentFile != null;
in
{
options.services.burrow.observability = {
enable = lib.mkEnableOption "Burrow Prometheus, OpenTelemetry, Jaeger, and Grafana wiring";
prometheusListenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address used by the local Prometheus server.";
};
prometheusPort = lib.mkOption {
type = lib.types.port;
default = 9090;
description = "Port used by the local Prometheus server.";
};
otelGrpcEndpoint = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1:4317";
description = "Local OTLP gRPC endpoint accepted by the OpenTelemetry collector.";
};
otelHttpEndpoint = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1:4318";
description = "Local OTLP HTTP endpoint accepted by the OpenTelemetry collector.";
};
otelPrometheusEndpoint = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1:9464";
description = "Prometheus endpoint where the OpenTelemetry collector exports its own metrics.";
};
jaegerImage = lib.mkOption {
type = lib.types.str;
default = "docker.io/jaegertracing/all-in-one:1.57";
description = "OCI image used for the local Jaeger all-in-one service.";
};
jaegerUiPort = lib.mkOption {
type = lib.types.port;
default = 16686;
description = "Local Jaeger UI and query API port.";
};
jaegerOtlpGrpcPort = lib.mkOption {
type = lib.types.port;
default = 14317;
description = "Local remapped OTLP gRPC port forwarded to Jaeger.";
};
jaegerOtlpHttpPort = lib.mkOption {
type = lib.types.port;
default = 14318;
description = "Local remapped OTLP HTTP port forwarded to Jaeger.";
};
jaegerAdminPort = lib.mkOption {
type = lib.types.port;
default = 14269;
description = "Local Jaeger admin and metrics port.";
};
tailscaleExporterEnvironmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "Optional environment file for the Prometheus Tailscale exporter OAuth credentials.";
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [
{
virtualisation.podman.enable = true;
services.grafana = {
settings = {
metrics = {
enabled = true;
disable_total_stats = true;
};
};
provision = {
enable = true;
datasources.settings = {
apiVersion = 1;
prune = true;
datasources = [
{
name = "Prometheus";
uid = "prometheus";
type = "prometheus";
access = "proxy";
url = "http://${cfg.prometheusListenAddress}:${toString cfg.prometheusPort}";
isDefault = true;
jsonData = {
timeInterval = "15s";
httpMethod = "POST";
};
}
{
name = "Jaeger";
uid = "jaeger";
type = "jaeger";
access = "proxy";
url = "http://127.0.0.1:${toString cfg.jaegerUiPort}";
}
];
};
};
};
services.prometheus = {
enable = true;
listenAddress = cfg.prometheusListenAddress;
port = cfg.prometheusPort;
retentionTime = "30d";
checkConfig = "syntax-only";
exporters = {
node = {
enable = true;
listenAddress = "127.0.0.1";
};
systemd = {
enable = true;
listenAddress = "127.0.0.1";
};
};
scrapeConfigs = [
{
job_name = "prometheus";
static_configs = [
{ targets = [ "${cfg.prometheusListenAddress}:${toString cfg.prometheusPort}" ]; }
];
}
{
job_name = "node";
static_configs = [
{ targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node.port}" ]; }
];
}
{
job_name = "systemd";
static_configs = [
{ targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.systemd.port}" ]; }
];
}
{
job_name = "grafana";
metrics_path = "/metrics";
static_configs = [
{ targets = [ "${grafanaHttpAddress}:${toString grafanaHttpPort}" ]; }
];
}
{
job_name = "headscale";
metrics_path = "/metrics";
static_configs = [
{ targets = [ "${headscaleCfg.metricsListenAddress}:${toString headscaleCfg.metricsPort}" ]; }
];
}
{
job_name = "otel-collector";
static_configs = [
{ targets = [ cfg.otelPrometheusEndpoint ]; }
];
}
{
job_name = "jaeger";
metrics_path = "/metrics";
static_configs = [
{ targets = [ "127.0.0.1:${toString cfg.jaegerAdminPort}" ]; }
];
}
] ++ lib.optionals garageCfg.enable [
{
job_name = "garage";
metrics_path = "/metrics";
static_configs = [
{ targets = [ "${garageCfg.adminListenAddress}:${toString garageCfg.adminPort}" ]; }
];
}
] ++ lib.optionals tailscaleExporterEnabled [
{
job_name = "tailscale";
static_configs = [
{ targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.tailscale.port}" ]; }
];
}
];
};
services.opentelemetry-collector = {
enable = true;
package = otelCollectorPackage;
settings = {
receivers = {
otlp = {
protocols = {
grpc.endpoint = cfg.otelGrpcEndpoint;
http.endpoint = cfg.otelHttpEndpoint;
};
};
};
processors = {
resource.attributes = [
{
key = "service.namespace";
value = "burrow";
action = "upsert";
}
{
key = "deployment.environment";
value = config.networking.hostName;
action = "upsert";
}
];
batch = { };
};
exporters = {
"otlp/jaeger" = {
endpoint = "127.0.0.1:${toString cfg.jaegerOtlpGrpcPort}";
tls.insecure = true;
};
prometheus.endpoint = cfg.otelPrometheusEndpoint;
};
service.pipelines = {
traces = {
receivers = [ "otlp" ];
processors = [
"resource"
"batch"
];
exporters = [ "otlp/jaeger" ];
};
metrics = {
receivers = [ "otlp" ];
processors = [
"resource"
"batch"
];
exporters = [ "prometheus" ];
};
};
};
};
virtualisation.oci-containers.containers.burrow-jaeger = {
image = cfg.jaegerImage;
autoStart = true;
environment = {
COLLECTOR_OTLP_ENABLED = "true";
SPAN_STORAGE_TYPE = "badger";
BADGER_EPHEMERAL = "false";
BADGER_DIRECTORY_VALUE = "/badger/data";
BADGER_DIRECTORY_KEY = "/badger/key";
};
volumes = [
"burrow-jaeger-badger:/badger"
];
ports = [
"127.0.0.1:${toString cfg.jaegerUiPort}:16686"
"127.0.0.1:${toString cfg.jaegerOtlpGrpcPort}:4317"
"127.0.0.1:${toString cfg.jaegerOtlpHttpPort}:4318"
"127.0.0.1:${toString cfg.jaegerAdminPort}:14269"
];
extraOptions = [ "--pull=always" ];
};
systemd.services.opentelemetry-collector.after = [ "podman-burrow-jaeger.service" ];
systemd.services.opentelemetry-collector.wants = [ "podman-burrow-jaeger.service" ];
environment.sessionVariables = {
OTEL_EXPORTER_OTLP_ENDPOINT = "http://${cfg.otelGrpcEndpoint}";
OTEL_EXPORTER_OTLP_PROTOCOL = "grpc";
};
}
(lib.mkIf tailscaleExporterEnabled {
services.prometheus.exporters.tailscale = {
enable = true;
listenAddress = "127.0.0.1";
environmentFile = cfg.tailscaleExporterEnvironmentFile;
};
})
]);
}

View file

@ -0,0 +1,101 @@
{ config, lib, ... }:
let
cfg = config.services.burrow.openbao;
in
{
options.services.burrow.openbao = {
enable = lib.mkEnableOption "the Burrow OpenBao deployment";
domain = lib.mkOption {
type = lib.types.str;
default = "vault.burrow.net";
description = "Public OpenBao domain.";
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Local OpenBao listener address.";
};
port = lib.mkOption {
type = lib.types.port;
default = 8200;
description = "Local OpenBao API port.";
};
clusterPort = lib.mkOption {
type = lib.types.port;
default = 8201;
description = "Local OpenBao cluster port.";
};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/burrow/openbao";
description = "Persistent OpenBao state directory.";
};
googleKmsSeal = {
enable = lib.mkEnableOption "Google Cloud KMS seal wrapping";
project = lib.mkOption {
type = lib.types.str;
default = "project-88c23ce9-918a-470a-b33";
description = "Google Cloud project containing the seal key.";
};
location = lib.mkOption {
type = lib.types.str;
default = "global";
description = "Google Cloud KMS key ring location.";
};
keyRing = lib.mkOption {
type = lib.types.str;
default = "burrow-identity";
description = "Google Cloud KMS key ring name.";
};
cryptoKey = lib.mkOption {
type = lib.types.str;
default = "openbao-seal";
description = "Google Cloud KMS crypto key name used for seal wrapping.";
};
};
};
config = lib.mkIf cfg.enable {
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0700 openbao openbao - -"
"d ${cfg.dataDir}/data 0700 openbao openbao - -"
];
services.openbao = {
enable = true;
settings = {
ui = true;
api_addr = "https://${cfg.domain}";
cluster_addr = "http://${cfg.listenAddress}:${toString cfg.clusterPort}";
storage.file.path = "${cfg.dataDir}/data";
listener.tcp = {
address = "${cfg.listenAddress}:${toString cfg.port}";
tls_disable = true;
};
} // lib.optionalAttrs cfg.googleKmsSeal.enable {
seal.gcpckms = {
project = cfg.googleKmsSeal.project;
region = cfg.googleKmsSeal.location;
key_ring = cfg.googleKmsSeal.keyRing;
crypto_key = cfg.googleKmsSeal.cryptoKey;
};
};
};
services.caddy.virtualHosts."${cfg.domain}".extraConfig = ''
encode gzip zstd
reverse_proxy ${cfg.listenAddress}:${toString cfg.port}
'';
};
}