feat: matrix: s3 object storage
This commit is contained in:
@@ -10,7 +10,14 @@
|
||||
...
|
||||
}: let
|
||||
cfg = config.hectic.services.matrix;
|
||||
s3Cfg = cfg.objectStorage.s3;
|
||||
|
||||
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
|
||||
user = cfg.users.${name};
|
||||
adminFlag = if user.admin then "--admin" else "--no-admin";
|
||||
@@ -27,10 +34,58 @@
|
||||
${adminFlag} \
|
||||
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 {
|
||||
options = {
|
||||
hectic.services.matrix = {
|
||||
enable = lib.mkEnableOption "Matrix Synapse homeserver with PostgreSQL and nginx";
|
||||
|
||||
secretsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = ''
|
||||
@@ -42,6 +97,7 @@ in {
|
||||
form_secret
|
||||
'';
|
||||
};
|
||||
|
||||
postgresql = {
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
@@ -50,6 +106,7 @@ in {
|
||||
postgres port
|
||||
'';
|
||||
};
|
||||
|
||||
initialEnvFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = ''
|
||||
@@ -60,12 +117,14 @@ in {
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
matrixDomain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = ''
|
||||
domain to matrix
|
||||
'';
|
||||
};
|
||||
|
||||
maxUploadSize = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "100M";
|
||||
@@ -73,6 +132,131 @@ in {
|
||||
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 {
|
||||
type = lib.types.attrsOf (lib.types.submodule {
|
||||
options = {
|
||||
@@ -82,6 +266,7 @@ in {
|
||||
Full path to a file containing the Matrix user's password.
|
||||
'';
|
||||
};
|
||||
|
||||
admin = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
@@ -98,166 +283,202 @@ in {
|
||||
};
|
||||
};
|
||||
};
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.matrix-synapse = {
|
||||
enable = true;
|
||||
extraConfigFiles = [
|
||||
cfg.secretsFile
|
||||
];
|
||||
settings = {
|
||||
|
||||
config = lib.mkMerge [
|
||||
(lib.mkIf cfg.enable {
|
||||
services.matrix-synapse = {
|
||||
enable = true;
|
||||
plugins = lib.optional s3Cfg.enable s3Plugin;
|
||||
extraConfigFiles = [
|
||||
cfg.secretsFile
|
||||
] ++ lib.optional s3Cfg.enable s3ConfigFile;
|
||||
|
||||
settings = {
|
||||
server_name = cfg.matrixDomain;
|
||||
public_baseurl = "https://${cfg.matrixDomain}";
|
||||
max_upload_size = cfg.maxUploadSize;
|
||||
media_store_path = lib.mkIf s3Cfg.enable s3Cfg.mediaStorePath;
|
||||
|
||||
experimental_features = {
|
||||
msc3266_enabled = true;
|
||||
msc4140_enabled = true;
|
||||
msc4143_enabled = true;
|
||||
msc4222_enabled = true;
|
||||
};
|
||||
matrix_rtc = {
|
||||
transports = [
|
||||
{
|
||||
type = "livekit";
|
||||
livekit_service_url = "https://${cfg.matrixDomain}/livekit/jwt";
|
||||
}
|
||||
];
|
||||
};
|
||||
listeners = [
|
||||
{
|
||||
port = 8008;
|
||||
bind_addresses = [ "0.0.0.0" ];
|
||||
type = "http";
|
||||
tls = false;
|
||||
resources = [
|
||||
msc4143_enabled = true;
|
||||
msc4222_enabled = true;
|
||||
};
|
||||
|
||||
matrix_rtc = {
|
||||
transports = [
|
||||
{
|
||||
names = [
|
||||
"client"
|
||||
# Ability speak between different matrix servers and get
|
||||
# global id, requires .well-known
|
||||
"federation"
|
||||
"openid"
|
||||
];
|
||||
compress = false;
|
||||
}
|
||||
];
|
||||
type = "livekit";
|
||||
livekit_service_url = "https://${cfg.matrixDomain}/livekit/jwt";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
listeners = [
|
||||
{
|
||||
port = 8008;
|
||||
bind_addresses = [ "0.0.0.0" ];
|
||||
type = "http";
|
||||
tls = false;
|
||||
resources = [
|
||||
{
|
||||
names = [
|
||||
"client"
|
||||
# Ability speak between different matrix servers and get
|
||||
# global id, requires .well-known
|
||||
"federation"
|
||||
"openid"
|
||||
];
|
||||
compress = false;
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
enable_registration = true;
|
||||
enable_registration_without_verification = true;
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [
|
||||
pkgs.matrix-synapse
|
||||
];
|
||||
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
package = pkgs.postgresql_17;
|
||||
|
||||
initdbArgs = [
|
||||
"--locale=C"
|
||||
"--encoding=UTF8"
|
||||
];
|
||||
|
||||
enableTCPIP = true;
|
||||
settings.port = cfg.postgresql.port;
|
||||
authentication = builtins.concatStringsSep "\n" [
|
||||
"local all all trust"
|
||||
"host sameuser all 127.0.0.1/32 scram-sha-256"
|
||||
"host sameuser all ::1/128 scram-sha-256"
|
||||
"host all all ::1/128 scram-sha-256"
|
||||
"host all all 0.0.0.0/0 scram-sha-256"
|
||||
|
||||
"host replication postgres 127.0.0.1/32 scram-sha-256"
|
||||
"host replication postgres ::1/128 scram-sha-256"
|
||||
];
|
||||
|
||||
settings = {
|
||||
wal_level = "replica";
|
||||
max_wal_senders = 10;
|
||||
};
|
||||
|
||||
ensureUsers = [
|
||||
{
|
||||
name = "matrix-synapse";
|
||||
ensureClauses.login = true;
|
||||
ensureDBOwnership = true;
|
||||
}
|
||||
];
|
||||
|
||||
enable_registration = true;
|
||||
enable_registration_without_verification = true;
|
||||
ensureDatabases = [
|
||||
"matrix-synapse"
|
||||
];
|
||||
|
||||
};
|
||||
};
|
||||
initialScript = pkgs.writeText "init-sql-script" ''
|
||||
-- setup password from env/sops
|
||||
DO $$#!${pkgs.dash}/bin/dash
|
||||
set -e
|
||||
. ${cfg.postgresql.initialEnvFile}
|
||||
psql -Atc "ALTER USER postgres WITH PASSWORD '$POSTGRESQL_PASSWORD'";
|
||||
$$ LANGUAGE plsh;
|
||||
|
||||
environment.systemPackages = [
|
||||
pkgs.matrix-synapse
|
||||
];
|
||||
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
package = pkgs.postgresql_17;
|
||||
|
||||
initdbArgs = [
|
||||
"--locale=C"
|
||||
"--encoding=UTF8"
|
||||
];
|
||||
|
||||
enableTCPIP = true;
|
||||
settings.port = cfg.postgresql.port;
|
||||
authentication = builtins.concatStringsSep "\n" [
|
||||
"local all all trust"
|
||||
"host sameuser all 127.0.0.1/32 scram-sha-256"
|
||||
"host sameuser all ::1/128 scram-sha-256"
|
||||
"host all all ::1/128 scram-sha-256"
|
||||
"host all all 0.0.0.0/0 scram-sha-256"
|
||||
|
||||
"host replication postgres 127.0.0.1/32 scram-sha-256"
|
||||
"host replication postgres ::1/128 scram-sha-256"
|
||||
];
|
||||
|
||||
settings = {
|
||||
wal_level = "replica";
|
||||
max_wal_senders = 10;
|
||||
CREATE ROLE myuser LOGIN PASSWORD 'matrix-synapse';
|
||||
'';
|
||||
};
|
||||
|
||||
ensureUsers = [
|
||||
{
|
||||
name = "matrix-synapse";
|
||||
ensureClauses.login = true;
|
||||
ensureDBOwnership = true;
|
||||
}
|
||||
];
|
||||
|
||||
ensureDatabases = [
|
||||
"matrix-synapse"
|
||||
];
|
||||
|
||||
initialScript = pkgs.writeText "init-sql-script" ''
|
||||
-- setup password from env/sops
|
||||
DO $$#!${pkgs.dash}/bin/dash
|
||||
set -e
|
||||
. ${cfg.postgresql.initialEnvFile}
|
||||
psql -Atc "ALTER USER postgres WITH PASSWORD '$POSTGRESQL_PASSWORD'";
|
||||
$$ LANGUAGE plsh;
|
||||
|
||||
CREATE ROLE myuser LOGIN PASSWORD 'matrix-synapse';
|
||||
'';
|
||||
};
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts.${cfg.matrixDomain} = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:8008";
|
||||
extraConfig = ''
|
||||
client_max_body_size ${cfg.maxUploadSize};
|
||||
'';
|
||||
};
|
||||
locations."=/.well-known/matrix/server" = {
|
||||
extraConfig = ''
|
||||
default_type application/json;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
|
||||
add_header Access-Control-Allow-Headers "X-Requested-With, Content-Type, Authorization";
|
||||
'';
|
||||
return = "200 '{\"m.server\": \"${cfg.matrixDomain}:443\"}'";
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts.${cfg.matrixDomain} = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:8008";
|
||||
extraConfig = ''
|
||||
client_max_body_size ${cfg.maxUploadSize};
|
||||
'';
|
||||
};
|
||||
locations."=/.well-known/matrix/server" = {
|
||||
extraConfig = ''
|
||||
default_type application/json;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
|
||||
add_header Access-Control-Allow-Headers "X-Requested-With, Content-Type, Authorization";
|
||||
'';
|
||||
return = "200 '{\"m.server\": \"${cfg.matrixDomain}:443\"}'";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
security.acme = {
|
||||
acceptTerms = true;
|
||||
defaults = {
|
||||
email = "hectic.yukkop.it@gmail.com";
|
||||
enableDebugLogs = true;
|
||||
security.acme = {
|
||||
acceptTerms = true;
|
||||
defaults = {
|
||||
email = "hectic.yukkop.it@gmail.com";
|
||||
enableDebugLogs = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
systemd.services.matrix-synapse-users = lib.mkIf (matrixUsers != []) {
|
||||
description = "Provision Matrix Synapse users";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ config.services.matrix-synapse.serviceUnit ];
|
||||
requires = [ config.services.matrix-synapse.serviceUnit ];
|
||||
path = with pkgs; [ curl coreutils gawk ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "matrix-synapse";
|
||||
};
|
||||
script = ''
|
||||
until curl -sf http://127.0.0.1:8008/_matrix/client/versions >/dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
REGISTRATION_SHARED_SECRET="$(awk -F': *' '$1 == "registration_shared_secret" { print $2; exit }' "${cfg.secretsFile}")"
|
||||
systemd.services.matrix-synapse-users = lib.mkIf (matrixUsers != []) {
|
||||
description = "Provision Matrix Synapse users";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ config.services.matrix-synapse.serviceUnit ];
|
||||
requires = [ config.services.matrix-synapse.serviceUnit ];
|
||||
path = with pkgs; [ curl coreutils gawk ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "matrix-synapse";
|
||||
};
|
||||
script = ''
|
||||
until curl -sf http://127.0.0.1:8008/_matrix/client/versions >/dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ -z "$REGISTRATION_SHARED_SECRET" ]; then
|
||||
printf 'registration_shared_secret not found in %s\n' '${cfg.secretsFile}' >&2
|
||||
exit 1
|
||||
fi
|
||||
REGISTRATION_SHARED_SECRET="$(awk -F': *' '$1 == "registration_shared_secret" { print $2; exit }' "${cfg.secretsFile}")"
|
||||
|
||||
if [ -z "$REGISTRATION_SHARED_SECRET" ]; then
|
||||
printf 'registration_shared_secret not found in %s\n' '${cfg.secretsFile}' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
${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;
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
@@ -75,6 +75,13 @@ in {
|
||||
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;
|
||||
};
|
||||
};
|
||||
@@ -169,6 +176,11 @@ in {
|
||||
key = "matrix/users/lvgkcfjl/password";
|
||||
owner = "matrix-synapse";
|
||||
};
|
||||
sops.secrets."matrix/object-storage/credentials" = {
|
||||
key = "matrix/object-storage/credentials";
|
||||
owner = "matrix-synapse";
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
services.mailserver = {
|
||||
enable = true;
|
||||
|
||||
Reference in New Issue
Block a user