Migrate matrix-synapse to clan services

This commit is contained in:
pinpox
2025-08-11 14:53:58 +02:00
parent a1ff794d57
commit b239c5bd88
22 changed files with 447 additions and 85 deletions

View File

@@ -98,7 +98,6 @@ in
# Container Tests # Container Tests
nixos-test-container = self.clanLib.test.containerTest ./container nixosTestArgs; nixos-test-container = self.clanLib.test.containerTest ./container nixosTestArgs;
nixos-test-zt-tcp-relay = self.clanLib.test.containerTest ./zt-tcp-relay nixosTestArgs; nixos-test-zt-tcp-relay = self.clanLib.test.containerTest ./zt-tcp-relay nixosTestArgs;
nixos-test-matrix-synapse = self.clanLib.test.containerTest ./matrix-synapse nixosTestArgs;
nixos-test-user-firewall-iptables = self.clanLib.test.containerTest ./user-firewall/iptables.nix nixosTestArgs; nixos-test-user-firewall-iptables = self.clanLib.test.containerTest ./user-firewall/iptables.nix nixosTestArgs;
nixos-test-user-firewall-nftables = self.clanLib.test.containerTest ./user-firewall/nftables.nix nixosTestArgs; nixos-test-user-firewall-nftables = self.clanLib.test.containerTest ./user-firewall/nftables.nix nixosTestArgs;

View File

@@ -1,83 +0,0 @@
(
{ pkgs, ... }:
{
name = "matrix-synapse";
nodes.machine =
{
config,
self,
lib,
...
}:
{
imports = [
self.clanModules.matrix-synapse
self.nixosModules.clanCore
{
clan.core.settings.directory = ./.;
services.nginx.virtualHosts."matrix.clan.test" = {
enableACME = lib.mkForce false;
forceSSL = lib.mkForce false;
};
clan.nginx.acme.email = "admins@clan.lol";
clan.matrix-synapse = {
server_tld = "clan.test";
app_domain = "matrix.clan.test";
};
clan.matrix-synapse.users.admin.admin = true;
clan.matrix-synapse.users.someuser = { };
clan.core.facts.secretStore = "vm";
clan.core.vars.settings.secretStore = "vm";
clan.core.vars.settings.publicStore = "in_repo";
# because we use systemd-tmpfiles to copy the secrets, we need to a separate systemd-tmpfiles call to provision them.
boot.postBootCommands = "${config.systemd.package}/bin/systemd-tmpfiles --create /etc/tmpfiles.d/00-vmsecrets.conf";
systemd.tmpfiles.settings."00-vmsecrets" = {
# run before 00-nixos.conf
"/etc/secrets" = {
d.mode = "0700";
z.mode = "0700";
};
"/etc/secrets/matrix-synapse/synapse-registration_shared_secret" = {
f.argument = "supersecret";
z = {
mode = "0400";
user = "root";
};
};
"/etc/secrets/matrix-password-admin/matrix-password-admin" = {
f.argument = "matrix-password1";
z = {
mode = "0400";
user = "root";
};
};
"/etc/secrets/matrix-password-someuser/matrix-password-someuser" = {
f.argument = "matrix-password2";
z = {
mode = "0400";
user = "root";
};
};
};
}
];
};
testScript = ''
start_all()
machine.wait_for_unit("matrix-synapse")
machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 8008")
machine.wait_until_succeeds("${pkgs.curl}/bin/curl -Ssf -L http://localhost/_matrix/static/ -H 'Host: matrix.clan.test'")
machine.systemctl("restart matrix-synapse >&2") # check if user creation is idempotent
machine.execute("journalctl -u matrix-synapse --no-pager >&2")
machine.wait_for_unit("matrix-synapse")
machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 8008")
machine.succeed("${pkgs.curl}/bin/curl -Ssf -L http://localhost/_matrix/static/ -H 'Host: matrix.clan.test'")
'';
}
)

View File

