let # generate one using openssl (somehow) # XXX: when importing, garage tells you importing is only meant for keys previously generated by garage. is it okay to generate them using openssl? it seems to work fine snakeoil_key = { id = "GK22a15201acacbd51cd43e327"; secret = "82b2b4cbef27bf8917b350d5b10a87c92fa9c8b13a415aeeea49726cf335d74e"; }; in # TODO: expand to a multi-machine setup { config, lib, pkgs, ... }: { # add in options to ensure creation of buckets and keys options = let inherit (lib) types mkOption mkEnableOption; in { services.garage = { ensureBuckets = mkOption { type = types.attrsOf (types.submodule { options = { website = mkOption { type = types.bool; default = false; }; # I think setting corsRules should allow another website to show images from your bucket corsRules = { enable = mkEnableOption "CORS Rules"; allowedHeaders = mkOption { type = types.listOf types.str; default = []; }; allowedMethods = mkOption { type = types.listOf types.str; default = []; }; allowedOrigins = mkOption { type = types.listOf types.str; default = []; }; }; aliases = mkOption { type = types.listOf types.str; default = []; }; }; }); }; ensureKeys = mkOption { type = types.attrsOf (types.submodule { # TODO: these should be managed as secrets, not in the nix store options = { id = mkOption { type = types.str; }; secret = mkOption { type = types.str; }; # TODO: assert at least one of these is true # currently, needs to be done in the top level module ensureAccess = mkOption { type = types.attrsOf (types.submodule { options = { read = mkOption { type = types.bool; default = false; }; write = mkOption { type = types.bool; default = false; }; owner = mkOption { type = types.bool; default = false; }; }; }); default = []; }; }; }); }; }; }; config = { virtualisation.vmVariant = { virtualisation.diskSize = 2048; virtualisation.forwardPorts = [ { from = "host"; host.port = 3901; guest.port = 3901; } { from = "host"; host.port = 3902; guest.port = 3902; } ]; }; environment.systemPackages = [ pkgs.minio-client pkgs.awscli ]; networking.firewall.allowedTCPPorts = [ 3901 3902 ]; services.garage = { enable = true; package = pkgs.garage_0_9; settings = { replication_mode = "none"; # TODO: use a secret file # I'd like to have a NixOS module that declares the need for a secret file # that way, the need can be met by any secrets solution (agenix, sops-nix, colmena, a nixops4 module, ...) rpc_secret = "d576c4478cc7d0d94cfc127138cbb82018b0155c037d1c827dfb6c36be5f6625"; # TODO: why does this have to be set? is there not a sensible default? rpc_bind_addr = "[::]:3901"; rpc_public_addr = "[::1]:3901"; s3_api.api_bind_addr = "[::]:3900"; s3_web.bind_addr = "[::]:3902"; s3_web.root_domain = ".web.garage.localhost"; index = "index.html"; s3_api.s3_region = "garage"; s3_api.root_domain = ".s3.garage.localhost"; }; }; systemd.services.ensure-garage = { after = [ "garage.service" ]; wantedBy = [ "garage.service" ]; path = [ config.services.garage.package pkgs.perl pkgs.awscli ]; script = '' set -xeuo pipefail # give garage time to start up sleep 3 # XXX: this is very sensitive to being a single instance # (doing the bare minimum to get garage up and running) # also, it's crazy that we have to parse command output like this # TODO: talk to garage maintainer about making this nicer to work with in Nix # before I do that though, I should figure out how setting it up across multiple machines will work # You could ask for a change or `--json` flag anyway, and maybe tell them what you're working on. GARAGE_ID=$(garage node id 2>/dev/null | perl -ne '/(.*)@.*/ && print $1') garage layout assign -z g1 -c 1G $GARAGE_ID LAYOUT_VER=$(garage layout show | perl -ne '/Current cluster layout version: (\d*)/ && print $1') garage layout apply --version $((LAYOUT_VER + 1)) # XXX: this is a hack because we want to write to the buckets here but we're not guaranteed any access keys # TODO: generate this key here rather than using a well-known key garage key import --yes -n tmp ${snakeoil_key.id} ${snakeoil_key.secret} export AWS_ACCESS_KEY_ID=${snakeoil_key.id}; export AWS_SECRET_ACCESS_KEY=${snakeoil_key.secret}; ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (bucket: { website, aliases, corsRules }: '' # garage bucket info tells us if the bucket already exists garage bucket info ${bucket} || garage bucket create ${bucket} # TODO: should this --deny the website if `website` is false? ${lib.optionalString website '' garage bucket website --allow ${/* more robust: */ lib.strings.escapeShellArg bucket} ''} ${lib.concatStringsSep "\n" (map (alias: '' garage bucket alias ${bucket} ${alias} '') aliases)} ${lib.optionalString corsRules.enable '' # TODO: can i turn this whole thing into one builtins.toJSON? # why not :D # we also have `lib.strings.escapeShellArg` for the quoting export CORS=${lib.concatStrings [ "'" ''{"CORSRules":[{'' ''"AllowedHeaders":${builtins.toJSON corsRules.allowedHeaders},'' ''"AllowedMethods":${builtins.toJSON corsRules.allowedMethods},'' ''"AllowedOrigins":${builtins.toJSON corsRules.allowedOrigins}'' ''}]}'' "'" ]} garage bucket allow --read --write --owner ${bucket} --key tmp aws --endpoint http://s3.garage.localhost:3900 s3api put-bucket-cors --bucket ${bucket} --cors-configuration $CORS garage bucket deny --read --write --owner ${bucket} --key tmp ''} '') config.services.garage.ensureBuckets) # probably nice to factor this out into a function } ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (key: {id, secret, ensureAccess}: '' garage key import --yes -n ${key} ${id} ${secret} ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (bucket: { read, write, owner }: '' garage bucket allow ${lib.optionalString read "--read"} ${lib.optionalString write "--write"} ${lib.optionalString owner "--owner"} ${bucket} --key ${key} '') ensureAccess) } '') config.services.garage.ensureKeys) } garage key delete ${snakeoil_key.id} --yes ''; }; }; }