cluster/services/dns: switch to acme-dns, host static records

This commit is contained in:
Max Headroom 2023-12-03 16:30:16 +01:00
parent eaa4bdb449
commit 2a9fdfa4f9
12 changed files with 132 additions and 221 deletions

View file

@ -0,0 +1,16 @@
age-encryption.org/v1
-> ssh-ed25519 NO562A YndVtONpmfFXYB1ASnPHsfczl1UbgZ2vccIrX2pEgx0
VzH2UD583L6wBLMCo6faIGyHR4+zXXOUTgQduEiFOxI
-> ssh-ed25519 5/zT0w +67r5S6PSFEgnrTu3eZpOd3eemZUdDOE+kjUw6GDgUM
jPzlW7hePFgsABUjryePu5yergQ2Qjczmmoxuo6CK+U
-> ssh-ed25519 TCgorQ DGJPjJYpeibxM+8OwofUCdttIT2OdNbvQ66wpWQM8XU
JCNQ3bT21j2ZsxbzA6FieKIui6lsvk1p0nvNOT7YtFo
-> ssh-ed25519 d3WGuA hIl5yluwf1f0DP5ZW1MalGPCj4XFYOu2sofwJSQZ6RE
BSHoe4cdRJlPrkc+taUIaIIUknexlGttzz2d9I3jtmk
-> ssh-ed25519 YIaSKQ EbqXS/XFQHSXCbzDJmg4gGUxP9TX3+vOxWtNQDJ8ih4
hNaWzoFG2iVef4Gm30LilGXYNsVkhmVt9dOvBo02mbM
-> V]i@xRtJ-grease
NEPxMUZa76GclWOasWptt6QS7frMclp9o+kD4KCLJB7ucFOYK7xxWfAEMkjtadfP
m0bbgbw7Jcs9/lA8VNAG2D5jTBayGgpkBQZ4
--- ViqZD8mJEKIMCZ5Q+wRQWR2FX/LMEfUwoumUtHlYabQ
KAÉû¹ÝgZü<šë*DfV6·=äG»+eœ`ºpª±ï÷­<1E>º[Û‘Û û¸¢ºÐý-H1<1B>»Ã›Íí[fV.¾¢HÁ"OhÐñŒ½j•ùö8ïßß$‰;Û‘&5<>äxw§/mŒë<C592>Öß^7îf5ÔµyÏŽÓûC´6”¹U•æýi-R=/_R<5F><52>„·==æà½1˜'Ò qÞ·ŒvÜcwø

View file

@ -0,0 +1,21 @@
age-encryption.org/v1
-> ssh-ed25519 NO562A 9n5IirzhNBIPRj9Gir+/yQhFH830sgfezsqY5Ulzz3o
VItDDdgfTFcvSq/QpIqTHnfr1VHqfI6nPz+WWKYQjHw
-> ssh-ed25519 5/zT0w MfBZrd8wJjoProwdPqsS9CZ9aYNTXgrYviFDwuchQVM
8WKPYO+i1ZSkPYDrHVJ5Pclj2hEzqwAtf31Agzei444
-> ssh-ed25519 TCgorQ 3QYtSx/2eiFp54W60F8FlERfHx+DUfnXXfugiXNPECg
pBx3If3qihD//Aq8hDWCt+U1tiWoCLUDcg/RyVCD0D0
-> ssh-ed25519 P/nEqQ NImm+vKuL50G2kdD2svmfkwsovmryCSyKyhnZ0duDDo
U0PTKHiCj4SxomnJdgubo+3sStSE+YwvCnrRl7aAS1Q
-> ssh-ed25519 FfIUuQ SRgJoBIoW71SiXuHqlnGqRG5AKUrnQy0ecwznGEGTHA
a0IS3hjMln1tWEjo30A6gYtaV7TJSY4SZDarhahMoLk
-> ssh-ed25519 d3WGuA 0qVNcrYe53Wo46zFJs6UZtX0dq7TUy72WGdGpLqB3yo
jTHE9PfhRw5lbBlfznS+ThkSsab3ioearf91xyPBfdQ
-> ssh-ed25519 YIaSKQ CCcBlAOms2aSkB6pws6tN+4Gf551idI9Zq0rokd0P1c
/3oFp6hf+jggurbcuu0cXdDL8lr6m/LTHEeNgiJt2gg
-> K&wn-grease ,Ewz Jc+dQQRp NU~.
FvDOuTGNaLuCfDelsrRbthjuJT9fBZAQ+kz+7Stoc2wciXV1YpCcOYDHSF38OwRF
X/pyjVudbJKS0Mphda6phw
--- 3JFwCzeJsIgRkTpmy9MAvQ64BCZoa98kNKOuT57WI6Y
O¿¹¸p ž-ÚP¶.+"<22>ðjÔG«
ëÇÐs<>gnz[t ‘ØóÄD÷•RŽÄ½±šmÃl<!Çê6;³Ù÷<C399>†8{ vmvJJ;lR<6C>×[Yà3˜XPËÜ<C38B>ÈPCÿè¯&¦àåYû×2ÃǤxVúÈF{zäQh nW*I$é;°Yc¨@7Ö-k4—À§xãͶx¿µ% <52>¤$z|»Ê“ñœ¹¯<C2B9>ëñ3

View file

@ -1,109 +0,0 @@
{ cluster, config, lib, pkgs, depot, ... }:
let
inherit (depot.lib.meta) domain;
inherit (config.links) pdnsAdmin;
inherit (cluster.config) vars;
pdns-api = cluster.config.links.powerdns-api;
dataDirUI = "/srv/storage/private/powerdns-admin";
translateConfig = withQuotes: cfg: let
pythonValue = val: if lib.isString val then "'${val}'"
else if lib.isAttrs val && val ? file then "[(f.read().strip('\\n'), f.close()) for f in [open('${val.file}')]][0][0]"
else if lib.isAttrs val && val ? env then "__import__('os').getenv('${val.env}')"
else if lib.isBool val then (if val then "True" else "False")
else if lib.isInt val then toString val
else throw "translateConfig: unsupported value type";
quote = str: if withQuotes then pythonValue str else str;
configList = lib.mapAttrsToList (n: v: "${n}=${quote v}") cfg;
in lib.concatStringsSep "\n" configList;
in {
age.secrets = {
pdns-admin-oidc-secrets = {
file = ./pdns-admin-oidc-secrets.age;
mode = "0400";
};
pdns-admin-salt = {
file = ./pdns-admin-salt.age;
mode = "0400";
owner = "powerdnsadmin";
group = "powerdnsadmin";
};
pdns-admin-secret = {
file = ./pdns-admin-secret.age;
mode = "0400";
owner = "powerdnsadmin";
group = "powerdnsadmin";
};
pdns-api-key = vars.pdns-api-key-secret // { owner = "powerdnsadmin"; };
};
links.pdnsAdmin.protocol = "http";
networking.firewall = {
allowedTCPPorts = [ 53 ];
allowedUDPPorts = [ 53 ];
};
systemd.tmpfiles.rules = [
"d '${dataDirUI}' 0700 powerdnsadmin powerdnsadmin - -"
];
services.powerdns = {
enable = true;
extraConfig = translateConfig false {
api = "yes";
webserver-allow-from = "127.0.0.1, ${vars.meshNet.cidr}";
webserver-address = pdns-api.ipv4;
webserver-port = pdns-api.portStr;
api-key = "$scrypt$ln=14,p=1,r=8$ZRgztsniH1y+F7P/RkXq/w==$QTil5kbJPzygpeQRI2jgo5vK6fGol9YS/NVR95cmWRs=";
};
};
services.powerdns-admin = {
enable = true;
secretKeyFile = config.age.secrets.pdns-admin-secret.path;
saltFile = config.age.secrets.pdns-admin-salt.path;
extraArgs = [ "-b" pdnsAdmin.tuple ];
config = translateConfig true {
SQLALCHEMY_DATABASE_URI = "sqlite:///${dataDirUI}/pda.db";
PDNS_VERSION = pkgs.pdns.version;
PDNS_API_URL = pdns-api.url;
PDNS_API_KEY.file = config.age.secrets.pdns-api-key.path;
SIGNUP_ENABLED = false;
OIDC_OAUTH_ENABLED = true;
OIDC_OAUTH_KEY = "net.privatevoid.dnsadmin1";
OIDC_OAUTH_SECRET.env = "OIDC_OAUTH_SECRET";
OIDC_OAUTH_SCOPE = "openid profile email roles";
OIDC_OAUTH_METADATA_URL = "https://login.${domain}/auth/realms/master/.well-known/openid-configuration";
};
};
systemd.services.powerdns-admin.serviceConfig = {
BindPaths = [
dataDirUI
config.age.secrets.pdns-api-key.path
];
TimeoutStartSec = "300s";
EnvironmentFile = config.age.secrets.pdns-admin-oidc-secrets.path;
};
services.nginx.virtualHosts."dnsadmin.${domain}" = lib.recursiveUpdate
(depot.lib.nginx.vhosts.proxy pdnsAdmin.url)
# backend sends really big headers for some reason
# increase buffer size accordingly
{
locations."/".extraConfig = ''
proxy_busy_buffers_size 512k;
proxy_buffers 4 512k;
proxy_buffer_size 256k;
'';
};
}

View file

@ -7,32 +7,42 @@ let
link = cluster.config.hostLinks.${hostName}.dnsAuthoritative;
patroni = cluster.config.links.patroni-pg-access;
inherit (cluster.config.hostLinks.${hostName}) acmeDnsApi;
otherDnsServers = lib.pipe (with cluster.config.services.dns.otherNodes; (master hostName) ++ (slave hostName)) [
otherDnsServers = lib.pipe (cluster.config.services.dns.otherNodes.authoritative hostName) [
(map (node: cluster.config.hostLinks.${node}.dnsAuthoritative.tuple))
(lib.concatStringsSep " ")
];
translateConfig = cfg: let
configList = lib.mapAttrsToList (n: v: "${n}=${v}") cfg;
in lib.concatStringsSep "\n" configList;
recordsList = lib.mapAttrsToList (lib.const lib.id) cluster.config.dns.records;
recordsPartitioned = lib.partition (record: record.rewrite.target == null) recordsList;
rewriteRecords = lib.filterAttrs (_: record: record.rewrite.target != null) cluster.config.dns.records;
staticRecords = let
escape = type: {
TXT = builtins.toJSON;
}.${type} or lib.id;
rewrites = lib.mapAttrsToList (_: record: let
recordName = record: {
"@" = "${record.root}.";
}.${record.name} or "${record.name}.${record.root}.";
in lib.flatten (
map (record: map (target: "${recordName record} ${record.type} ${escape record.type target}") record.target) recordsPartitioned.right
);
rewrites = map (record: let
maybeEscapeRegex = str: if record.rewrite.type == "regex" then "${lib.escapeRegex str}$" else str;
in "rewrite stop name ${record.rewrite.type} ${record.name}${maybeEscapeRegex ".${record.root}."} ${record.rewrite.target}. answer auto") rewriteRecords;
in "rewrite stop name ${record.rewrite.type} ${record.name}${maybeEscapeRegex ".${record.root}."} ${record.rewrite.target}. answer auto") recordsPartitioned.wrong;
rewriteConf = pkgs.writeText "coredns-rewrites.conf" (lib.concatStringsSep "\n" rewrites);
in {
links.localAuthoritativeDNS = {};
age.secrets = {
pdns-db-credentials = {
file = ./pdns-db-credentials.age;
mode = "0400";
owner = "pdns";
group = "pdns";
acmeDnsDbCredentials = {
file = ./acme-dns-db-credentials.age;
};
acmeDnsDirectKey = {
file = ./acme-dns-direct-key.age;
};
};
@ -41,23 +51,33 @@ in {
allowedUDPPorts = [ 53 ];
};
services.powerdns = {
services.acme-dns = {
enable = true;
extraConfig = translateConfig {
launch = "gpgsql";
local-address = config.links.localAuthoritativeDNS.tuple;
gpgsql-host = patroni.ipv4;
gpgsql-port = patroni.portStr;
gpgsql-dbname = "powerdns";
gpgsql-user = "powerdns";
gpgsql-extra-connection-parameters = "passfile=${config.age.secrets.pdns-db-credentials.path}";
version-string = "Private Void DNS";
enable-lua-records = "yes";
expand-alias = "yes";
resolver = "127.0.0.1:8600";
package = depot.packages.acme-dns;
settings = {
general = {
listen = config.links.localAuthoritativeDNS.tuple;
inherit domain;
nsadmin = "hostmaster.${domain}";
nsname = "eu1.ns.${domain}";
records = staticRecords;
};
api = {
ip = acmeDnsApi.ipv4;
inherit (acmeDnsApi) port;
};
database = {
engine = "postgres";
connection = "postgres://acmedns@${patroni.tuple}/acmedns?sslmode=disable";
};
};
};
systemd.services.acme-dns.serviceConfig.EnvironmentFile = with config.age.secrets; [
acmeDnsDbCredentials.path
acmeDnsDirectKey.path
];
services.coredns = {
enable = true;
config = ''
@ -85,18 +105,29 @@ in {
};
systemd.services.coredns = {
after = [ "pdns.service" ];
after = [ "acme-dns.service" ];
};
consul.services.pdns = {
mode = "external";
definition = {
name = "authoritative-dns-backend";
address = config.links.localAuthoritativeDNS.ipv4;
port = config.links.localAuthoritativeDNS.port;
consul.services = {
authoritative-dns = {
unit = "acme-dns";
definition = {
name = "authoritative-dns-backend";
address = config.links.localAuthoritativeDNS.ipv4;
port = config.links.localAuthoritativeDNS.port;
checks = lib.singleton {
interval = "60s";
tcp = config.links.localAuthoritativeDNS.tuple;
};
};
};
acme-dns.definition = {
name = "acme-dns";
address = acmeDnsApi.ipv4;
port = acmeDnsApi.port;
checks = lib.singleton {
interval = "60s";
tcp = config.links.localAuthoritativeDNS.tuple;
http = "${acmeDnsApi.url}/health";
};
};
};

View file

@ -13,10 +13,9 @@ let
(lib.concatStringsSep " ")
];
authoritativeServers = lib.pipe (with cluster.config.services.dns.nodes; master ++ slave) [
(map (node: cluster.config.hostLinks.${node}.dnsAuthoritative.tuple))
(lib.concatStringsSep ";")
];
authoritativeServers = map
(node: cluster.config.hostLinks.${node}.dnsAuthoritative.tuple)
cluster.config.services.dns.nodes.authoritative;
inherit (depot.packages) stevenblack-hosts;
dot = config.security.acme.certs."securedns.${domain}";
@ -54,29 +53,29 @@ in
services.coredns = {
enable = true;
config = ''
.:${link.portStr} {
${lib.optionalString (interfaces ? vstub) "bind ${interfaces.vstub.addr}"}
bind 127.0.0.1
bind ${link.ipv4}
(localresolver) {
hosts ${stevenblack-hosts} {
fallthrough
}
chaos "Private Void DNS" info@privatevoid.net
forward hyprspace. 127.80.1.53:5380
forward ${domain}. ${lib.concatStringsSep " " authoritativeServers} {
policy random
}
forward . ${backend.tuple} ${otherRecursors} {
policy sequential
}
}
.:${link.portStr} {
${lib.optionalString (interfaces ? vstub) "bind ${interfaces.vstub.addr}"}
bind 127.0.0.1
bind ${link.ipv4}
import localresolver
}
tls://.:853 {
bind ${interfaces.primary.addr}
tls {$CREDENTIALS_DIRECTORY}/dot-cert.pem {$CREDENTIALS_DIRECTORY}/dot-key.pem
hosts ${stevenblack-hosts} {
fallthrough
}
chaos "Private Void DNS" info@privatevoid.net
forward . ${backend.tuple} ${otherRecursors} {
policy sequential
}
import localresolver
}
'';
};
@ -86,7 +85,7 @@ in
dnssecValidation = "process";
forwardZones = {
# optimize queries against our own domain
"${domain}" = authoritativeServers;
"${domain}" = lib.concatStringsSep ";" authoritativeServers;
};
dns = {
inherit (backend) port;

View file

@ -9,26 +9,27 @@ in
./options.nix
];
vars.pdns-api-key-secret = {
file = ./pdns-api-key.age;
mode = "0400";
};
links = {
dnsResolver = {
ipv4 = hours.VEGAS.interfaces.vstub.addr;
port = 53;
};
powerdns-api = {
ipv4 = config.vars.mesh.VEGAS.meshIp;
acmeDnsApi = {
hostname = "acme-dns-challenge.internal.${depot.lib.meta.domain}";
protocol = "http";
};
};
hostLinks = lib.mkMerge [
(lib.genAttrs (with cfg.nodes; master ++ slave) (node: {
(lib.genAttrs cfg.nodes.authoritative (node: {
dnsAuthoritative = {
ipv4 = hours.${node}.interfaces.primary.addrPublic;
port = 53;
};
acmeDnsApi = {
ipv4 = config.vars.mesh.${node}.meshIp;
inherit (config.links.acmeDnsApi) port;
protocol = "http";
};
}))
(lib.genAttrs cfg.nodes.coredns (node: {
dnsResolver = {
@ -44,21 +45,19 @@ in
];
services.dns = {
nodes = {
master = [ "VEGAS" ];
slave = [ "checkmate" "prophet" ];
authoritative = [ "VEGAS" "checkmate" "prophet" ];
coredns = [ "checkmate" "VEGAS" ];
client = [ "checkmate" "grail" "thunderskin" "VEGAS" "prophet" ];
};
nixos = {
master = [
./authoritative.nix
./admin.nix
];
slave = ./authoritative.nix;
authoritative = ./authoritative.nix;
coredns = ./coredns.nix;
client = ./client.nix;
};
};
dns.records.securedns.consulService = "securedns";
dns.records = {
securedns.consulService = "securedns";
"acme-dns-challenge.internal".consulService = "acme-dns";
};
}

View file

@ -1,11 +0,0 @@
age-encryption.org/v1
-> ssh-ed25519 NO562A d/YNanH/cHoFLPp8WcCXHh/LQLRwaUa95JiRLbgb8RI
UPEHpnHHTU6dGKi2MbApEspcpt1lFtFZ4XJjShL7OoE
-> ssh-ed25519 5/zT0w Rv9ZS5P2Eca3npPLR7yym/XTRSDfVmgRwH1pAGR79T8
4A/KXc2wxxokfDAwWYf0ZTUEzQ8ldkC+zRNZY3KjBTs
-> ssh-ed25519 d3WGuA 2R0kaVjuhU3wT9pjj214zkEaHYNSlMxf9Z+MfBssHwY
EU5LWk6xfohWM/3sAqYtUvFmRgIPxOLXHnlqbsQ3+ok
-> -|(-grease W=cc~ O2q5
FZzh/ZwDS2EqvVZ9NErmUwCMN72op1Qy
--- Ducan3ugRJC3dmWLr7+FKok+WmInOgOzW0ccYeqAFAQ
Ì•ãÆ*Q. SC<53>ûf¹‰*`5<>„ÑÖw"~ÍxwÜ*–ã\êÙ"²ÅtŒ 'É0ï™<C3AF>ï