@@ -1 +0,0 @@
registration_shared_secret: supersecret

View File

@@ -0,0 +1,236 @@
{ ... }:
{
_class = "clan.service";
manifest.name = "clan-core/matrix-synapese";
manifest.description = "A federated messaging server with end-to-end encryption.";
manifest.categories = [ "Social" ];
roles.default = {
interface =
{ lib, ... }:
{
options = {
acmeEmail = lib.mkOption {
type = lib.types.str;
description = ''
Email address for account creation and correspondence from the CA.
It is recommended to use the same email for all certs to avoid account
creation limits.
'';
};
services.matrix-synapse.package = lib.mkOption {
readOnly = false;
description = "Package to use for matrix-synapse";
};
server_tld = lib.mkOption {
type = lib.types.str;
description = "The address that is suffixed after your username i.e @alice:example.com";
example = "example.com";
};
app_domain = lib.mkOption {
type = lib.types.str;
description = "The matrix server hostname also serves the element client";
example = "matrix.example.com";
};
users = lib.mkOption {
default = { };
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
description = "The name of the user";
};
admin = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether the user should be an admin";
};
};
}
)
);
description = "A list of users. Not that only new users will be created and existing ones are not modified.";
example.alice = {
admin = true;
};
};
};
};
perInstance =
{ settings, ... }:
{
nixosModule =
{
pkgs,
config,
lib,
...
}:
{
imports = [ ./nginx.nix ];
security.acme.defaults.email = settings.acmeEmail;
services.matrix-synapse = {
enable = true;
settings = {
server_name = settings.server_tld;
database = {
args.user = "matrix-synapse";
args.database = "matrix-synapse";
name = "psycopg2";
};
turn_uris = [
"turn:turn.matrix.org?transport=udp"
"turn:turn.matrix.org?transport=tcp"
];
registration_shared_secret_path = "/run/synapse-registration-shared-secret";
listeners = [
{
port = 8008;
bind_addresses = [ "::1" ];
type = "http";
tls = false;
x_forwarded = true;
resources = [
{
names = [ "client" ];
compress = true;
}
{
names = [ "federation" ];
compress = false;
}
];
}
];
};
};
clan.core.postgresql.enable = true;
clan.core.postgresql.users.matrix-synapse = { };
clan.core.postgresql.databases.matrix-synapse.create.options = {
TEMPLATE = "template0";
LC_COLLATE = "C";
LC_CTYPE = "C";
ENCODING = "UTF8";
OWNER = "matrix-synapse";
};
clan.core.postgresql.databases.matrix-synapse.restore.stopOnRestore = [ "matrix-synapse" ];
clan.core.vars.generators = {
"matrix-synapse" = {
files."synapse-registration_shared_secret" = { };
runtimeInputs = with pkgs; [
coreutils
pwgen
];
migrateFact = "matrix-synapse";
script = ''
echo -n "$(pwgen -s 32 1)" > "$out"/synapse-registration_shared_secret
'';
};
}
// lib.mapAttrs' (
name: user:
lib.nameValuePair "matrix-password-${user.name}" {
files."matrix-password-${user.name}" = { };
migrateFact = "matrix-password-${user.name}";
runtimeInputs = with pkgs; [ xkcdpass ];
script = ''
xkcdpass -n 4 -d - > "$out"/${lib.escapeShellArg "matrix-password-${user.name}"}
'';
}
) settings.users;
systemd.services.matrix-synapse =
let
usersScript = ''
while ! ${pkgs.netcat}/bin/nc -z -v ::1 8008; do
if ! kill -0 "$MAINPID"; then exit 1; fi
sleep 1;
done
''
+ lib.concatMapStringsSep "\n" (user: ''
# only create user if it doesn't exist
/run/current-system/sw/bin/matrix-synapse-register_new_matrix_user --exists-ok --password-file ${
config.clan.core.vars.generators."matrix-password-${user.name}".files."matrix-password-${user.name}".path
} --user "${user.name}" ${if user.admin then "--admin" else "--no-admin"}
'') (lib.attrValues settings.users);
in
{
path = [ pkgs.curl ];
serviceConfig.ExecStartPre = lib.mkBefore [
"+${pkgs.coreutils}/bin/install -o matrix-synapse -g matrix-synapse ${
lib.escapeShellArg
config.clan.core.vars.generators.matrix-synapse.files."synapse-registration_shared_secret".path
} /run/synapse-registration-shared-secret"
];
serviceConfig.ExecStartPost = [
''+${pkgs.writeShellScript "matrix-synapse-create-users" usersScript}''
];
};
services.nginx = {
enable = true;
virtualHosts = {
"${settings.server_tld}" = {
locations."= /.well-known/matrix/server".extraConfig = ''
add_header Content-Type application/json;
return 200 '${builtins.toJSON { "m.server" = "${settings.app_domain}:443"; }}';
'';
locations."= /.well-known/matrix/client".extraConfig = ''
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '${
builtins.toJSON {
"m.homeserver" = {
"base_url" = "https://${settings.app_domain}";
};
"m.identity_server" = {
"base_url" = "https://vector.im";
};
}
}';
'';
forceSSL = true;
enableACME = true;
};
"${settings.app_domain}" =
let
# FIXME: This was taken from upstream. Drop this when our patch is upstream
element-web =
pkgs.runCommand "element-web-with-config" { nativeBuildInputs = [ pkgs.buildPackages.jq ]; }
''
cp -r ${pkgs.element-web} $out
chmod -R u+w $out
jq '."default_server_config"."m.homeserver" = { "base_url": "https://${settings.app_domain}:443", "server_name": "${settings.server_tld}" }' \
> $out/config.json < ${pkgs.element-web}/config.json
ln -s $out/config.json $out/config.${settings.app_domain}.json
'';
in
{
forceSSL = true;
enableACME = true;
locations."/".root = element-web;
locations."/_matrix".proxyPass = "http://localhost:8008"; # TODO: We should make the port configurable
locations."/_synapse".proxyPass = "http://localhost:8008";
};
};
};
};
};
};
}

