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
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:
parent
97c569fb35
commit
002bd382e9
199 changed files with 14268 additions and 185 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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" ''
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
284
nixos/modules/burrow-garage.nix
Normal file
284
nixos/modules/burrow-garage.nix
Normal 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}
|
||||
'';
|
||||
})
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
@ -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}";
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
45
nixos/modules/burrow-jitsi.nix
Normal file
45
nixos/modules/burrow-jitsi.nix
Normal 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
|
||||
'';
|
||||
};
|
||||
}
|
||||
162
nixos/modules/burrow-nix-cache.nix
Normal file
162
nixos/modules/burrow-nix-cache.nix
Normal 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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
302
nixos/modules/burrow-observability.nix
Normal file
302
nixos/modules/burrow-observability.nix
Normal 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;
|
||||
};
|
||||
})
|
||||
]);
|
||||
}
|
||||
101
nixos/modules/burrow-openbao.nix
Normal file
101
nixos/modules/burrow-openbao.nix
Normal 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}
|
||||
'';
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue