feat: matrix: s3 object storage

This commit is contained in:
2026-05-23 09:59:34 +00:00
parent d86eeae4a5
commit ef59f14372
3 changed files with 377 additions and 142 deletions

View File

@@ -10,7 +10,14 @@
... ...
}: let }: let
cfg = config.hectic.services.matrix; cfg = config.hectic.services.matrix;
s3Cfg = cfg.objectStorage.s3;
matrixUsers = builtins.attrNames cfg.users; matrixUsers = builtins.attrNames cfg.users;
s3Plugin = pkgs.matrix-synapse-plugins.matrix-synapse-s3-storage-provider;
s3ConfigDir = "/run/matrix-synapse";
s3ConfigFile = "${s3ConfigDir}/s3-media-storage.yaml";
mkUserRegistration = name: let mkUserRegistration = name: let
user = cfg.users.${name}; user = cfg.users.${name};
adminFlag = if user.admin then "--admin" else "--no-admin"; adminFlag = if user.admin then "--admin" else "--no-admin";
@@ -27,10 +34,58 @@
${adminFlag} \ ${adminFlag} \
http://127.0.0.1:8008 || true http://127.0.0.1:8008 || true
''; '';
mkS3Config = ''
if [ ! -r "${s3Cfg.credentialsFile}" ]; then
printf 'Missing Matrix object storage credentials file: %s\n' '${s3Cfg.credentialsFile}' >&2
exit 1
fi
. "${s3Cfg.credentialsFile}"
if [ -z "$ACCESS_KEY_ID" ] || [ -z "$SECRET_ACCESS_KEY" ]; then
printf 'ACCESS_KEY_ID or SECRET_ACCESS_KEY missing in %s\n' '${s3Cfg.credentialsFile}' >&2
exit 1
fi
mkdir -p "${s3ConfigDir}"
cat > "${s3ConfigFile}" <<EOF
media_storage_providers:
- module: s3_storage_provider.S3StorageProviderBackend
store_local: ${lib.boolToString s3Cfg.storeLocal}
store_remote: ${lib.boolToString s3Cfg.storeRemote}
store_synchronous: ${lib.boolToString s3Cfg.storeSynchronous}
config:
bucket: ${s3Cfg.bucket}
endpoint_url: ${s3Cfg.endpointUrl}
region_name: ${s3Cfg.regionName}
prefix: "${s3Cfg.prefix}"
storage_class: "${s3Cfg.storageClass}"
threadpool_size: ${toString s3Cfg.threadpoolSize}
access_key_id: $ACCESS_KEY_ID
secret_access_key: $SECRET_ACCESS_KEY
EOF
chown matrix-synapse:matrix-synapse "${s3ConfigFile}"
chmod 0400 "${s3ConfigFile}"
'';
mkS3SyncScript = ''
${s3Plugin}/bin/s3_media_upload write
${s3Plugin}/bin/s3_media_upload upload "${s3Cfg.mediaStorePath}" "${s3Cfg.bucket}" \
--endpoint-url "${s3Cfg.endpointUrl}" \
--storage-class "${s3Cfg.storageClass}" \
${lib.optionalString (s3Cfg.prefix != "") "--prefix \"${s3Cfg.prefix}\" \\"}
${lib.optionalString s3Cfg.sync.deleteLocalAfterUpload "--delete"}
${s3Plugin}/bin/s3_media_upload update-db 0s
${s3Plugin}/bin/s3_media_upload check-deleted "${s3Cfg.mediaStorePath}"
'';
in { in {
options = { options = {
hectic.services.matrix = { hectic.services.matrix = {
enable = lib.mkEnableOption "Matrix Synapse homeserver with PostgreSQL and nginx"; enable = lib.mkEnableOption "Matrix Synapse homeserver with PostgreSQL and nginx";
secretsFile = lib.mkOption { secretsFile = lib.mkOption {
type = lib.types.path; type = lib.types.path;
description = '' description = ''
@@ -42,6 +97,7 @@ in {
form_secret form_secret
''; '';
}; };
postgresql = { postgresql = {
port = lib.mkOption { port = lib.mkOption {
type = lib.types.port; type = lib.types.port;
@@ -50,6 +106,7 @@ in {
postgres port postgres port
''; '';
}; };
initialEnvFile = lib.mkOption { initialEnvFile = lib.mkOption {
type = lib.types.path; type = lib.types.path;
description = '' description = ''
@@ -60,12 +117,14 @@ in {
''; '';
}; };
}; };
matrixDomain = lib.mkOption { matrixDomain = lib.mkOption {
type = lib.types.str; type = lib.types.str;
description = '' description = ''
domain to matrix domain to matrix
''; '';
}; };
maxUploadSize = lib.mkOption { maxUploadSize = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "100M"; default = "100M";
@@ -73,6 +132,131 @@ in {
Maximum file upload size accepted by Synapse and nginx. Maximum file upload size accepted by Synapse and nginx.
''; '';
}; };
objectStorage.s3 = {
enable = lib.mkEnableOption "S3-compatible object storage for Matrix media";
bucket = lib.mkOption {
type = lib.types.str;
description = ''
Bucket name used for Matrix media objects.
'';
};
regionName = lib.mkOption {
type = lib.types.str;
description = ''
Region name passed to the Synapse S3 storage provider.
'';
};
endpointUrl = lib.mkOption {
type = lib.types.str;
description = ''
S3-compatible endpoint URL.
'';
};
credentialsFile = lib.mkOption {
type = lib.types.path;
description = ''
Path to an env-style file containing:
ACCESS_KEY_ID=
SECRET_ACCESS_KEY=
'';
};
mediaStorePath = lib.mkOption {
type = lib.types.str;
default = "/var/lib/matrix-synapse/media_store";
description = ''
Local Synapse media store path used before upload to object storage.
'';
};
prefix = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Optional object key prefix inside the bucket.
'';
};
storageClass = lib.mkOption {
type = lib.types.str;
default = "STANDARD";
description = ''
Storage class passed to the upload tool.
'';
};
threadpoolSize = lib.mkOption {
type = lib.types.int;
default = 40;
description = ''
Worker pool size for the Synapse S3 storage provider.
'';
};
storeLocal = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Mirror local uploads to object storage.
'';
};
storeRemote = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Mirror remotely-fetched media to object storage.
'';
};
storeSynchronous = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Wait for object storage upload before completing the client request.
'';
};
sync = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Periodically migrate older local media to object storage.
'';
};
olderThan = lib.mkOption {
type = lib.types.str;
default = "1d";
description = ''
Age threshold passed to `s3_media_upload update`.
'';
};
onCalendar = lib.mkOption {
type = lib.types.str;
default = "hourly";
description = ''
systemd timer schedule for media sync.
'';
};
deleteLocalAfterUpload = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Remove local media after successful object storage upload.
'';
};
};
};
users = lib.mkOption { users = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule { type = lib.types.attrsOf (lib.types.submodule {
options = { options = {
@@ -82,6 +266,7 @@ in {
Full path to a file containing the Matrix user's password. Full path to a file containing the Matrix user's password.
''; '';
}; };
admin = lib.mkOption { admin = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
@@ -98,22 +283,29 @@ in {
}; };
}; };
}; };
config = lib.mkIf cfg.enable {
config = lib.mkMerge [
(lib.mkIf cfg.enable {
services.matrix-synapse = { services.matrix-synapse = {
enable = true; enable = true;
plugins = lib.optional s3Cfg.enable s3Plugin;
extraConfigFiles = [ extraConfigFiles = [
cfg.secretsFile cfg.secretsFile
]; ] ++ lib.optional s3Cfg.enable s3ConfigFile;
settings = { settings = {
server_name = cfg.matrixDomain; server_name = cfg.matrixDomain;
public_baseurl = "https://${cfg.matrixDomain}"; public_baseurl = "https://${cfg.matrixDomain}";
max_upload_size = cfg.maxUploadSize; max_upload_size = cfg.maxUploadSize;
media_store_path = lib.mkIf s3Cfg.enable s3Cfg.mediaStorePath;
experimental_features = { experimental_features = {
msc3266_enabled = true; msc3266_enabled = true;
msc4140_enabled = true; msc4140_enabled = true;
msc4143_enabled = true; msc4143_enabled = true;
msc4222_enabled = true; msc4222_enabled = true;
}; };
matrix_rtc = { matrix_rtc = {
transports = [ transports = [
{ {
@@ -122,6 +314,7 @@ in {
} }
]; ];
}; };
listeners = [ listeners = [
{ {
port = 8008; port = 8008;
@@ -145,7 +338,6 @@ in {
enable_registration = true; enable_registration = true;
enable_registration_without_verification = true; enable_registration_without_verification = true;
}; };
}; };
@@ -234,6 +426,7 @@ in {
enableDebugLogs = true; enableDebugLogs = true;
}; };
}; };
systemd.services.matrix-synapse-users = lib.mkIf (matrixUsers != []) { systemd.services.matrix-synapse-users = lib.mkIf (matrixUsers != []) {
description = "Provision Matrix Synapse users"; description = "Provision Matrix Synapse users";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
@@ -259,5 +452,33 @@ in {
${builtins.concatStringsSep "\n" (map mkUserRegistration matrixUsers)} ${builtins.concatStringsSep "\n" (map mkUserRegistration matrixUsers)}
''; '';
}; };
})
(lib.mkIf (cfg.enable && s3Cfg.enable) {
systemd.services.matrix-synapse-s3-config = {
description = "Generate Synapse S3 media storage config";
before = [ config.services.matrix-synapse.serviceUnit ];
requiredBy = [ config.services.matrix-synapse.serviceUnit ];
serviceConfig.Type = "oneshot";
script = mkS3Config;
}; };
systemd.services.matrix-synapse-s3-media-sync = lib.mkIf s3Cfg.sync.enable {
description = "Sync Matrix media to S3-compatible object storage";
after = [ config.services.matrix-synapse.serviceUnit ];
wants = [ config.services.matrix-synapse.serviceUnit ];
serviceConfig = {
Type = "oneshot";
User = "matrix-synapse";
WorkingDirectory = "/var/lib/matrix-synapse";
};
script = mkS3SyncScript;
};
systemd.timers.matrix-synapse-s3-media-sync = lib.mkIf s3Cfg.sync.enable {
wantedBy = [ "timers.target" ];
timerConfig.OnCalendar = s3Cfg.sync.onCalendar;
};
})
];
} }

View File

@@ -75,6 +75,13 @@ in {
passwordFile = config.sops.secrets."matrix/users/lvgkcfjl/password".path; passwordFile = config.sops.secrets."matrix/users/lvgkcfjl/password".path;
}; };
}; };
objectStorage.s3 = {
enable = true;
bucket = "matrix-hectic-lab";
regionName = "hel1";
endpointUrl = "https://hel1.your-objectstorage.com";
credentialsFile = config.sops.secrets."matrix/object-storage/credentials".path;
};
inherit matrixDomain; inherit matrixDomain;
}; };
}; };
@@ -169,6 +176,11 @@ in {
key = "matrix/users/lvgkcfjl/password"; key = "matrix/users/lvgkcfjl/password";
owner = "matrix-synapse"; owner = "matrix-synapse";
}; };
sops.secrets."matrix/object-storage/credentials" = {
key = "matrix/object-storage/credentials";
owner = "matrix-synapse";
mode = "0400";
};
services.mailserver = { services.mailserver = {
enable = true; enable = true;

View File

@@ -13,6 +13,8 @@ mailserver:
hashedPassword: ENC[AES256_GCM,data:6Rgj4JIrEF9ZRRRwGpV4yCdS7cw81xKLfavuii1cHqZK3JDlD2HOAVYgrrl+fWD6rNxUPAXpVuAIgxCu,iv:Y67je0qtEpnbwhiYXL2FJUAedPlKdTTb6wGeSVVEaPQ=,tag:Thvt+gsebEjoIjwOmNgBGQ==,type:str] hashedPassword: ENC[AES256_GCM,data:6Rgj4JIrEF9ZRRRwGpV4yCdS7cw81xKLfavuii1cHqZK3JDlD2HOAVYgrrl+fWD6rNxUPAXpVuAIgxCu,iv:Y67je0qtEpnbwhiYXL2FJUAedPlKdTTb6wGeSVVEaPQ=,tag:Thvt+gsebEjoIjwOmNgBGQ==,type:str]
init-postgresql: ENC[AES256_GCM,data:Iw8M2P1QoqPVaEdM8Zo0qlHrYgop0iknDY4NtgDo,iv:RWj9AFnh4/KWCm3UH4RoCdM2lzsXGY7A7qko8xCxjp8=,tag:l8acSq8+NBXB4L1rVzG6kw==,type:str] init-postgresql: ENC[AES256_GCM,data:Iw8M2P1QoqPVaEdM8Zo0qlHrYgop0iknDY4NtgDo,iv:RWj9AFnh4/KWCm3UH4RoCdM2lzsXGY7A7qko8xCxjp8=,tag:l8acSq8+NBXB4L1rVzG6kw==,type:str]
matrix: matrix:
object-storage:
credentials: ENC[AES256_GCM,data:n2sDhGMR8y0in9pdn4zNEQBC5dqk+4JwbuJgEeQxyjn8bL9GebFBaqeE+frvPAGXj/DgpU6lFlFPgaGTWaMZAEEVpXyFeOdODpgW049q83ug5e4j/mbZgFM36XoItw==,iv:MW9H0zASdrY7SX1XM/jfoBihBYX0Fmlew4f71AvvV6Y=,tag:cAiOKUtOeTnczudps8YgQw==,type:str]
secrets: ENC[AES256_GCM,data:ivXp2YSiMI4hgL6122Ex+fGW0lsZvGD6XmiRvNgFgvzLH5yDv9uLsYcGCTYfQSL3X5VyIMGvsdRF+4pbIjBZMuQKrjvXv74E7aFBLQ2Qk98N3IIrznUFR3KXbHR6xXy5ILd7Bmw5JI/ZHULbmITahXUBt2kEJvfh4eAtqShNA4vsJrabHX9A8Q+2Ddp16w0cWftV5++WXzlNpvIc2Py6BwvfroNAjpSaO+ILYDOIL7XjPvF83fTt64pxZ9nsi3hCzcDtBgGkqc8=,iv:wvt9V2uYQUwivSwEIYZwcHjXr5WwMw19lgFDIa1CcVw=,tag:/22UZvp7+1hLbt+kV+wokQ==,type:str] secrets: ENC[AES256_GCM,data:ivXp2YSiMI4hgL6122Ex+fGW0lsZvGD6XmiRvNgFgvzLH5yDv9uLsYcGCTYfQSL3X5VyIMGvsdRF+4pbIjBZMuQKrjvXv74E7aFBLQ2Qk98N3IIrznUFR3KXbHR6xXy5ILd7Bmw5JI/ZHULbmITahXUBt2kEJvfh4eAtqShNA4vsJrabHX9A8Q+2Ddp16w0cWftV5++WXzlNpvIc2Py6BwvfroNAjpSaO+ILYDOIL7XjPvF83fTt64pxZ9nsi3hCzcDtBgGkqc8=,iv:wvt9V2uYQUwivSwEIYZwcHjXr5WwMw19lgFDIa1CcVw=,tag:/22UZvp7+1hLbt+kV+wokQ==,type:str]
users: users:
yukkop: yukkop:
@@ -83,7 +85,7 @@ sops:
Yk43ZmlTc09aNFV1VjdjN2RWQlFWTDQKcYSvA2lHP8GS0lkYY19Tm8RXmFHQX5Ck Yk43ZmlTc09aNFV1VjdjN2RWQlFWTDQKcYSvA2lHP8GS0lkYY19Tm8RXmFHQX5Ck
qV2Fn22Fic4M5FVKDEMfaO6WmeXgki9a8dGeO9LlC+Phf16SOq7eLw== qV2Fn22Fic4M5FVKDEMfaO6WmeXgki9a8dGeO9LlC+Phf16SOq7eLw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2026-05-23T07:08:25Z" lastmodified: "2026-05-23T09:54:16Z"
mac: ENC[AES256_GCM,data:D7V2jX6AK89s/TRjrNXBHHyJHIbp+OQSzV2XfZ3qUOxJKfrNvNes8tARGO7fF1OMdbnZJVC7VBCMVOg0UtN6UlLepF9lL/jYzKPbPO6ohhhTdgv4OUkiTNVZ6MGQOY2win9NiHoAhn6zdNw8bZeXNPN5D7eY+Spy6zXLvjrl2EY=,iv:CoVVHHmFga/ecqP0KNp0Gy6rx08SuqImNuc6zc0JGpU=,tag:s4tJBGtBUjRyDCkPXqaQAQ==,type:str] mac: ENC[AES256_GCM,data:n3ljtQmDWFbhYo8eXjxPpE6xi/HIAMHaT1SATANIHi2Ged0c5PI/YS8jodRPUdb4zgLSMtoJjX5bqfJZ1en4WBY1rg69hDXst2sZ42zmDkafGTFpPBoS+QtC1c9UYQwvJfJ+4fX5qIcO6wplhIZG5PZrqb2dpPUD4vkRNxez2ho=,iv:ny5RCC7/3bNTiR2V4Hn3DuLdn8lThZeUKoCwXrzL0V0=,tag:v6THc2C4w7Uujh1VLKcokw==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.10.2 version: 3.10.2