View File

@@ -0,0 +1,16 @@
{ lib, ... }:
let
module = lib.modules.importApply ./default.nix { };
in
{
clan.modules.matrix-synapse = module;
perSystem =
{ ... }:
{
clan.nixosTests.matrix-synapse = {
imports = [ ./tests/vm/default.nix ];
clan.modules."@clan/matrix-synapse" = module;
};
};
}

View File

@@ -0,0 +1,48 @@
{
config,
lib,
...
}:
{
security.acme.acceptTerms = true;
networking.firewall.allowedTCPPorts = [
443
80
];
services.nginx = {
enable = true;
statusPage = lib.mkDefault true;
recommendedBrotliSettings = lib.mkDefault true;
recommendedGzipSettings = lib.mkDefault true;
recommendedOptimisation = lib.mkDefault true;
recommendedProxySettings = lib.mkDefault true;
recommendedTlsSettings = lib.mkDefault true;
# Nginx sends all the access logs to /var/log/nginx/access.log by default.
# instead of going to the journal!
commonHttpConfig = "access_log syslog:server=unix:/dev/log;";
resolver.addresses =
let
isIPv6 = addr: builtins.match ".*:.*:.*" addr != null;
escapeIPv6 = addr: if isIPv6 addr then "[${addr}]" else addr;
cloudflare = [
"1.1.1.1"
"2606:4700:4700::1111"
];
resolvers =
if config.networking.nameservers == [ ] then cloudflare else config.networking.nameservers;
in
map escapeIPv6 resolvers;
sslDhparam = config.security.dhparams.params.nginx.path;
};
security.dhparams = {
enable = true;
params.nginx = { };
};
}

View File