View file

@ -1,12 +0,0 @@
age-encryption.org/v1
-> ssh-ed25519 NO562A hUR+UdHnpazhANM8DKToI5Th3lv1aAuxZ1IQKvCOv34
PvsiSym8YdleDULLnWuTs1x08KO3EmAg/AAjulgrgqE
-> ssh-ed25519 5/zT0w qMXS2xLOLv/+l6brG11i+3FwHdrhlmxZBNtBiU9hu2g
BlFYPvH4mFJRMHTlHwnBdJb6QcugylwZuT5bgSKcQa0
-> ssh-ed25519 d3WGuA k2fRQ3+HyZP+bb/gkVKQqUmbITJLPm9tGp67DbRfiCs
RX9CACfYpYKvSqyfXjvEokTGsp4+ECQBD8i1ehD5xRg
-> IB@F$9G-grease
cXRgUVdIPGEjft1CJA
--- si16Det/GwF7GLHLt0ha8v4rFFeJXyhEylIiqzZVAK8
Ö°å¤pÐǺ#ê4^©— ~u Uuç­aòQ´Bâj˜(N)qÃ<"¤%ì’,V9û5ZÔh§#W«[»ò¶”"Mÿ&”îäøÖýá+%Œ«„SQ€B÷ÞÕÀèÕyàÜî<aéó]P$´Ä±B¨½qQÑÉQ‡M‰TË
·s¹mÿ~qWÖ«çêõÜ×Ì=.Q“"ù”Þø¶ÏnqRk<52>=ÏcÿçüßÃqv¢¾>#ŠÏ«²tïwq,÷ »3YyIq}Ê“ì>sgíz™ûs±Þ ¸ƆFÄPê|ÍüÅ¡=ùÃþ~KQR,DZuÐ+ÕºZGHëa=‹©;ÀõC.ÏuVShÅ$Và€AË9Ð= ?•¢

