Files
sp-mastodon-module/module.nix

228 lines
7.0 KiB
Nix
Raw Normal View History

2025-09-11 17:30:40 +03:00
{
config,
lib,
pkgs,
...
}:
let
sp = config.selfprivacy;
cfg = sp.modules.mastodon;
oauthClientID = "mastodon";
auth-passthru = config.selfprivacy.passthru.auth;
2025-09-12 15:26:28 +03:00
oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID;
2025-09-11 17:30:40 +03:00
issuer = lib.strings.removeSuffix "/.well-known/openid-configuration" oauthDiscoveryURL;
2025-09-20 12:46:40 +03:00
oauthRedirectURL = "https://${cfg.subdomain}.${sp.domain}/auth/auth/openid_connect/callback";
2025-09-11 17:30:40 +03:00
usersGroup = "sp.mastodon.users";
2025-09-11 18:08:18 +03:00
adminsGroup = "sp.mastodon.admins";
2025-09-11 17:30:40 +03:00
oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP oauthClientID;
2025-09-20 12:46:40 +03:00
serviceAccountFP = auth-passthru.mkServiceAccountTokenFP "mastodon";
2025-09-20 00:53:43 +03:00
secrets = rec {
dir = "/run/keys/mastodon";
hashedPasswordFile = "${dir}/hashed_email_password";
passwordFile = "${dir}/email_password";
};
2025-09-11 17:30:40 +03:00
in
{
options.selfprivacy.modules.mastodon = {
enable =
(lib.mkOption {
default = false;
type = lib.types.bool;
description = "Enable Mastodon";
})
// {
meta = {
type = "enable";
};
};
location =
(lib.mkOption {
type = lib.types.str;
description = "Mastodon location";
})
// {
meta = {
type = "location";
};
};
subdomain =
(lib.mkOption {
default = "mastodon";
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
2025-09-13 10:31:28 +03:00
description = "Subdomain (changing subdomain after setting up will cause breakage of the server!)";
2025-09-11 17:30:40 +03:00
})
// {
meta = {
widget = "subdomain";
type = "string";
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
weight = 0;
};
};
2025-09-20 12:46:40 +03:00
dissallowUnauthenticatedAPI =
(lib.mkOption {
default = true;
type = lib.types.bool;
description = "Allow unauthenticated API access";
})
// {
meta = {
type = "bool";
weight = 1;
};
};
2025-09-11 17:30:40 +03:00
};
config = lib.mkIf cfg.enable {
fileSystems = lib.mkIf sp.useBinds {
"/var/lib/mastodon" = {
device = "/volumes/${cfg.location}/mastodon";
options = [ "bind" ];
};
};
services.mastodon = {
enable = true;
localDomain = "${cfg.subdomain}.${sp.domain}";
enableUnixSocket = false;
configureNginx = true;
database.createLocally = true;
2025-09-11 18:20:50 +03:00
streamingProcesses = 3;
2025-09-11 22:38:06 +03:00
smtp = {
createLocally = false;
2025-09-13 10:31:28 +03:00
2025-09-20 00:53:43 +03:00
fromAddress = "mastodon@${sp.domain}";
user = "mastodon";
passwordFile = secrets.passwordFile;
authenticate = true;
2025-09-13 10:31:28 +03:00
2025-09-13 03:06:36 +03:00
host = "hollowness.top";
2025-09-13 01:39:15 +03:00
port = 465;
2025-09-11 22:38:06 +03:00
};
2025-09-12 23:36:48 +03:00
extraConfig = {
2025-09-13 02:59:37 +03:00
# "SMTP_ENABLE_STARTTLS" = "never";
"SMTP_ENABLE_STARTTLS_AUTO" = "true";
"SMTP_ENABLE_STARTTLS" = "always";
2025-09-13 02:47:42 +03:00
"SMTP_TLS" = "true";
2025-09-13 02:38:29 +03:00
"SMTP_SSL" = "true";
2025-09-20 12:46:40 +03:00
"DISALLOW_UNAUTHENTICATED_API_ACCESS" = lib.boolToString cfg.dissallowUnauthenticatedAPI;
2025-09-12 23:36:48 +03:00
};
2025-09-11 17:30:40 +03:00
};
2025-09-20 02:29:39 +03:00
users.users.mastodon.isSystemUser = lib.mkForce false;
users.users.mastodon.isNormalUser = lib.mkForce true;
2025-09-11 17:30:40 +03:00
2025-09-20 00:09:46 +03:00
selfprivacy.emails."mastodon" = {
2025-09-20 00:53:43 +03:00
hashedPasswordFile = secrets.hashedPasswordFile;
2025-09-16 18:21:51 +03:00
systemdTargets = [ "mastodon-email-password-setup.service" ];
2025-09-20 00:53:43 +03:00
sendOnly = true;
2025-09-16 17:12:44 +03:00
};
2025-09-11 17:30:40 +03:00
systemd = {
2025-09-11 22:38:06 +03:00
services.mastodon-email-password-setup = {
enable = true;
wantedBy = [ "multi-user.target" "mastodon-web.service" "postfix.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = pkgs.writeShellScript "gen-mastodon-email-password" ''
export password=$(head -c 32 /dev/urandom | base64 | sed 's/[+=\\/A-Z]//g')
2025-09-20 00:53:43 +03:00
mkdir ${secrets.dir} || true # Create ${secrets.dir} if it doesn't exist
2025-09-11 22:38:06 +03:00
2025-09-20 00:53:43 +03:00
rm -f ${secrets.passwordFile} || true
echo "$password" > ${secrets.passwordFile}
chmod 400 ${secrets.passwordFile}
chown ${config.services.mastodon.user}:${config.services.mastodon.group} ${secrets.passwordFile}
2025-09-11 22:38:06 +03:00
2025-09-12 20:58:32 +03:00
export hashedPassword=$(${lib.getExe pkgs.mkpasswd} -sm bcrypt "$password")
2025-09-20 00:53:43 +03:00
rm -f ${secrets.hashedPasswordFile} || true
echo "$hashedPassword" > ${secrets.hashedPasswordFile}
chmod 440 ${secrets.hashedPasswordFile}
chown ${config.services.postfix.user}:${config.services.postfix.group} ${secrets.hashedPasswordFile}
2025-09-11 22:38:06 +03:00
'';
};
};
2025-09-23 15:53:51 +03:00
services.mastodon-kanidm-sync = {
after = [
# "mastodon.service" # TODO: ??
2025-09-23 16:14:53 +03:00
"postgresql.service"
2025-09-23 15:53:51 +03:00
"kanidm.service"
];
requires = [
"kanidm.service"
2025-09-23 16:14:53 +03:00
"postgresql.service"
2025-09-23 15:53:51 +03:00
];
wantedBy = [ "multi-user.target" ];
environment = let db = config.services.mastodon.database;
in {
KANIDM_URL = config.services.kanidm.serverSettings.origin;
KANIDM_TOKEN_PATH = serviceAccountFP;
POSTGRES_DBNAME = db.name;
POSTGRES_USER = db.user;
POSTGRES_HOST = db.host;
};
serviceConfig = {
Slice = "mastodon.slice";
User = "mastodon";
Group = "mastodon";
LoadCredential = [ "kanidm-token:${serviceAccountFP}" ];
ExecStart = pkgs.writers.writePython3 "mas-kanidm-sync" {
doCheck = false;
libraries = with pkgs.python3Packages; [
requests
psycopg
];
2025-09-23 16:00:23 +03:00
} (builtins.readFile ./mastodon-kanidm-sync.py);
2025-09-23 15:53:51 +03:00
};
};
2025-09-11 18:51:52 +03:00
services.mastodon-web = {
2025-09-11 17:30:40 +03:00
unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/mastodon";
serviceConfig = {
2025-09-12 19:46:34 +03:00
Slice = "hedgedoc.slice";
2025-09-12 16:46:24 +03:00
LoadCredential = ["client-secret:${oauthClientSecretFP}"];
2025-09-12 15:05:42 +03:00
ExecStart = lib.mkForce (pkgs.writeShellScript "run-mastodon-with-client-secret" ''
2025-09-12 19:16:33 +03:00
export OIDC_CLIENT_SECRET=$(cat $CREDENTIALS_DIRECTORY/client-secret)
${config.services.mastodon.package}/bin/puma -C config/puma.rb
2025-09-12 15:05:42 +03:00
'');
2025-09-11 17:30:40 +03:00
};
environment = {
2025-09-12 14:42:50 +03:00
OIDC_ENABLED = "true";
2025-09-11 17:30:40 +03:00
OIDC_DISPLAY_NAME= "Kanidm";
2025-09-12 18:12:53 +03:00
OIDC_ISSUER = issuer;
2025-09-12 14:42:50 +03:00
OIDC_DISCOVERY = "true";
2025-09-12 20:58:32 +03:00
OIDC_SCOPE = "openid,profile,email";
2025-09-12 19:22:27 +03:00
OIDC_UID_FIELD = "preferred_username";
2025-09-11 17:30:40 +03:00
OIDC_CLIENT_ID = oauthClientID;
OIDC_REDIRECT_URI = oauthRedirectURL;
2025-09-12 20:29:34 +03:00
OIDC_SECURITY_ASSUME_EMAIL_IS_VERIFIED = "false";
2025-09-13 00:20:53 +03:00
};
2025-09-11 17:30:40 +03:00
};
slices.mastodon = {
description = "Mastodon service slice";
};
};
selfprivacy.auth.clients.${oauthClientID} = {
inherit usersGroup;
2025-09-11 18:08:18 +03:00
inherit adminsGroup;
2025-09-20 12:46:40 +03:00
isTokenNeeded = true;
2025-09-11 17:30:40 +03:00
subdomain = cfg.subdomain;
originLanding = "https://${cfg.subdomain}.${sp.domain}/";
originUrl = oauthRedirectURL;
clientSystemdUnits = [ "mastodon.service" ];
2025-09-12 15:40:59 +03:00
enablePkce = false;
2025-09-12 19:46:34 +03:00
useShortPreferredUsername = true;
2025-09-11 17:30:40 +03:00
linuxUserOfClient = "mastodon";
linuxGroupOfClient = "mastodon";
};
};
}