{ 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} ''; }) ]; }; }