diff --git a/.gitignore b/.gitignore index 9b42106..e532ccd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .direnv/ +.terraform* +tf/ diff --git a/README.md b/README.md index 8ffff02..175ea1b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,27 @@ # nixlab The Nix part of my homelab, just messing around + +## boostrap +TODO: Figure out if addNetworking is required still +- Set `addNetworking` to `false` in `config/tf.nix` +- `just deploy` +- Wait for the infra to build +- Start the containers and log in to them +- Redeploy infra with `addNetworking` +- Put the `host-key.pub` into `~/.ssh/authorized_keys` on each host +- (This wil fail) `just deploy` +- Fetch the SSH keys of the hosts and put them in `secrets/secrets.nix` +- Remember to add it to `systems`! +- Rekey the secrets with `just rekey` +- `just deploy` +- Wait for the deploy +- Set the IPs properly in `flake.nix` (using the TF config as a ref) +- Reboot the nodes + +## ref +- https://nixos.wiki/wiki/Proxmox_Linux_Container +- https://github.com/ryantm/agenix +- https://registry.terraform.io/providers/Telmate/proxmox/latest/docs/resources/lxc +- https://terranix.org/documentation/flakes.html +- https://discourse.nixos.org/t/qbittorrent-headless-service-module/32397 diff --git a/config/nix01.nix b/config/nix01.nix new file mode 100644 index 0000000..3291f85 --- /dev/null +++ b/config/nix01.nix @@ -0,0 +1,140 @@ +{ modulesPath, pkgs, config, lib, ... }: + +{ + imports = [ + # Include the default lxd configuration. + "${modulesPath}/virtualisation/proxmox-lxc.nix" + # Include the container-specific autogenerated configuration. + ./lxd.nix + ./services/sharkey.nix + ]; + + networking = { + dhcpcd.enable = false; + useDHCP = false; + useHostResolvConf = false; + firewall.enable = false; + nameservers = ["192.168.1.155" "1.1.1.1"]; + }; + + environment.systemPackages = with pkgs; [ + git + curl + vim + ]; + + services.postgresql = { + enable = true; + enableTCPIP = true; + ensureDatabases = [ "authentik" "blog" "forgejo" "infisical" "sharkey" ]; + ensureUsers = [ + { + name = "authentik"; + ensureDBOwnership = true; + } + { + name = "blog"; + ensureDBOwnership = true; + } + { + name = "forgejo"; + ensureDBOwnership = true; + } + { + name = "infisical"; + ensureDBOwnership = true; + } + { + name = "sharkey"; + ensureDBOwnership = true; + } + ]; + + authentication = pkgs.lib.mkOverride 10 '' + # type database DBuser auth-method + local all all trust + # ipv4 + host all all 127.0.0.1/32 trust + # ipv6 + host all all ::1/128 trust + # LAN + host all all 192.168.0.0/16 trust + ''; + }; + + services.calibre-server = { + enable = true; + auth = { + enable = true; + userDb = "/var/lib/calibre-server/.config/calibre/server-users.sqlite"; + }; + libraries = [ + "/var/lib/calibre-server" + ]; + }; + + services.pgadmin = { + enable = true; + initialEmail = "hello@amyerskine.me"; + initialPasswordFile = config.age.secrets."pgadmin.password".path; + }; + + services.nginx.enable = true; + services.nginx.virtualHosts."pg.nix01.cluster" = { + locations."/" = { + proxyPass = "http://127.0.0.1:5050"; + proxyWebsockets = true; + }; + }; + + services.nginx.virtualHosts."sharkey.nix01.cluster" = { + locations."/" = { + proxyPass = "http://127.0.0.1:3001"; + proxyWebsockets = true; + }; + }; + + services.nginx.virtualHosts."calibre.nix01.cluster" = { + locations."/" = { + proxyPass = "http://127.0.0.1:8080"; + proxyWebsockets = true; + }; + }; + + services.sharkey = { + enable = true; + domain = "fedi.amy.mov"; + package = (pkgs.callPackage ./services/sharkey-pkg.nix {}); + + database = { + passwordFile = config.age.secrets."sharkey.dbpass".path; + }; + + redis = { + passwordFile = config.age.secrets."sharkey.redispass".path; + }; + + meilisearch = { + createLocally = false; + }; + + settings = { + id = "aidx"; + port = 3001; + + maxNoteLength = 8192; + maxFileSize = 1024 * 1024 * 1024; + proxyRemoteFiles = true; + + # at the suggestion of Sharkey maintainers, + # this allows the server to run multiple workers + # and without this (and postgres tuning), the instance runs slowly + clusterLimit = 3; + + signToActivityPubGet = true; + CheckActivityPubGetSigned = false; + }; + }; + + system.stateVersion = "24.11"; # Did you read the comment? +} diff --git a/config/nix02.nix b/config/nix02.nix new file mode 100644 index 0000000..cb033d1 --- /dev/null +++ b/config/nix02.nix @@ -0,0 +1,90 @@ +{ modulesPath, pkgs, unstable, config, ... }: + +{ + imports = [ + # Include the default lxd configuration. + "${modulesPath}/virtualisation/proxmox-lxc.nix" + # Include the container-specific autogenerated configuration. + ./lxd.nix + ]; + + networking = { + dhcpcd.enable = false; + useDHCP = false; + useHostResolvConf = false; + firewall.enable = false; + nameservers = ["192.168.1.155" "1.1.1.1"]; + }; + + environment.systemPackages = with pkgs; [ + git + curl + vim + ]; + + services.nginx = { + enable = true; + }; + + services.nginx.virtualHosts."forgejo.nix02.cluster" = { + locations."/" = { + proxyPass = "http://127.0.0.1:8312"; + proxyWebsockets = true; + }; + }; + + services.nginx.virtualHosts."forge.amy.mov" = { + locations."/" = { + proxyPass = "http://127.0.0.1:8312"; + proxyWebsockets = true; + }; + }; + + services.forgejo = { + enable = true; + package = unstable.forgejo; + settings = { + server = { + HTTP_PORT = 8312; + ROOT_URL = "https://forge.amy.mov"; + }; + }; + + database = { + createDatabase = false; + + type = "postgres"; + host = "nix01.cluster"; + name = "forgejo"; + user = "forgejo"; + passwordFile = config.age.secrets."forgejo.dbpass".path; + }; + }; + + services.authentik = { + enable = true; + environmentFile = config.age.secrets."authentik.env".path; + + nginx = { + enable = true; + enableACME = false; + host = "auth.nix02.cluster"; + }; + + createDatabase = false; + + settings = { + postgresql = { + host = "nix01.cluster"; + user = "authentik"; + password = "authentik"; + name = "authentik"; + }; + + disable_startup_analytics = true; + avatars = "initials"; + }; + }; + + system.stateVersion = "24.11"; # Did you read the comment? +} diff --git a/config/nixos.nix b/config/nixos.nix index 25997a4..dbf3562 100644 --- a/config/nixos.nix +++ b/config/nixos.nix @@ -7,6 +7,8 @@ # Include the container-specific autogenerated configuration. ./lxd.nix ./services/opengist.nix + ./services/kener.nix + #./services/upsnap.nix ]; networking = { @@ -22,6 +24,36 @@ vim ]; + # services.upsnap = { + # enable = true; + # }; + + services.kener = { + enable = true; + }; + + # Would like to use my PG DB for this but the service just doesn't + # support DBs that are hosted outside of the Nix box + services.writefreely = { + enable = true; + host = "write.amy.mov"; + + database = { + type = "sqlite3"; + }; + + admin = { + name = "amy"; + }; + + settings = { + server = { + bind = "0.0.0.0"; + port = 8123; + }; + }; + }; + services.opengist = { enable = true; config = ./opengist.yml; diff --git a/config/services/kener-pkg.nix b/config/services/kener-pkg.nix new file mode 100644 index 0000000..58c1ec8 --- /dev/null +++ b/config/services/kener-pkg.nix @@ -0,0 +1,45 @@ +{ lib, buildNpmPackage, fetchFromGitHub }: + +let + pname = "kener"; + version = "3.2.12"; +in +buildNpmPackage rec { + inherit pname version; + + src = fetchFromGitHub { + owner = "rajnandan1"; + repo = pname; + rev = "e6b5600a4726f719c2228d7d2da5a919e4bc15a3"; + hash = "sha256-UBmt7SYZ2WukJvT1TOcwVr/L8RZVpVkLamCNV/xC8L4="; + }; + + npmDepsHash = "sha256-csB6qMJt3wBQyyWrK31F0FaRck3rt0JiH/lv77f+570="; + + npmBuild = "npm run build"; + # Copy src because the main runner (hosts the API etc) calls some stuff from it + installPhase = '' + mkdir $out + + cp -R src/ $out + cp -R node_modules/ $out + cp -R build/ $out + cp -R migrations/ $out + cp -R seeds/ $out + cp -R static/ $out + + cp -R knexfile.js $out + sed -i "s@./migrations@$out/migrations@g" $out/knexfile.js + sed -i "s@./seeds@$out/seeds@g" $out/knexfile.js + + cp main.js $out + ''; + + meta = with lib; { + description = "Stunning status pages, batteries included!"; + homepage = "https://kener.ing"; + license = licenses.mit; + maintainers = with maintainers; [ nullishamy ]; + mainProgram = ""; + }; +} diff --git a/config/services/kener.nix b/config/services/kener.nix new file mode 100644 index 0000000..04ff0d0 --- /dev/null +++ b/config/services/kener.nix @@ -0,0 +1,44 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.kener; + src = builtins.fetchTarball { + url = "https://github.com/rajnandan1/kener/archive/refs/tags/3.2.12.tar.gz"; + sha256 = "sha256:0a301jz8vqi2bd93k4lyabinshvadz084jfnkzrmxqrfr7w9gqbl"; + }; +in { + options = { + services.kener = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable Kener. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.kener = + let + kener = (pkgs.callPackage ./kener-pkg.nix {}); + in { + path = [ + pkgs.nodejs + pkgs.unixtools.ping + ]; + environment = { + "DATABASE_URL" = "sqlite:///opt/kener/kener.db"; + "ORIGIN" = "https://amy.mov"; + "KENER_SECRET_KEY" = "my-super-strong-key"; + }; + name = "kener.service"; + enable = true; + script = "node ${kener}/main.js"; + description = "Kener"; + }; + }; +} diff --git a/config/services/sharkey-pkg.nix b/config/services/sharkey-pkg.nix new file mode 100644 index 0000000..0b580d3 --- /dev/null +++ b/config/services/sharkey-pkg.nix @@ -0,0 +1,141 @@ +{ + lib, + stdenv, + fetchFromGitLab, + bash, + makeWrapper, + jemalloc, + ffmpeg-headless, + python3, + pkg-config, + glib, + vips, + pnpm_9, + nodejs, + pixman, + pango, + cairo, +}: +stdenv.mkDerivation (finalAttrs: { + pname = "sharkey"; + version = "2025.4.2"; + + src = fetchFromGitLab { + domain = "activitypub.software"; + owner = "TransFem-org"; + repo = "Sharkey"; + rev = finalAttrs.version; + fetchSubmodules = true; + hash = "sha256-gCZY9d/YLNQRGVFqsK7//UDiS19Jtqa7adGliIdE+4c="; + }; + + pnpmDeps = pnpm_9.fetchDeps { + inherit (finalAttrs) src pname; + hash = "sha256-2bt/sHKGNIjKfOvZ6DCXvdJcKoOJX/ueWdLULlYK3YU="; + }; + + nativeBuildInputs = [ + pnpm_9.configHook + nodejs + makeWrapper + python3 + pkg-config + ]; + + buildInputs = [ + glib + vips + + pixman + pango + cairo + ]; + + # This environment variable is required for `node-gyp`, which is used by some native dependencies we build below. + # Without it, `node-gyp` won't know where the source code for node.js is, and will fail to download it instead. + npm_config_nodedir = nodejs; + + # Sharkey depends on some packages with native code that needs to be built. + # These aren't built by default, so we need to run their build scripts manually. + # + # The tricky thing is that not all of them required for Sharkey to "successfully" build. + # They will trick you, make you think that Sharkey works, and successfully run your databse migrations. + # And then, when your instance tries to run, it will crash with an error like: + # + # Error [ERR_INTERNAL_ASSERTION]: This is caused by either a bug in Node.js or incorrect usage of Node.js internals. + # Please open an issue with this stack trace at https://github.com/nodejs/node/issues + # + # If you see that error, IT IS LYING TO YOU. It means Sharkey added a new dependency that required native code to be built. + # Figure out what is the new dependency. You can ask in their discord, and they'll probably tell you. + # And then, build it in the `buildPhase` below. + + buildPhase = '' + runHook preBuild + + ( + cd node_modules/.pnpm/node_modules/v-code-diff + pnpm run postinstall + ) + ( + cd node_modules/.pnpm/node_modules/re2 + pnpm run rebuild + ) + ( + cd node_modules/.pnpm/node_modules/sharp + pnpm run install + ) + ( + cd node_modules/.pnpm/node_modules/canvas + pnpm run install + ) + + pnpm build + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out/Sharkey + + ln -s /var/lib/sharkey $out/Sharkey/files + ln -s /run/sharkey $out/Sharkey/.config + cp -r * $out/Sharkey + + makeWrapper ${lib.getExe pnpm_9} $out/bin/sharkey \ + --chdir $out/Sharkey \ + --prefix PATH : ${ + lib.makeBinPath [ + bash + pnpm_9 + nodejs + ] + } \ + --prefix LD_LIBRARY_PATH : ${ + lib.makeLibraryPath [ + jemalloc + ffmpeg-headless + stdenv.cc.cc.lib + ] + } + + runHook postInstall + ''; + + passthru = { + inherit (finalAttrs) pnpmDeps; + }; + + meta = { + description = "🌎 A Sharkish microblogging platform 🚀"; + homepage = "https://joinsharkey.org"; + license = lib.licenses.gpl3Only; + maintainers = with lib.maintainers; [ sodiboo ]; + platforms = [ + "x86_64-linux" + "aarch64-linux" + ]; + mainProgram = "sharkey"; + }; +}) diff --git a/config/services/sharkey.nix b/config/services/sharkey.nix new file mode 100644 index 0000000..af3bec0 --- /dev/null +++ b/config/services/sharkey.nix @@ -0,0 +1,246 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.sharkey; + + createDB = cfg.database.host == "127.0.0.1" && cfg.database.createLocally; + createRedis = cfg.redis.host == "127.0.0.1" && cfg.redis.createLocally; + createMeili = cfg.meilisearch.host == "127.0.0.1" && cfg.meilisearch.createLocally; + + createMeiliKey = cfg.meilisearch.key == lib.fakeSha256; + + settingsFormat = pkgs.formats.yaml { }; + configFile = settingsFormat.generate "sharkey-config.yml" cfg.settings; +in +{ + options = { + services.sharkey = with lib; { + enable = mkEnableOption "sharkey"; + + domain = mkOption { + type = lib.types.str; + example = "fedi.amy.mov"; + }; + + package = lib.mkOption { + type = lib.types.package; + defaultText = lib.literalExpression "pkgs.sharkey"; + description = "Sharkey package to use."; + }; + + database = { + createLocally = mkOption { + type = lib.types.bool; + default = true; + }; + + host = mkOption { + type = lib.types.str; + default = "127.0.0.1"; + }; + + port = mkOption { + type = lib.types.port; + default = 5432; + }; + + name = mkOption { + type = lib.types.str; + default = "sharkey"; + }; + + passwordFile = mkOption { + description = '' + Path to a file containing the password for the database user. + + This file must be readable by the `sharkey` user. + + If creating a database locally, it must also be readable by the `postgres` user. + ''; + type = lib.types.path; + example = "/run/secrets/sharkey-db-password"; + }; + }; + + redis = { + createLocally = mkOption { + type = lib.types.bool; + default = true; + }; + + host = mkOption { + type = lib.types.str; + default = "127.0.0.1"; + }; + + port = mkOption { + type = lib.types.port; + default = 6379; + }; + + passwordFile = mkOption { + description = '' + Path to a file containing the password for the redis server. + + This file must be readable by the `sharkey` user. + ''; + type = lib.types.path; + example = "/run/secrets/sharkey-redis-password"; + }; + }; + + meilisearch = { + createLocally = mkOption { + type = lib.types.bool; + default = true; + }; + + host = mkOption { + type = lib.types.str; + default = "127.0.0.1"; + }; + + port = mkOption { + type = lib.types.port; + default = 7700; + }; + + index = mkOption { + type = lib.types.str; + default = replaceStrings [ "." ] [ "_" ] cfg.domain; + }; + + key = mkOption { + type = lib.types.str; + default = "$MEILI_MASTER_KEY"; + }; + }; + + settings = mkOption { + type = settingsFormat.type; + default = { }; + description = '' + Configuration for Sharkey, see + + for supported settings. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + documentation.enable = false; + + assertions = [ + { + assertion = createMeiliKey -> createMeili; + message = "services.sharkey.meilisearch.key is required to be set when connecting to a remote meilisearch instance"; + } + ]; + + services.sharkey.settings = { + url = "https://${cfg.domain}/"; + db.host = cfg.database.host; + db.port = cfg.database.port; + db.db = cfg.database.name; + db.user = cfg.database.name; + db.pass = "$SHARKEY_DB_PASSWORD"; + redis.host = cfg.redis.host; + redis.port = cfg.redis.port; + redis.pass = "$SHARKEY_REDIS_PASSWORD"; + meilisearch.host = cfg.meilisearch.host; + meilisearch.port = cfg.meilisearch.port; + meilisearch.apiKey = cfg.meilisearch.key; + meilisearch.index = cfg.meilisearch.index; + meilisearch.ssl = !createMeili; + meilisearch.scope = "global"; + }; + + environment.etc."sharkey.yml".source = configFile; + + systemd.services.sharkey = { + after = + [ "network-online.target" ] + ++ lib.optionals createDB [ "postgresql.service" ] + ++ lib.optionals createRedis [ "redis-sharkey.service" ] + ++ lib.optionals createMeili [ "meilisearch.service" ]; + wantedBy = [ "multi-user.target" ]; + + preStart = '' + SHARKEY_DB_PASSWORD="$(cat ${lib.escapeShellArg cfg.database.passwordFile})" \ + SHARKEY_REDIS_PASSWORD="$(cat ${lib.escapeShellArg cfg.redis.passwordFile})" \ + ${pkgs.envsubst}/bin/envsubst -i "${configFile}" > $MISSKEY_CONFIG_YML + ''; + + environment.MISSKEY_CONFIG_YML = "/run/sharkey/config.yml"; + environment.NODE_ENV = "production"; + + serviceConfig = { + EnvironmentFile = lib.mkIf ( + config.services.meilisearch.masterKeyEnvironmentFile != null + ) config.services.meilisearch.masterKeyEnvironmentFile; + Type = "simple"; + User = "sharkey"; + + StateDirectory = "sharkey"; + StateDirectoryMode = "0700"; + RuntimeDirectory = "sharkey"; + RuntimeDirectoryMode = "0700"; + ExecStart = "${cfg.package}/bin/sharkey migrateandstart"; + TimeoutSec = 60; + Restart = "always"; + + StandardOutput = "journal"; + StandardError = "journal"; + SyslogIdentifier = "sharkey"; + }; + }; + + services.postgresql = lib.mkIf createDB { + enable = true; + settings.port = cfg.database.port; + ensureUsers = [ + { + name = cfg.database.name; + ensureDBOwnership = true; + } + ]; + ensureDatabases = [ cfg.database.name ]; + }; + + services.redis = lib.mkIf createRedis { + servers.sharkey = { + enable = true; + user = "sharkey"; + bind = "127.0.0.1"; + port = cfg.redis.port; + requirePassFile = cfg.redis.passwordFile; + }; + }; + + systemd.services.postgresql.postStart = lib.mkIf createDB '' + $PSQL -tAc "ALTER ROLE ${cfg.database.name} WITH ENCRYPTED PASSWORD '$(printf "%s" $(cat ${cfg.database.passwordFile} | tr -d "\n"))';" + ''; + + services.meilisearch = lib.mkIf createMeili { + enable = true; + listenAddress = "127.0.0.1"; + listenPort = cfg.meilisearch.port; + environment = "production"; + }; + + users.users.sharkey = { + group = "sharkey"; + isSystemUser = true; + home = "/run/sharkey"; + packages = [ cfg.package ]; + }; + + users.groups.sharkey = { }; + }; + meta.maintainers = with lib.maintainers; [ sodiboo ]; +} diff --git a/config/services/upsnap.nix b/config/services/upsnap.nix new file mode 100644 index 0000000..95ac0fe --- /dev/null +++ b/config/services/upsnap.nix @@ -0,0 +1,43 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.upsnap; + src = pkgs.fetchzip { + url = "https://github.com/seriousm4x/UpSnap/releases/download/5.0.4/UpSnap_5.0.4_linux_amd64.zip"; + sha256 = "sha256:1qlav9if6f2c50rzakyilxgzmq2c5bzcs6lx1w7sffxhl440nxhs"; + }; +in { + options = { + services.upsnap = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable UpSnap. + ''; + }; + + bind = mkOption { + type = types.str; + default = "0.0.0.0:8090"; + description = '' + The bind address/port + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.upsnap = { + environment = { + "HOME" = "/opt/upsnap"; + }; + name = "upsnap.service"; + enable = true; + script = "${src} serve --http ${cfg.bind}"; + description = "UpSnap"; + }; + }; +} diff --git a/config/tf.nix b/config/tf.nix new file mode 100644 index 0000000..207c24b --- /dev/null +++ b/config/tf.nix @@ -0,0 +1,94 @@ +{ lib, ... }: +let + # Set to false to boostrap the containers + addNetworking = true; + + pmHost = "https://192.168.1.100"; + + creds = { + ctPassword = "password"; + }; + + sshKeys = '' + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDTbclOyOwIAPgVE/v5lIuf0P+Tq/Qkw3+GFa4YuRaCC amy@nixon + ''; + + templates = { + nixos = "nas-main:vztmpl/nixos-system-x86_64-linux.tar.xz"; + }; +in { + terraform = { + required_providers = { + proxmox = { + source = "telmate/proxmox"; + version = "3.0.2-rc01"; + }; + }; + }; + + provider.proxmox = { + pm_api_url = "${pmHost}:8006/api2/json"; + pm_tls_insecure = true; + + pm_user = "root@pam"; + pm_password = ""; + }; + + resource.proxmox_lxc = { + nix01 = { + target_node = "strawberry"; + hostname = "nix01"; + ostemplate = templates.nixos; + password = creds.ctPassword; + unprivileged = true; + swap = 1024; + ostype = "nixos"; + cmode = "console"; + + rootfs = { + storage = "local-lvm"; + size = "16G"; + }; + + network = lib.mkIf addNetworking { + name = "eth0"; + bridge = "vmbr0"; + ip = "192.168.1.220/32"; + gw = "192.168.1.1"; + firewall = false; + }; + + features = { + nesting = true; + }; + }; + + nix02 = { + target_node = "strawberry"; + hostname = "nix02"; + ostemplate = templates.nixos; + password = creds.ctPassword; + unprivileged = true; + swap = 1024; + ostype = "nixos"; + cmode = "console"; + + rootfs = { + storage = "local-lvm"; + size = "16G"; + }; + + network = lib.mkIf addNetworking { + name = "eth0"; + bridge = "vmbr0"; + ip = "192.168.1.221/32"; + gw = "192.168.1.1"; + firewall = false; + }; + + features = { + nesting = true; + }; + }; + }; +} diff --git a/flake.lock b/flake.lock index 9fdbea0..ee29d07 100644 --- a/flake.lock +++ b/flake.lock @@ -21,6 +21,50 @@ "type": "github" } }, + "authentik-nix": { + "inputs": { + "authentik-src": "authentik-src", + "flake-compat": "flake-compat", + "flake-parts": "flake-parts", + "flake-utils": "flake-utils", + "napalm": "napalm", + "nixpkgs": "nixpkgs_2", + "pyproject-build-systems": "pyproject-build-systems", + "pyproject-nix": "pyproject-nix", + "systems": "systems_2", + "uv2nix": "uv2nix" + }, + "locked": { + "lastModified": 1749129962, + "narHash": "sha256-gc1l5z5dWw9a9DWsrp0ZiD+SSMsNpEwMEiRi8K5sh5c=", + "owner": "nix-community", + "repo": "authentik-nix", + "rev": "271a38f7c4e2551f0674b894e2adf7cd1ddb8168", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "authentik-nix", + "type": "github" + } + }, + "authentik-src": { + "flake": false, + "locked": { + "lastModified": 1749043670, + "narHash": "sha256-gwHngqb23U8By7jhxFWQZOXy+vPQApJSkvr4gHI5ifQ=", + "owner": "goauthentik", + "repo": "authentik", + "rev": "bda30c5ad5838fea36dc0a06f8580cca437f0fc0", + "type": "github" + }, + "original": { + "owner": "goauthentik", + "ref": "version/2025.4.2", + "repo": "authentik", + "type": "github" + } + }, "darwin": { "inputs": { "nixpkgs": [ @@ -45,8 +89,8 @@ }, "deploy-rs": { "inputs": { - "flake-compat": "flake-compat", - "nixpkgs": "nixpkgs_2", + "flake-compat": "flake-compat_2", + "nixpkgs": "nixpkgs_3", "utils": "utils" }, "locked": { @@ -64,6 +108,22 @@ } }, "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { "flake": false, "locked": { "lastModified": 1696426674, @@ -83,6 +143,24 @@ "inputs": { "nixpkgs-lib": "nixpkgs-lib" }, + "locked": { + "lastModified": 1748821116, + "narHash": "sha256-F82+gS044J1APL0n4hH50GYdPRv/5JWm34oCJYmVKdE=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "49f0870db23e8c1ca0b5259734a02cd9e1e371a1", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib_2" + }, "locked": { "lastModified": 1743550720, "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", @@ -97,6 +175,48 @@ "type": "github" } }, + "flake-parts_3": { + "inputs": { + "nixpkgs-lib": [ + "terranix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1736143030, + "narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": [ + "authentik-nix", + "systems" + ] + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "home-manager": { "inputs": { "nixpkgs": [ @@ -118,6 +238,32 @@ "type": "github" } }, + "napalm": { + "inputs": { + "flake-utils": [ + "authentik-nix", + "flake-utils" + ], + "nixpkgs": [ + "authentik-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1725806412, + "narHash": "sha256-lGZjkjds0p924QEhm/r0BhAxbHBJE1xMOldB/HmQH04=", + "owner": "willibutz", + "repo": "napalm", + "rev": "b492440d9e64ae20736d3bec5c7715ffcbde83f5", + "type": "github" + }, + "original": { + "owner": "willibutz", + "ref": "avoid-foldl-stack-overflow", + "repo": "napalm", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1703013332, @@ -135,6 +281,21 @@ } }, "nixpkgs-lib": { + "locked": { + "lastModified": 1748740939, + "narHash": "sha256-rQaysilft1aVMwF14xIdGS3sj1yHlI6oKQNBRTF40cc=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "656a64127e9d791a334452c6b6606d17539476e2", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs-lib_2": { "locked": { "lastModified": 1743296961, "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", @@ -149,7 +310,39 @@ "type": "github" } }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1749794982, + "narHash": "sha256-Kh9K4taXbVuaLC0IL+9HcfvxsSUx8dPB5s5weJcc9pc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ee930f9755f58096ac6e8ca94a1887e0534e2d81", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs_2": { + "locked": { + "lastModified": 1748929857, + "narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { "locked": { "lastModified": 1702272962, "narHash": "sha256-D+zHwkwPc6oYQ4G3A1HuadopqRwUY/JkMwHz1YF7j4Q=", @@ -165,7 +358,7 @@ "type": "github" } }, - "nixpkgs_3": { + "nixpkgs_4": { "locked": { "lastModified": 1743583204, "narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=", @@ -181,12 +374,80 @@ "type": "github" } }, + "nixpkgs_5": { + "locked": { + "lastModified": 1728956102, + "narHash": "sha256-J8zo+UYNjHATsxn2/ROl8iaji2RgLm+sG7b3VcD36YM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "3d85bae2431f20ab1ac5cf14d03d314dffe629af", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, + "pyproject-build-systems": { + "inputs": { + "nixpkgs": [ + "authentik-nix", + "nixpkgs" + ], + "pyproject-nix": [ + "authentik-nix", + "pyproject-nix" + ], + "uv2nix": [ + "authentik-nix", + "uv2nix" + ] + }, + "locked": { + "lastModified": 1748562898, + "narHash": "sha256-STk4QklrGpM3gliPKNJdBLSQvIrqRuwHI/rnYb/5rh8=", + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "rev": "33bd58351957bb52dd1700ea7eeefe34de06a892", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "type": "github" + } + }, + "pyproject-nix": { + "inputs": { + "nixpkgs": [ + "authentik-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1746540146, + "narHash": "sha256-QxdHGNpbicIrw5t6U3x+ZxeY/7IEJ6lYbvsjXmcxFIM=", + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "e09c10c24ebb955125fda449939bfba664c467fd", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "type": "github" + } + }, "root": { "inputs": { "agenix": "agenix", + "authentik-nix": "authentik-nix", "deploy-rs": "deploy-rs", - "flake-parts": "flake-parts", - "nixpkgs": "nixpkgs_3" + "flake-parts": "flake-parts_2", + "nixpkgs": "nixpkgs_4", + "nixpkgs-unstable": "nixpkgs-unstable", + "terranix": "terranix" } }, "systems": { @@ -205,6 +466,21 @@ } }, "systems_2": { + "locked": { + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default-linux", + "type": "github" + } + }, + "systems_3": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", @@ -219,9 +495,44 @@ "type": "github" } }, + "systems_4": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "terranix": { + "inputs": { + "flake-parts": "flake-parts_3", + "nixpkgs": "nixpkgs_5", + "systems": "systems_4" + }, + "locked": { + "lastModified": 1749381683, + "narHash": "sha256-16z7tXZch12SAd3d8tbAiEOamyq3zFbw1oUq/ipmTkM=", + "owner": "terranix", + "repo": "terranix", + "rev": "9d2370279d595be9e728b68d29ff0b546d88e619", + "type": "github" + }, + "original": { + "owner": "terranix", + "repo": "terranix", + "type": "github" + } + }, "utils": { "inputs": { - "systems": "systems_2" + "systems": "systems_3" }, "locked": { "lastModified": 1701680307, @@ -236,6 +547,31 @@ "repo": "flake-utils", "type": "github" } + }, + "uv2nix": { + "inputs": { + "nixpkgs": [ + "authentik-nix", + "nixpkgs" + ], + "pyproject-nix": [ + "authentik-nix", + "pyproject-nix" + ] + }, + "locked": { + "lastModified": 1748916602, + "narHash": "sha256-GiwjjmPIISDFD0uQ1DqQ+/38hZ+2z1lTKVj/TkKaWwQ=", + "owner": "pyproject-nix", + "repo": "uv2nix", + "rev": "a4dd471de62b27928191908f57bfcd702ec2bfc9", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "uv2nix", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 379d4d4..073f2c1 100644 --- a/flake.nix +++ b/flake.nix @@ -4,38 +4,110 @@ inputs = { flake-parts.url = "github:hercules-ci/flake-parts"; + authentik-nix.url = "github:nix-community/authentik-nix"; + terranix.url = "github:terranix/terranix"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + # Later version of nixpkgs for forgejo + nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable"; + deploy-rs.url = "github:serokell/deploy-rs"; agenix.url = "github:ryantm/agenix"; }; outputs = inputs@{ flake-parts, self, ... }: + let + sshUser = "root"; + activateConfig = inputs.deploy-rs.lib.x86_64-linux.activate.nixos; + + baseModules = [ + ./secrets + inputs.agenix.nixosModules.default + { + _module.args.unstable = import inputs.nixpkgs-unstable { + system = "x86_64-linux"; + config.allowUnfree = true; + }; + } + ]; + + hosts = { + nix01 = { + location = "nix01.cluster"; + }; + + nix02 = { + location = "nix02.cluster"; + }; + }; + in flake-parts.lib.mkFlake { inherit inputs; } { - systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ]; + systems = [ "x86_64-linux" ]; perSystem = { config, self', inputs', pkgs, system, ... }: { + _module.args.pkgs = import inputs.nixpkgs { + inherit system; + config.allowUnfree = true; + }; + devShells.default = pkgs.mkShell { packages = with pkgs; [ + terraform deploy-rs + just inputs'.agenix.packages.default ]; }; + + packages.default = inputs.terranix.lib.terranixConfiguration { + inherit system; + modules = [ ./config/tf.nix ]; + }; }; flake = { nixosConfigurations.nixos = inputs.nixpkgs.lib.nixosSystem { system = "x86_64-linux"; - modules = [ + modules = baseModules ++ [ ./config/nixos.nix - ./secrets - inputs.agenix.nixosModules.default ]; }; - deploy.nodes.nixos = { - hostname = "nixos.cluster"; + # deploy.nodes.nixos = { + # hostname = "nixos.cluster"; + # profiles.system = { + # sshUser = "root"; + # path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.nixos; + # }; + # }; + + nixosConfigurations.nix01 = inputs.nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = baseModules ++ [ + ./config/nix01.nix + ]; + }; + + deploy.nodes.nix01 = { + hostname = hosts.nix01.location; profiles.system = { - sshUser = "root"; - path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.nixos; + inherit sshUser; + path = activateConfig self.nixosConfigurations.nix01; + }; + }; + + nixosConfigurations.nix02 = inputs.nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = baseModules ++ [ + ./config/nix02.nix + inputs.authentik-nix.nixosModules.default + ]; + }; + + deploy.nodes.nix02 = { + hostname = hosts.nix02.location; + profiles.system = { + inherit sshUser; + path = activateConfig self.nixosConfigurations.nix02; }; }; diff --git a/justfile b/justfile new file mode 100644 index 0000000..131c956 --- /dev/null +++ b/justfile @@ -0,0 +1,15 @@ +[private] +default: + @just --list + +# Deploy the infrastructure through Terraform +infra: + nix build -o tf/homelab.tf.json && cd tf && terraform apply + +# Deploy the systems through deploy-rs +deploy: + ssh-add host-key; deploy --auto-rollback false --magic-rollback false + +# Rekey all of the secrets +rekey: + cd secrets && agenix -r -i ../host-key diff --git a/secrets/atticd.env.age b/secrets/atticd.env.age index bdf798f..dd857c2 100644 Binary files a/secrets/atticd.env.age and b/secrets/atticd.env.age differ diff --git a/secrets/authentik.env.age b/secrets/authentik.env.age new file mode 100644 index 0000000..9c10f63 --- /dev/null +++ b/secrets/authentik.env.age @@ -0,0 +1,11 @@ +age-encryption.org/v1 +-> ssh-ed25519 IYaO9g m8IFAIsugPKr+aH/NKMEuEaUKxgsOEkglfVU+LeCkHE +UmtFqaY5jLy0Vw/mrfGVADj1RFCCdLHE4g1t8SXjiR4 +-> ssh-ed25519 Xqrm3g ZemtqeCHXBipTzFF8Wi6bYMQhOtUPa7cmFDea9RFB2Q +5/ZkLwrYpRyGMb/iQ3rOzZgCfod01lk5s+QgSajESq8 +-> ssh-ed25519 FkAUOA BdMn4hxvWNOvSM5wRhsDtKEFrOOJoqHEF659cC6pO1c +9Txf5IOCDLIVR6aaR29EXfXF505GBzYJv79c2aSad6w +-> ssh-ed25519 G48T3w uV889WAiFjGtIrdqqf05C7Coy+0ZaaeGd0PMCCzfa1U +K32JwPfl2pTNhHZpWbbwD0ESdQhy4VuVG2R+2uy48Qs +--- L4+z0lU8ww3YDkHUpR7zCMLfQMLzpW8VLl22u2yHtTE +KC7D%sAQhDZצ]p]7ks]Fg@Za+?}`fN6;by'ؽ0and[o3W4kPз_Lu 2,#{Y;G7H͑48`Heg;!/Ng` ItY=pQvT~ ", $  j6݆e_}=ۂ@kjխ#|-l1qB \ No newline at end of file diff --git a/secrets/blog.dbpass.age b/secrets/blog.dbpass.age new file mode 100644 index 0000000..e13b168 --- /dev/null +++ b/secrets/blog.dbpass.age @@ -0,0 +1,12 @@ +age-encryption.org/v1 +-> ssh-ed25519 IYaO9g ddDuW7b6yGdgv2TWdNWtn/9cA7Onw7NhnmAqk217jWk +kEZ6a9fr2ujIjFrUmpcrPkOSHiD76r8XoqQ+STYCZxg +-> ssh-ed25519 Xqrm3g 8IHpKy90zF1jGTJ8GpN5pzJvJ53sGWO94ze3sI5wDVw +wNlEOKy4z8f9Fj+/dyfe/gw4csMokoCIGmGGhvZTTXc +-> ssh-ed25519 FkAUOA QRc7iYIMYP/wFDOeswkIoVY9ybFO21GJTX5f0ddAZR0 +W/ZCrz/Ce17zZRqKcych5fxJQDB+ShLCYGFAWBHgrJs +-> ssh-ed25519 G48T3w 1EUU7Vjhf/i8b9oxfg9IhQcu6Wolto74yK/6TvbLZ3g +LQsvfwD1Urxo/wdUkt0QktWEEh0X9E5htHLusqdZRUg +--- HEFxRiVTg2950mW0Gjf8wuzpMo7sa72gn3w92fhbBv4 +qE;'Q +GAVi"pbV͵ \ No newline at end of file diff --git a/secrets/default.nix b/secrets/default.nix index c716f07..e091acc 100644 --- a/secrets/default.nix +++ b/secrets/default.nix @@ -4,6 +4,8 @@ fileName: _: lib.nameValuePair (lib.removeSuffix ".age" fileName) { file = ./. + "/${fileName}"; + # FIXME: Don't do this bruh + mode = "0644"; } ) (import ./secrets.nix); } diff --git a/secrets/forgejo.dbpass.age b/secrets/forgejo.dbpass.age new file mode 100644 index 0000000..f92e244 --- /dev/null +++ b/secrets/forgejo.dbpass.age @@ -0,0 +1,11 @@ +age-encryption.org/v1 +-> ssh-ed25519 IYaO9g VGA0gLwtQGiFgmgEf3tjwTgHLgGEi1RUDmDRLTnIFHY +p74Eblp5zp+6PQNxEPeAdfEYjIWCJptatCJjiqGzXTw +-> ssh-ed25519 Xqrm3g tCFqDPviklsnX5sM1k6aZTTEYXsMRCGE/fPR9Pvy0D0 +EN3zvXgiR2I2gsoJHrf4Ws0e0APrIL4abJpTxmCU0QY +-> ssh-ed25519 FkAUOA v78yauukg/kqKxwyV7OSjrK6cTYsR/WMfrmqX2To50Q +JBBrbiE1OcrU1ccc2dcR075/smE4S34fmEMed8dxhRw +-> ssh-ed25519 G48T3w MJ/fDTqSKaiQayZMYxaIOaQimPMEzsjxHXEYUKB5VBE +x7/Tc8vC5s14t5AAsZBI74h9ylqZWgARDof8tBwkxfE +--- X2s9FwwDdkcRWFMNLiv1JX/BE8RcPZGP86vh+PdpdtE + S=ݡxU]:FÐhD!/7 \ No newline at end of file diff --git a/secrets/pgadmin.password.age b/secrets/pgadmin.password.age new file mode 100644 index 0000000..5d5055d --- /dev/null +++ b/secrets/pgadmin.password.age @@ -0,0 +1,11 @@ +age-encryption.org/v1 +-> ssh-ed25519 IYaO9g MOUCzOR71o8NIie8OHb738/OQ63ztsQm+sJktwTRHUY +lFIByNCGmCE2PWOp2PZE/hxFw4xkn9yUM50gwc2ut68 +-> ssh-ed25519 Xqrm3g IwwSBGM8ua3DqaQN+Wbnf3OmysOfLGJ7TOuNJZYNT3g +kxZmpD/qBlRvJocKxJdwmS5xDTcqDh4n8OuioR+hKtc +-> ssh-ed25519 FkAUOA Uu9awt3H4XnIKzJQZvgJDdqrY6KrCMWJ5QPc25N5gQw +bdXLLhlC6I3QtDcRPXY1gKUhHKePpeQaSWqO2I5CTzg +-> ssh-ed25519 G48T3w ScMxEKkuhSvubQpJCnhr3UdMBl+aF20Bejx3tiBB+lg +DZaY3phejHvYxGrZdE6VnLWrQG/h9Vxm9587SuoEZcE +--- 8B25N9XV5c5T0lLQhUYLs7VV0Zi9Jn1VE8cEGAYyKPs +pxMݬ呹m}Omm5 \ No newline at end of file diff --git a/secrets/secrets.nix b/secrets/secrets.nix index 8dc8c74..03b4a11 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -1,10 +1,21 @@ +# Used by the agenix cli and our module to generate all of the secret entries into the agenix module (see ./default.nix) let + # host-key.pub amy = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDTbclOyOwIAPgVE/v5lIuf0P+Tq/Qkw3+GFa4YuRaCC amy@nixon"; users = [ amy ]; + # /etc/ssh/ssh_host_ed25519_key.pub on each host nixos = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILMAy1iKOrL2yBCWljLnuwo29G5plDblI41jJ4Woy1el root@nixos"; - systems = [ nixos ]; + nix01 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBQfwok81BymeM9zW8D/LPZxRX6HGLkeTi1hS7GjPoZF root@nix01"; + nix02 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGFJBDr16y8BAhtLfbc2WYJLwtgrxEyrpJx0zJpHPn/Z root@nix02"; + systems = [ nixos nix01 nix02 ]; in { "atticd.env.age".publicKeys = users ++ systems; + "blog.dbpass.age".publicKeys = users ++ systems; + "pgadmin.password.age".publicKeys = users ++ systems; + "sharkey.dbpass.age".publicKeys = users ++ systems; + "sharkey.redispass.age".publicKeys = users ++ systems; + "authentik.env.age".publicKeys = users ++ systems; + "forgejo.dbpass.age".publicKeys = users ++ systems; } diff --git a/secrets/sharkey.dbpass.age b/secrets/sharkey.dbpass.age new file mode 100644 index 0000000..6ecf913 Binary files /dev/null and b/secrets/sharkey.dbpass.age differ diff --git a/secrets/sharkey.redispass.age b/secrets/sharkey.redispass.age new file mode 100644 index 0000000..61cfac7 --- /dev/null +++ b/secrets/sharkey.redispass.age @@ -0,0 +1,11 @@ +age-encryption.org/v1 +-> ssh-ed25519 IYaO9g DItojUGo0JgjIqrK08qOAHEPQJyi1O1nxrPlgy/AP1E +mCBsazT0fmMkZS0IPAwED+T9HKTe3tKyQ1Za/aJIgH8 +-> ssh-ed25519 Xqrm3g SElTQ//ZPGb3WcAl8eAlJ15GBFWNdcsb3YQIb70OxlU +sZ2t9r5/D31qnAsrB/L5wktCpqioX2wXqbVxXfhSKWQ +-> ssh-ed25519 FkAUOA pt/3qcltuba+E+z82uhY7jvV28wmrKv49kiTIVYcn3o +B2PoSaa8WTGFNk6R0tq6JXXRQQa3MthhRZtWDfS1MYs +-> ssh-ed25519 G48T3w Zn7f2iF40UtqNyIp+mR/uzK3Gie0ei7EnYqlk83P/08 +eefHjO7mEHG6XmX0iN+vVtMHUe1F25p4Revh6Ii8SUY +--- fU40EtRSgZ9IrSbs8CytvsbTaTWh30xoKsMHmMkUWsE +K=mv>+e1n&@cqI!#9Tq; \ No newline at end of file