@@ -0,0 +1,56 @@
{
name = "matrix-synapse";
clan = {
directory = ./.;
inventory = {
machines.machine = { };
instances = {
matrix-synaps = {
module.name = "@clan/matrix-synapse";
module.input = "self";
roles.default.machines."machine".settings = {
acmeEmail = "admins@clan.lol";
server_tld = "clan.test";
app_domain = "matrix.clan.test";
users.admin.admin = true;
users.someuser = { };
};
};
};
};
};
nodes.machine =
{ lib, pkgs, ... }:
{
environment.systemPackages = with pkgs; [
curl
netcat
];
services.nginx.virtualHosts."matrix.clan.test" = {
enableACME = lib.mkForce false;
forceSSL = lib.mkForce false;
};
};
testScript = ''
start_all()
machine.wait_for_unit("matrix-synapse")
machine.succeed("nc -z -v ::1 8008")
machine.wait_until_succeeds("curl -Ssf -L http://localhost/_matrix/static/ -H 'Host: matrix.clan.test'")
machine.systemctl("restart matrix-synapse >&2") # check if user creation is idempotent
machine.execute("journalctl -u matrix-synapse --no-pager >&2")
machine.wait_for_unit("matrix-synapse")
machine.succeed("nc -z -v ::1 8008")
machine.succeed("curl -Ssf -L http://localhost/_matrix/static/ -H 'Host: matrix.clan.test'")
'';
}

View File

@@ -0,0 +1,6 @@
[
{
"publickey": "age172llwtsm7g4zkej0w5x5kq73mf7tkc26k078nqzu0ehs408x6d8qcufyy7",
"type": "age"
}
]

View File