View file

@ -1,20 +0,0 @@
age-encryption.org/v1
-> ssh-ed25519 NO562A OQaDWMrfvfQoluWFIldZgZFEdqzFfXhPvO6BqOZofnU
qoUEZlKSTNJ53jgTK9eP2GDJogugtCfKqBaVH7mCqZY
-> ssh-ed25519 5/zT0w U5w9w/DE+zDgw4YI6DDVAMSaAAcR+3+BIioVXAGMfHg
9Ps2qB+P2DWDdYPRPuzmBECWzJ90LVq8B71LlrO0Gyk
-> ssh-ed25519 TCgorQ s91OjOZH6825aSBRfiSN+ODBOJvbjff6s2fzf/8o2Wk
zJI/5oKwagyOJUy1siwAcZ7wcsEMUyekYjP7TlsAjoY
-> ssh-ed25519 d3WGuA 1gPF8W/p+wVclVrMGbvnBAO9IvSX9G8qNEaKpHeX23w
L4N6MxD5SeEhqcjRx1e8M/rMtK2Qg+elYgKCHkHi71o
-> ssh-ed25519 YIaSKQ eOwUbPa6RceRM4zsB8lHSCYtSJoLX1Fqs8CdzM7qkCQ
8OPkkFP0B+uN0zBZAUmEgogp97YO+qlvsG6wnMwkzLw
-> L_-grease 51PFh7A
k9hZ2FbD3JDWGN8/WFjOCM0Ud/uvQhZZDceL/Esa8cfp
--- v5Noo1KII/WFJxNGjEO2hqdhgHdastilx/M1vFos5dE
 mÄÜ´Räx¡˜ ÐòÁ¬;ä³ÁH°pæ áµå-ìásÌïaÎᙵ­Ô ™÷Ð4ö®y ˆÑYýÀïQ<>ûÂHPe 0Ó0[ÙÕ» É
ÔŽÜyÖ'ª±¨|È2[q<>—ÀÛ<C380><C39B>WS/dö.ÏQÁÒÙé49ÆÄ,͆±¢}o¦<6F>Ú ÍGO¦k€rGMGœ&öÊ¡²
4Óá"8.êm槫¹<C2AB>7Pku ð@XAå$• >·¦+Äì|Çå–è<1F> ÎVtn¡”Â|Cµ>\a<>2
{U²´ªÝs <0B>Ù èé¾Ï÷„b½É‡Â<E280BA>¿½gÀ.sœ3‡M24[š+ÀU£ÊD!PØ´õù7Á[½_†ºÁ>aº¿Õ3
Šñs

View file

@ -13,11 +13,8 @@ in with hosts;
"cluster/services/cachix-deploy-agent/credentials/prophet.age".publicKeys = max ++ map systemKeys [ prophet ];
"cluster/services/cachix-deploy-agent/credentials/VEGAS.age".publicKeys = max ++ map systemKeys [ VEGAS ];
"cluster/services/cachix-deploy-agent/credentials/thunderskin.age".publicKeys = max ++ map systemKeys [ thunderskin ];
"cluster/services/dns/pdns-admin-oidc-secrets.age".publicKeys = max ++ map systemKeys [ VEGAS ];
"cluster/services/dns/pdns-admin-salt.age".publicKeys = max ++ map systemKeys [ VEGAS ];
"cluster/services/dns/pdns-admin-secret.age".publicKeys = max ++ map systemKeys [ VEGAS ];
"cluster/services/dns/pdns-api-key.age".publicKeys = max ++ map systemKeys [ checkmate grail thunderskin VEGAS prophet ];
"cluster/services/dns/pdns-db-credentials.age".publicKeys = max ++ map systemKeys [ checkmate VEGAS prophet ];
"cluster/services/dns/acme-dns-direct-key.age".publicKeys = max ++ map systemKeys [ checkmate grail thunderskin VEGAS prophet ];
"cluster/services/dns/acme-dns-db-credentials.age".publicKeys = max ++ map systemKeys [ checkmate VEGAS prophet ];
"cluster/services/forge/credentials/forgejo-oidc-secret.age".publicKeys = max ++ map systemKeys [ VEGAS ];
"cluster/services/forge/credentials/forgejo-db-credentials.age".publicKeys = max ++ map systemKeys [ VEGAS ];
"cluster/services/hercules-ci-multi-agent/secrets/hci-cache-config.age".publicKeys = max ++ map systemKeys [ VEGAS prophet ];