@@ -0,0 +1,15 @@
{
"data": "ENC[AES256_GCM,data:oHcz6O1WTWbxF4pNPDFelEXsGbO8/jkaIyynCHTSglMT9U1HA6KwHf9NmV3nAk7gHupBSTQzQ6iwagW0mS/Ujb8Ugar43WPajTw=,iv:+KElrlY7YQSPJ1ErNxhq+C5Kx6KKhKWqc8Y+XkZycEE=,tag:eDX86E42Rf6Li1rMADhwvA==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnbVZpdGdDNTZ6dVVUeXlo\nWGt5OTZSSUFvN1lrM1V4MlcyMTRsMUNUOVY0ClBKTmNNN2Z1SWJXVXptVkE0Z1gx\ncENrQko1QnBPS0VoQy9INFdnbEl3d1EKLS0tIHNTYXRDS0d1U3d6K0d1QTYzOWtV\nRE1ZVDlyOXFRTDA5WG43VWZsb1NlUkEKbM8AQJ/te8SNNFxWWy2c3TtrgWYDhtr9\nvRzC5T8IGxlUaPyRWeHsoJhBwgLogmIzSnhZlZQYKMONQPlm87aQSg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-08-11T12:50:09Z",
"mac": "ENC[AES256_GCM,data:BPXgOx3kq2IedntQMcZEigF5ImDbdHp3dKIJmPUqfsGCy2In7E56kHIzkuvM10LGvZtQPWRvrb+Oxgi9TWtWICHPtOC7+fLAESuz5hWKHnkL3z6Z9rWyBFZfRGQPl1BdMsAuisdtTJugIFo4BDJhDpBMVpBLs2MAU2gULbEk4vw=,iv:h6p1g1i5N4/dCMNDHoVyZat82+SH/yOq8QnHuz4JHTA=,tag:5DT7qP+ght1vsGuu9in9Zg==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../users/admin

View File

@@ -0,0 +1,4 @@
{
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"type": "age"
}

View File

@@ -0,0 +1 @@
../../../../../../sops/machines/machine

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:iXhekfMnuMpEQvicMvR97qj2/rPKjxjI9NZTIpk=,iv:qWO6+OkTIJNJtyT0y0OS906WalGdy9KR1RnusZPKo4s=,tag:Ipx9pXOC9KQcwcKRFyCDsA==,type:str]",
"sops": {
"age": [
{
"recipient": "age172llwtsm7g4zkej0w5x5kq73mf7tkc26k078nqzu0ehs408x6d8qcufyy7",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSa3FXQ0M0Y011SGd4Sm1r\nQmU0THI5ZU54SXdMbCtzelhrS29ndHQrdFNRCnJ6RlhHY0I5SzZxWlUybDZaQVB0\nUTFLWnpEOUk1RnRvYUhUaDcxa1F1d0EKLS0tIGphZjk5RlY2UjJybE44V2diSncy\ndTgzNnE3dFpQYkttOU4rc2J5Zzk4TUUKOoZeoflBVOzZs6Whj68jLuBqtxCexD58\na6wZOmEnOs3aWxXWpYI3V+f1wBqHpTWLUZzCXuLvZrpOQQT5dJm+Gg==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXMWt6eXV6SDBFa01mOEY2\nZGtzZEowWWRoUzlNK0xlM3MyL0pHY1lRdDFFCmcrRGozanBkSHBMTmd2Wk1pRjIv\nUDYvM1Vac3V0YXNQSTJHOVJpL3Y5NG8KLS0tIE1ENndVYlZMQ1JGMDJZWnE3RVZ5\nN2hNZ0xBdWY2R1NFbmZsd2hQa204a1EKHeYs9bZx4pDIx9FNzwGjAQBTmKhYoMd3\nzH1UqZLXV+JJ96BhJlfKDUcdci02p3OvaSBkLzsQYwcBGa53AmfdSg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-08-11T12:50:09Z",
"mac": "ENC[AES256_GCM,data:FafQRxGPFc5AuAqFQ/7jDGTwDvhki6ateKqcdXNNuEc9eYo7D7yRHsJR43jL1kCYhbfF40kwhmitRgGPUgKy+ut+aFuGc5kWFBMNK/89pX4w0m7fXbNyk8bXR3QQJSo6MKWTGsBdjk3ogUIoYb6TI+PVOZ0R9x3FcW3sMWKQteA=,iv:NbGSkVWuJ8Aso5+5mFVk+IGLTi9RK/7joDvZD2z5f88=,tag:4b/1J4ZODUxcvm42zNrKCQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1 @@
../../../../../../sops/users/admin

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:e1CjqqTn9obMX95u3x43BDWiZJUnm0gItrs/kkQ5Qw==,iv:WPFkA6x73fjdn7b8BpSIA2N6AlX57+nJXQvxef8CtjE=,tag:3/EosUDn98II5Ha5/DwPsw==,type:str]",
"sops": {
"age": [
{
"recipient": "age172llwtsm7g4zkej0w5x5kq73mf7tkc26k078nqzu0ehs408x6d8qcufyy7",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvVXNjeHE0VWpNSWdSSWJ0\neWRwUXNiRmQ2bFJFV2hObGJGdTk2TUkyNXkwCkJ4SHJyOGZkSzBFeW4zWDhFamZ5\nUlU2eXpucytYYlNSMlczdGtzZWhVT1kKLS0tIEpSUzl6L29kZ285aHFIdTlWN21r\nUUUyVFFMOVZ1OXZrSFBvbjdMUkpadmcK9JSI5DEbQ64M5wk8+Y0ATUDh/0VaogDJ\nAMLumw0rM8+/KVUX9TdPS5oppQTGESuBhQeU9E61+5YwfJCSTjwIpQ==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUb1BMQWt4ZEhBaWtWdkZH\nN01DMFpObWVQMVlSWm1XTnZENWhYNGxmY1ZZClVmN1B6SlZYdFJsVUk4WkJrYnhn\ncWpuQUQrWjNrUUhQRGZuTHdPdDlMYWsKLS0tIFlhUDBOeUQ5SXNtVisrclhKb1V2\nOVlOZjJoSGpKQXJTaEJaVDAwU3JteTQKoQ6hndQhxCvC6pXBZc6U8kynMPlEyM5v\nylfFLZ3AvbkAegZ6e7XPKaYFhBllUrUVwreX1LbermI7KY8UTcEOuw==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-08-11T12:50:12Z",
"mac": "ENC[AES256_GCM,data:KkdRJrrskyrXl+yCnNC3HE7J6SZXgvRJgXfOrWMIorYAZuKKRj2cjwDcpoibE+J/gEaippNRf809bv8+fQrBcxVSqyYs3k3cryP44RlLdkRwpetrrVrN/ClzPPchklEOs6zMBtyXTup/cOlvHaMBjh3+EOwUhb581eVCohPjHq8=,iv:SwHOYM0yDUDJIEzNg3k8M8ym5HWIvXpV2UlZ94Q5VYI=,tag:jnzod5S3puI3IXhMFz5jWQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:ypoXgGGj6hT6KbiZeOU0PHkSJlxANK1P/R0VuwQ3LgY=,iv:l5qtZZPJuz39TBood/0BOJ/ozpm1DS/0Z9jPh1zdcoI=,tag:rYdl/VenObkTP8t6GR9+Pw==,type:str]",
"sops": {
"age": [
{
"recipient": "age172llwtsm7g4zkej0w5x5kq73mf7tkc26k078nqzu0ehs408x6d8qcufyy7",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2RG83QnlaYU9ZT2N4OURK\nMW9CbjBOblJOM3V0NXU1Zm5CRHM3WDVmNkhzCkY2TDBBY2YwcFFiZHJkNlQxNWhi\naXZaUVBpbFVMWWVRYU8zQk5kNk5oOTgKLS0tIGlneFFoV09LTGJ0ZS9OaFpnS1Iv\nUmoySW01NUFmL3FOWUxDc01mYi9pdjgKI+i506u3x/o99Ntkpxp31LTsG09HPu61\n9/QIlHwSfQ1iqmu4JB6VKpgJgaYK17U7sUGUYHq0Nqa16yz04ukkCA==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTcWNkR2ZGNXMvM0E1dytL\nNndaOXZHQUdHN2gwdE5SQlBGWmR6RGN3aGg4CmE1SUsva3FOUUdxZDZxcno2R3BF\nckNPbWt2YjBsNlZWN2plRHpUQ3kyTVEKLS0tIE5JU0wxZXo4ZkRIUUVRaTFHSVpB\nUmNDeTdremZ3czVERmhzQlRPczVsZmcKZRrPzTLItkB3PKl79501hlcOGQv/E2QJ\nlbkl4suMOg5voxcgPNDkOjrgGAbEFVTYdleUcdF18UNBF/X+g/hXgg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-08-11T12:50:16Z",
"mac": "ENC[AES256_GCM,data:59QrLTgiBAbBVciy/eeLEC9FbKs3RAWobUhQLTuMp1Q7+tmL+RYMaN09TmNQAvsX+GCdcXnvpRsDG75/Yelq71N1GkRoloBIpccYuOV6IuIICrI1V+TkC3sKHK81giVoyk24VwYouncuEDE7BBEgfP6NqC+KMxPWqHdQKuNHlD0=,iv:np4j5p11v8bYBP2iiqLO8gnaodJy1Sb/fQ8QpTKmT/w=,tag:y2itnrczFFVNuOkrrX2xFA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -101,6 +101,7 @@ nav:
- reference/clanServices/garage.md - reference/clanServices/garage.md
- reference/clanServices/hello-world.md - reference/clanServices/hello-world.md
- reference/clanServices/importer.md - reference/clanServices/importer.md
- reference/clanServices/matrix-synapse.md
- reference/clanServices/mycelium.md - reference/clanServices/mycelium.md
- reference/clanServices/packages.md - reference/clanServices/packages.md
- reference/clanServices/sshd.md - reference/clanServices/sshd.md