summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMikael Voss <mvs@nyantec.com>2022-09-23 15:54:14 +0200
committerMikael Voss <mvs@nyantec.com>2022-12-27 14:37:00 +0100
commit2490ee906edf4a75624c590350760a66030a863f (patch)
tree9531fe8c383f11c62a68a31c4de18a2372a98287
parentakkoma-emoji/blobs_gg: init at unstable-2019-17-24 (diff)
downloadnixpkgs-2490ee906edf4a75624c590350760a66030a863f.tar.gz
nixos/akkoma: init
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/web-apps/akkoma.md332
-rw-r--r--nixos/modules/services/web-apps/akkoma.nix1086
-rw-r--r--nixos/modules/services/web-apps/akkoma.xml396
4 files changed, 1815 insertions, 0 deletions
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index ac9ce95fe6f3..a1e7cf01882e 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1102,6 +1102,7 @@
./services/video/rtsp-simple-server.nix
./services/video/unifi-video.nix
./services/wayland/cage.nix
+ ./services/web-apps/akkoma.nix
./services/web-apps/alps.nix
./services/web-apps/atlassian/confluence.nix
./services/web-apps/atlassian/crowd.nix
diff --git a/nixos/modules/services/web-apps/akkoma.md b/nixos/modules/services/web-apps/akkoma.md
new file mode 100644
index 000000000000..fc849be0c872
--- /dev/null
+++ b/nixos/modules/services/web-apps/akkoma.md
@@ -0,0 +1,332 @@
+# Akkoma {#module-services-akkoma}
+
+[Akkoma](https://akkoma.dev/) is a lightweight ActivityPub microblogging server forked from Pleroma.
+
+## Service configuration {#modules-services-akkoma-service-configuration}
+
+The Elixir configuration file required by Akkoma is generated automatically from
+[{option}`services.akkoma.config`](options.html#opt-services.akkoma.config). Secrets must be
+included from external files outside of the Nix store by setting the configuration option to
+an attribute set containing the attribute {option}`_secret` – a string pointing to the file
+containing the actual value of the option.
+
+For the mandatory configuration settings these secrets will be generated automatically if the
+referenced file does not exist during startup, unless disabled through
+[{option}`services.akkoma.initSecrets`](options.html#opt-services.akkoma.initSecrets).
+
+The following configuration binds Akkoma to the Unix socket `/run/akkoma/socket`, expecting to
+be run behind a HTTP proxy on `fediverse.example.com`.
+
+
+```nix
+services.akkoma.enable = true;
+services.akkoma.config = {
+ ":pleroma" = {
+ ":instance" = {
+ name = "My Akkoma instance";
+ description = "More detailed description";
+ email = "admin@example.com";
+ registration_open = false;
+ };
+
+ "Pleroma.Web.Endpoint" = {
+ url.host = "fediverse.example.com";
+ };
+ };
+};
+```
+
+Please refer to the [configuration cheat sheet](https://docs.akkoma.dev/stable/configuration/cheatsheet/)
+for additional configuration options.
+
+## User management {#modules-services-akkoma-user-management}
+
+After the Akkoma service is running, the administration utility can be used to
+[manage users](https://docs.akkoma.dev/stable/administration/CLI_tasks/user/). In particular an
+administrative user can be created with
+
+```ShellSession
+$ pleroma_ctl user new <nickname> <email> --admin --moderator --password <password>
+```
+
+## Proxy configuration {#modules-services-akkoma-proxy-configuration}
+
+Although it is possible to expose Akkoma directly, it is common practice to operate it behind an
+HTTP reverse proxy such as nginx.
+
+```nix
+services.akkoma.nginx = {
+ enableACME = true;
+ forceSSL = true;
+};
+
+services.nginx = {
+ enable = true;
+
+ clientMaxBodySize = "16m";
+ recommendedTlsSettings = true;
+ recommendedOptimisation = true;
+ recommendedGzipSettings = true;
+};
+```
+
+Please refer to [](#module-security-acme) for details on how to provision an SSL/TLS certificate.
+
+### Media proxy {#modules-services-akkoma-media-proxy}
+
+Without the media proxy function, Akkoma does not store any remote media like pictures or video
+locally, and clients have to fetch them directly from the source server.
+
+```nix
+# Enable nginx slice module distributed with Tengine
+services.nginx.package = pkgs.tengine;
+
+# Enable media proxy
+services.akkoma.config.":pleroma".":media_proxy" = {
+ enabled = true;
+ proxy_opts.redirect_on_failure = true;
+};
+
+# Adjust the persistent cache size as needed:
+# Assuming an average object size of 128 KiB, around 1 MiB
+# of memory is required for the key zone per GiB of cache.
+# Ensure that the cache directory exists and is writable by nginx.
+services.nginx.commonHttpConfig = ''
+ proxy_cache_path /var/cache/nginx/cache/akkoma-media-cache
+ levels= keys_zone=akkoma_media_cache:16m max_size=16g
+ inactive=1y use_temp_path=off;
+'';
+
+services.akkoma.nginx = {
+ locations."/proxy" = {
+ proxyPass = "http://unix:/run/akkoma/socket";
+
+ extraConfig = ''
+ proxy_cache akkoma_media_cache;
+
+ # Cache objects in slices of 1 MiB
+ slice 1m;
+ proxy_cache_key $host$uri$is_args$args$slice_range;
+ proxy_set_header Range $slice_range;
+
+ # Decouple proxy and upstream responses
+ proxy_buffering on;
+ proxy_cache_lock on;
+ proxy_ignore_client_abort on;
+
+ # Default cache times for various responses
+ proxy_cache_valid 200 1y;
+ proxy_cache_valid 206 301 304 1h;
+
+ # Allow serving of stale items
+ proxy_cache_use_stale error timeout invalid_header updating;
+ '';
+ };
+};
+```
+
+#### Prefetch remote media {#modules-services-akkoma-prefetch-remote-media}
+
+The following example enables the `MediaProxyWarmingPolicy` MRF policy which automatically
+fetches all media associated with a post through the media proxy, as soon as the post is
+received by the instance.
+
+```nix
+services.akkoma.config.":pleroma".":mrf".policies =
+ map (pkgs.formats.elixirConf { }).lib.mkRaw [
+ "Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy"
+];
+```
+
+#### Media previews {#modules-services-akkoma-media-previews}
+
+Akkoma can generate previews for media.
+
+```nix
+services.akkoma.config.":pleroma".":media_preview_proxy" = {
+ enabled = true;
+ thumbnail_max_width = 1920;
+ thumbnail_max_height = 1080;
+};
+```
+
+## Frontend management {#modules-services-akkoma-frontend-management}
+
+Akkoma will be deployed with the `pleroma-fe` and `admin-fe` frontends by default. These can be
+modified by setting
+[{option}`services.akkoma.frontends`](options.html#opt-services.akkoma.frontends).
+
+The following example overrides the primary frontend’s default configuration using a custom
+derivation.
+
+```nix
+services.akkoma.frontends.primary.package = pkgs.runCommand "pleroma-fe" {
+ config = builtins.toJSON {
+ expertLevel = 1;
+ collapseMessageWithSubject = false;
+ stopGifs = false;
+ replyVisibility = "following";
+ webPushHideIfCW = true;
+ hideScopeNotice = true;
+ renderMisskeyMarkdown = false;
+ hideSiteFavicon = true;
+ postContentType = "text/markdown";
+ showNavShortcuts = false;
+ };
+ nativeBuildInputs = with pkgs; [ jq xorg.lndir ];
+ passAsFile = [ "config" ];
+} ''
+ mkdir $out
+ lndir ${pkgs.akkoma-frontends.pleroma-fe} $out
+
+ rm $out/static/config.json
+ jq -s add ${pkgs.akkoma-frontends.pleroma-fe}/static/config.json ${config} \
+ >$out/static/config.json
+'';
+```
+
+## Federation policies {#modules-services-akkoma-federation-policies}
+
+Akkoma comes with a number of modules to police federation with other ActivityPub instances.
+The most valuable for typical users is the
+[`:mrf_simple`](https://docs.akkoma.dev/stable/configuration/cheatsheet/#mrf_simple) module
+which allows limiting federation based on instance hostnames.
+
+This configuration snippet provides an example on how these can be used. Choosing an adequate
+federation policy is not trivial and entails finding a balance between connectivity to the rest
+of the fediverse and providing a pleasant experience to the users of an instance.
+
+
+```nix
+services.akkoma.config.":pleroma" = with (pkgs.formats.elixirConf { }).lib; {
+ ":mrf".policies = map mkRaw [
+ "Pleroma.Web.ActivityPub.MRF.SimplePolicy"
+ ];
+
+ ":mrf_simple" = {
+ # Tag all media as sensitive
+ media_nsfw = mkMap {
+ "nsfw.weird.kinky" = "Untagged NSFW content";
+ };
+
+ # Reject all activities except deletes
+ reject = mkMap {
+ "kiwifarms.cc" = "Persistent harassment of users, no moderation";
+ };
+
+ # Force posts to be visible by followers only
+ followers_only = mkMap {
+ "beta.birdsite.live" = "Avoid polluting timelines with Twitter posts";
+ };
+ };
+};
+```
+
+## Upload filters {#modules-services-akkoma-upload-filters}
+
+This example strips GPS and location metadata from uploads, deduplicates them and anonymises the
+the file name.
+
+```nix
+services.akkoma.config.":pleroma"."Pleroma.Upload".filters =
+ map (pkgs.formats.elixirConf { }).lib.mkRaw [
+ "Pleroma.Upload.Filter.Exiftool"
+ "Pleroma.Upload.Filter.Dedupe"
+ "Pleroma.Upload.Filter.AnonymizeFilename"
+ ];
+```
+
+## Migration from Pleroma {#modules-services-akkoma-migration-pleroma}
+
+Pleroma instances can be migrated to Akkoma either by copying the database and upload data or by
+pointing Akkoma to the existing data. The necessary database migrations are run automatically
+during startup of the service.
+
+The configuration has to be copy‐edited manually.
+
+Depending on the size of the database, the initial migration may take a long time and exceed the
+startup timeout of the system manager. To work around this issue one may adjust the startup timeout
+{option}`systemd.services.akkoma.serviceConfig.TimeoutStartSec` or simply run the migrations
+manually:
+
+```ShellSession
+pleroma_ctl migrate
+```
+
+### Copying data {#modules-services-akkoma-migration-pleroma-copy}
+
+Copying the Pleroma data instead of re‐using it in place may permit easier reversion to Pleroma,
+but allows the two data sets to diverge.
+
+First disable Pleroma and then copy its database and upload data:
+
+```ShellSession
+# Create a copy of the database
+nix-shell -p postgresql --run 'createdb -T pleroma akkoma'
+
+# Copy upload data
+mkdir /var/lib/akkoma
+cp -R --reflink=auto /var/lib/pleroma/uploads /var/lib/akkoma/
+```
+
+After the data has been copied, enable the Akkoma service and verify that the migration has been
+successful. If no longer required, the original data may then be deleted:
+
+```ShellSession
+# Delete original database
+nix-shell -p postgresql --run 'dropdb pleroma'
+
+# Delete original Pleroma state
+rm -r /var/lib/pleroma
+```
+
+### Re‐using data {#modules-services-akkoma-migration-pleroma-reuse}
+
+To re‐use the Pleroma data in place, disable Pleroma and enable Akkoma, pointing it to the
+Pleroma database and upload directory.
+
+```nix
+# Adjust these settings according to the database name and upload directory path used by Pleroma
+services.akkoma.config.":pleroma"."Pleroma.Repo".database = "pleroma";
+services.akkoma.config.":pleroma".":instance".upload_dir = "/var/lib/pleroma/uploads";
+```
+
+Please keep in mind that after the Akkoma service has been started, any migrations applied by
+Akkoma have to be rolled back before the database can be used again with Pleroma. This can be
+achieved through `pleroma_ctl ecto.rollback`. Refer to the
+[Ecto SQL documentation](https://hexdocs.pm/ecto_sql/Mix.Tasks.Ecto.Rollback.html) for
+details.
+
+## Advanced deployment options {#modules-services-akkoma-advanced-deployment}
+
+### Confinement {#modules-services-akkoma-confinement}
+
+The Akkoma systemd service may be confined to a chroot with
+
+```nix
+services.systemd.akkoma.confinement.enable = true;
+```
+
+Confinement of services is not generally supported in NixOS and therefore disabled by default.
+Depending on the Akkoma configuration, the default confinement settings may be insufficient and
+lead to subtle errors at run time, requiring adjustment:
+
+Use
+[{option}`services.systemd.akkoma.confinement.packages`](options.html#opt-systemd.services._name_.confinement.packages)
+to make packages available in the chroot.
+
+{option}`services.systemd.akkoma.serviceConfig.BindPaths` and
+{option}`services.systemd.akkoma.serviceConfig.BindReadOnlyPaths` permit access to outside paths
+through bind mounts. Refer to
+[{manpage}`systemd.exec(5)`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths=)
+for details.
+
+### Distributed deployment {#modules-services-akkoma-distributed-deployment}
+
+Being an Elixir application, Akkoma can be deployed in a distributed fashion.
+
+This requires setting
+[{option}`services.akkoma.dist.address`](options.html#opt-services.akkoma.dist.address) and
+[{option}`services.akkoma.dist.cookie`](options.html#opt-services.akkoma.dist.cookie). The
+specifics depend strongly on the deployment environment. For more information please check the
+relevant [Erlang documentation](https://www.erlang.org/doc/reference_manual/distributed.html).
diff --git a/nixos/modules/services/web-apps/akkoma.nix b/nixos/modules/services/web-apps/akkoma.nix
new file mode 100644
index 000000000000..47ba53e42221
--- /dev/null
+++ b/nixos/modules/services/web-apps/akkoma.nix
@@ -0,0 +1,1086 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+ cfg = config.services.akkoma;
+ ex = cfg.config;
+ db = ex.":pleroma"."Pleroma.Repo";
+ web = ex.":pleroma"."Pleroma.Web.Endpoint";
+
+ isConfined = config.systemd.services.akkoma.confinement.enable;
+ hasSmtp = (attrByPath [ ":pleroma" "Pleroma.Emails.Mailer" "adapter" "value" ] null ex) == "Swoosh.Adapters.SMTP";
+
+ isAbsolutePath = v: isString v && substring 0 1 v == "/";
+ isSecret = v: isAttrs v && v ? _secret && isAbsolutePath v._secret;
+
+ absolutePath = with types; mkOptionType {
+ name = "absolutePath";
+ description = "absolute path";
+ descriptionClass = "noun";
+ check = isAbsolutePath;
+ inherit (str) merge;
+ };
+
+ secret = mkOptionType {
+ name = "secret";
+ description = "secret value";
+ descriptionClass = "noun";
+ check = isSecret;
+ nestedTypes = {
+ _secret = absolutePath;
+ };
+ };
+
+ ipAddress = with types; mkOptionType {
+ name = "ipAddress";
+ description = "IPv4 or IPv6 address";
+ descriptionClass = "conjunction";
+ check = x: str.check x && builtins.match "[.0-9:A-Fa-f]+" x != null;
+ inherit (str) merge;
+ };
+
+ elixirValue = let
+ elixirValue' = with types;
+ nullOr (oneOf [ bool int float str (attrsOf elixirValue') (listOf elixirValue') ]) // {
+ description = "Elixir value";
+ };
+ in elixirValue';
+
+ frontend = {
+ options = {
+ package = mkOption {
+ type = types.package;
+ description = mdDoc "Akkoma frontend package.";
+ example = literalExpression "pkgs.akkoma-frontends.pleroma-fe";
+ };
+
+ name = mkOption {
+ type = types.nonEmptyStr;
+ description = mdDoc "Akkoma frontend name.";
+ example = "pleroma-fe";
+ };
+
+ ref = mkOption {
+ type = types.nonEmptyStr;
+ description = mdDoc "Akkoma frontend reference.";
+ example = "stable";
+ };
+ };
+ };
+
+ sha256 = builtins.hashString "sha256";
+
+ replaceSec = let
+ replaceSec' = { }@args: v:
+ if isAttrs v
+ then if v ? _secret
+ then if isAbsolutePath v._secret
+ then sha256 v._secret
+ else abort "Invalid secret path (_secret = ${v._secret})"
+ else mapAttrs (_: val: replaceSec' args val) v
+ else if isList v
+ then map (replaceSec' args) v
+ else v;
+ in replaceSec' { };
+
+ # Erlang/Elixir uses a somewhat special format for IP addresses
+ erlAddr = addr: fileContents
+ (pkgs.runCommand addr {
+ nativeBuildInputs = with pkgs; [ elixir ];
+ code = ''
+ case :inet.parse_address('${addr}') do
+ {:ok, addr} -> IO.inspect addr
+ {:error, _} -> System.halt(65)
+ end
+ '';
+ passAsFile = [ "code" ];
+ } ''elixir "$codePath" >"$out"'');
+
+ format = pkgs.formats.elixirConf { };
+ configFile = format.generate "config.exs"
+ (replaceSec
+ (attrsets.updateManyAttrsByPath [{
+ path = [ ":pleroma" "Pleroma.Web.Endpoint" "http" "ip" ];
+ update = addr:
+ if isAbsolutePath addr
+ then format.lib.mkTuple
+ [ (format.lib.mkAtom ":local") addr ]
+ else format.lib.mkRaw (erlAddr addr);
+ }] cfg.config));
+
+ writeShell = { name, text, runtimeInputs ? [ ] }:
+ pkgs.writeShellApplication { inherit name text runtimeInputs; } + "/bin/${name}";
+
+ genScript = writeShell {
+ name = "akkoma-gen-cookie";
+ runtimeInputs = with pkgs; [ coreutils util-linux ];
+ text = ''
+ install -m 0400 \
+ -o ${escapeShellArg cfg.user } \
+ -g ${escapeShellArg cfg.group} \
+ <(hexdump -n 16 -e '"%02x"' /dev/urandom) \
+ "$RUNTIME_DIRECTORY/cookie"
+ '';
+ };
+
+ copyScript = writeShell {
+ name = "akkoma-copy-cookie";
+ runtimeInputs = with pkgs; [ coreutils ];
+ text = ''
+ install -m 0400 \
+ -o ${escapeShellArg cfg.user} \
+ -g ${escapeShellArg cfg.group} \
+ ${escapeShellArg cfg.dist.cookie._secret} \
+ "$RUNTIME_DIRECTORY/cookie"
+ '';
+ };
+
+ secretPaths = catAttrs "_secret" (collect isSecret cfg.config);
+
+ vapidKeygen = pkgs.writeText "vapidKeygen.exs" ''
+ [public_path, private_path] = System.argv()
+ {public_key, private_key} = :crypto.generate_key :ecdh, :prime256v1
+ File.write! public_path, Base.url_encode64(public_key, padding: false)
+ File.write! private_path, Base.url_encode64(private_key, padding: false)
+ '';
+
+ initSecretsScript = writeShell {
+ name = "akkoma-init-secrets";
+ runtimeInputs = with pkgs; [ coreutils elixir ];
+ text = let
+ key-base = web.secret_key_base;
+ jwt-signer = ex.":joken".":default_signer";
+ signing-salt = web.signing_salt;
+ liveview-salt = web.live_view.signing_salt;
+ vapid-private = ex.":web_push_encryption".":vapid_details".private_key;
+ vapid-public = ex.":web_push_encryption".":vapid_details".public_key;
+ in ''
+ secret() {
+ # Generate default secret if non‐existent
+ test -e "$2" || install -D -m 0600 <(tr -dc 'A-Za-z-._~' </dev/urandom | head -c "$1") "$2"
+ if [ "$(stat --dereference --format='%s' "$2")" -lt "$1" ]; then
+ echo "Secret '$2' is smaller than minimum size of $1 bytes." >&2
+ exit 65
+ fi
+ }
+
+ secret 64 ${escapeShellArg key-base._secret}
+ secret 64 ${escapeShellArg jwt-signer._secret}
+ secret 8 ${escapeShellArg signing-salt._secret}
+ secret 8 ${escapeShellArg liveview-salt._secret}
+
+ ${optionalString (isSecret vapid-public) ''
+ { test -e ${escapeShellArg vapid-private._secret} && \
+ test -e ${escapeShellArg vapid-public._secret}; } || \
+ elixir ${escapeShellArgs [ vapidKeygen vapid-public._secret vapid-private._secret ]}
+ ''}
+ '';
+ };
+
+ configScript = writeShell {
+ name = "akkoma-config";
+ runtimeInputs = with pkgs; [ coreutils replace-secret ];
+ text = ''
+ cd "$RUNTIME_DIRECTORY"
+ tmp="$(mktemp config.exs.XXXXXXXXXX)"
+ trap 'rm -f "$tmp"' EXIT TERM
+
+ cat ${escapeShellArg configFile} >"$tmp"
+ ${concatMapStrings (file: ''
+ replace-secret ${escapeShellArgs [ (sha256 file) file ]} "$tmp"
+ '') secretPaths}
+
+ chown ${escapeShellArg cfg.user}:${escapeShellArg cfg.group} "$tmp"
+ chmod 0400 "$tmp"
+ mv -f "$tmp" config.exs
+ '';
+ };
+
+ pgpass = let
+ esc = escape [ ":" ''\'' ];
+ in if (cfg.initDb.password != null)
+ then pkgs.writeText "pgpass.conf" ''
+ *:*:*${esc cfg.initDb.username}:${esc (sha256 cfg.initDb.password._secret)}
+ ''
+ else null;
+
+ escapeSqlId = x: ''"${replaceStrings [ ''"'' ] [ ''""'' ] x}"'';
+ escapeSqlStr = x: "'${replaceStrings [ "'" ] [ "''" ] x}'";
+
+ setupSql = pkgs.writeText "setup.psql" ''
+ \set ON_ERROR_STOP on
+
+ ALTER ROLE ${escapeSqlId db.username}
+ LOGIN PASSWORD ${if db ? password
+ then "${escapeSqlStr (sha256 db.password._secret)}"
+ else "NULL"};
+
+ ALTER DATABASE ${escapeSqlId db.database}
+ OWNER TO ${escapeSqlId db.username};
+
+ \connect ${escapeSqlId db.database}
+ CREATE EXTENSION IF NOT EXISTS citext;
+ CREATE EXTENSION IF NOT EXISTS pg_trgm;
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+ '';
+
+ dbHost = if db ? socket_dir then db.socket_dir
+ else if db ? socket then db.socket
+ else if db ? hostname then db.hostname
+ else null;
+
+ initDbScript = writeShell {
+ name = "akkoma-initdb";
+ runtimeInputs = with pkgs; [ coreutils replace-secret config.services.postgresql.package ];
+ text = ''
+ pgpass="$(mktemp -t pgpass-XXXXXXXXXX.conf)"
+ setupSql="$(mktemp -t setup-XXXXXXXXXX.psql)"
+ trap 'rm -f "$pgpass $setupSql"' EXIT TERM
+
+ ${optionalString (dbHost != null) ''
+ export PGHOST=${escapeShellArg dbHost}
+ ''}
+ export PGUSER=${escapeShellArg cfg.initDb.username}
+ ${optionalString (pgpass != null) ''
+ cat ${escapeShellArg pgpass} >"$pgpass"
+ replace-secret ${escapeShellArgs [
+ (sha256 cfg.initDb.password._secret) cfg.initDb.password._secret ]} "$pgpass"
+ export PGPASSFILE="$pgpass"
+ ''}
+
+ cat ${escapeShellArg setupSql} >"$setupSql"
+ ${optionalString (db ? password) ''
+ replace-secret ${escapeShellArgs [
+ (sha256 db.password._secret) db.password._secret ]} "$setupSql"
+ ''}
+
+ # Create role if non‐existent
+ psql -tAc "SELECT 1 FROM pg_roles
+ WHERE rolname = "${escapeShellArg (escapeSqlStr db.username)} | grep -F -q 1 || \
+ psql -tAc "CREATE ROLE "${escapeShellArg (escapeSqlId db.username)}
+
+ # Create database if non‐existent
+ psql -tAc "SELECT 1 FROM pg_database
+ WHERE datname = "${escapeShellArg (escapeSqlStr db.database)} | grep -F -q 1 || \
+ psql -tAc "CREATE DATABASE "${escapeShellArg (escapeSqlId db.database)}"
+ OWNER "${escapeShellArg (escapeSqlId db.username)}"
+ TEMPLATE template0
+ ENCODING 'utf8'
+ LOCALE 'C'"
+
+ psql -f "$setupSql"
+ '';
+ };
+
+ envWrapper = let
+ script = writeShell {
+ name = "akkoma-env";
+ text = ''
+ cd "${cfg.package}"
+
+ RUNTIME_DIRECTORY="''${RUNTIME_DIRECTORY:-/run/akkoma}"
+ AKKOMA_CONFIG_PATH="$RUNTIME_DIRECTORY/config.exs" \
+ ERL_EPMD_ADDRESS="${cfg.dist.address}" \
+ ERL_EPMD_PORT="${toString cfg.dist.epmdPort}" \
+ ERL_FLAGS="${concatStringsSep " " [
+ "-kernel inet_dist_use_interface '${erlAddr cfg.dist.address}'"
+ "-kernel inet_dist_listen_min ${toString cfg.dist.portMin}"
+ "-kernel inet_dist_listen_max ${toString cfg.dist.portMax}"
+ ]}" \
+ RELEASE_COOKIE="$(<"$RUNTIME_DIRECTORY/cookie")" \
+ RELEASE_NAME="akkoma" \
+ exec "${cfg.package}/bin/$(basename "$0")" "$@"
+ '';
+ };
+ in pkgs.runCommandLocal "akkoma-env" { } ''
+ mkdir -p "$out/bin"
+
+ ln -r -s ${escapeShellArg script} "$out/bin/pleroma"
+ ln -r -s ${escapeShellArg script} "$out/bin/pleroma_ctl"
+ '';
+
+ userWrapper = pkgs.writeShellApplication {
+ name = "pleroma_ctl";
+ text = ''
+ if [ "''${1-}" == "update" ]; then
+ echo "OTP releases are not supported on NixOS." >&2
+ exit 64
+ fi
+
+ exec sudo -u ${escapeShellArg cfg.user} \
+ "${envWrapper}/bin/pleroma_ctl" "$@"
+ '';
+ };
+
+ socketScript = if isAbsolutePath web.http.ip
+ then writeShell {
+ name = "akkoma-socket";
+ runtimeInputs = with pkgs; [ coreutils inotify-tools ];
+ text = ''
+ coproc {
+ inotifywait -q -m -e create ${escapeShellArg (dirOf web.http.ip)}
+ }
+
+ trap 'kill "$COPROC_PID"' EXIT TERM
+
+ until test -S ${escapeShellArg web.http.ip}
+ do read -r -u "''${COPROC[0]}"
+ done
+
+ chmod 0666 ${escapeShellArg web.http.ip}
+ '';
+ }
+ else null;
+
+ staticDir = ex.":pleroma".":instance".static_dir;
+ uploadDir = ex.":pleroma".":instance".upload_dir;
+
+ staticFiles = pkgs.runCommandLocal "akkoma-static" { } ''
+ ${concatStringsSep "\n" (mapAttrsToList (key: val: ''
+ mkdir -p $out/frontends/${escapeShellArg val.name}/
+ ln -s ${escapeShellArg val.package} $out/frontends/${escapeShellArg val.name}/${escapeShellArg val.ref}
+ '') cfg.frontends)}
+
+ ${optionalString (cfg.extraStatic != null)
+ (concatStringsSep "\n" (mapAttrsToList (key: val: ''
+ mkdir -p "$out/$(dirname ${escapeShellArg key})"
+ ln -s ${escapeShellArg val} $out/${escapeShellArg key}
+ '') cfg.extraStatic))}
+ '';
+in {
+ options = {
+ services.akkoma = {
+ enable = mkEnableOption (mdDoc "Akkoma");
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.akkoma;
+ defaultText = literalExpression "pkgs.akkoma";
+ description = mdDoc "Akkoma package to use.";
+ };
+
+ user = mkOption {
+ type = types.nonEmptyStr;
+ default = "akkoma";
+ description = mdDoc "User account under which Akkoma runs.";
+ };
+
+ group = mkOption {
+ type = types.nonEmptyStr;
+ default = "akkoma";
+ description = mdDoc "Group account under which Akkoma runs.";
+ };
+
+ initDb = {
+ enable = mkOption {
+ type = types.bool;
+ default = true;
+ description = mdDoc ''
+ Whether to automatically initialise the database on startup. This will create a
+ database role and database if they do not already exist, and (re)set the role password
+ and the ownership of the database.
+
+ This setting can be used safely even if the database already exists and contains data.
+
+ The database settings are configured through
+ [{option}`config.services.akkoma.config.":pleroma"."Pleroma.Repo"`](#opt-services.akkoma.config.__pleroma_._Pleroma.Repo_).
+
+ If disabled, the database has to be set up manually:
+
+ ```SQL
+ CREATE ROLE akkoma LOGIN;
+
+ CREATE DATABASE akkoma
+ OWNER akkoma
+ TEMPLATE template0
+ ENCODING 'utf8'
+ LOCALE 'C';
+
+ \connect akkoma
+ CREATE EXTENSION IF NOT EXISTS citext;
+ CREATE EXTENSION IF NOT EXISTS pg_trgm;
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+ ```
+ '';
+ };
+
+ username = mkOption {
+ type = types.nonEmptyStr;
+ default = config.services.postgresql.superUser;
+ defaultText = literalExpression "config.services.postgresql.superUser";
+ description = mdDoc ''
+ Name of the database user to initialise the database with.
+
+ This user is required to have the `CREATEROLE` and `CREATEDB` capabilities.
+ '';
+ };
+
+ password = mkOption {
+ type = types.nullOr secret;
+ default = null;
+ description = mdDoc ''
+ Password of the database user to initialise the database with.
+
+ If set to `null`, no password will be used.
+
+ The attribute `_secret` should point to a file containing the secret.
+ '';
+ };
+ };
+
+ initSecrets = mkOption {
+ type = types.bool;
+ default = true;
+ description = mdDoc ''
+ Whether to initialise non‐existent secrets with random values.
+
+ If enabled, appropriate secrets for the following options will be created automatically
+ if the files referenced in the `_secrets` attribute do not exist during startup.
+
+ - {option}`config.":pleroma"."Pleroma.Web.Endpoint".secret_key_base`
+ - {option}`config.":pleroma"."Pleroma.Web.Endpoint".signing_salt`
+ - {option}`config.":pleroma"."Pleroma.Web.Endpoint".live_view.signing_salt`
+ - {option}`config.":web_push_encryption".":vapid_details".private_key`
+ - {option}`config.":web_push_encryption".":vapid_details".public_key`
+ - {option}`config.":joken".":default_signer"`
+ '';
+ };
+
+ installWrapper = mkOption {
+ type = types.bool;
+ default = true;
+ description = mdDoc ''
+ Whether to install a wrapper around `pleroma_ctl` to simplify administration of the
+ Akkoma instance.
+ '';
+ };
+
+ extraPackages = mkOption {
+ type = with types; listOf package;
+ default = with pkgs; [ exiftool ffmpeg_5-headless graphicsmagick-imagemagick-compat ];
+ defaultText = literalExpression "with pkgs; [ exiftool graphicsmagick-imagemagick-compat ffmpeg_5-headless ]";
+ example = literalExpression "with pkgs; [ exiftool imagemagick ffmpeg_5-full ]";
+ description = mdDoc ''
+ List of extra packages to include in the executable search path of the service unit.
+ These are needed by various configurable components such as:
+
+ - ExifTool for the `Pleroma.Upload.Filter.Exiftool` upload filter,
+ - ImageMagick for still image previews in the media proxy as well as for the
+ `Pleroma.Upload.Filters.Mogrify` upload filter, and
+ - ffmpeg for video previews in the media proxy.
+ '';
+ };
+
+ frontends = mkOption {
+ description = mdDoc "Akkoma frontends.";
+ type = with types; attrsOf (submodule frontend);
+ default = {
+ primary = {
+ package = pkgs.akkoma-frontends.pleroma-fe;
+ name = "pleroma-fe";
+ ref = "stable";
+ };
+ admin = {
+ package = pkgs.akkoma-frontends.admin-fe;
+ name = "admin-fe";
+ ref = "stable";
+ };
+ };
+ defaultText = literalExpression ''
+ {
+ primary = {
+ package = pkgs.akkoma-frontends.pleroma-fe;
+ name = "pleroma-fe";
+ ref = "stable";
+ };
+ admin = {
+ package = pkgs.akkoma-frontends.admin-fe;
+ name = "admin-fe";
+ ref = "stable";
+ };
+ }
+ '';
+ };
+
+ extraStatic = mkOption {
+ type = with types; nullOr (attrsOf package);
+ description = mdDoc ''
+ Attribute set of extra packages to add to the static files directory.
+
+ Do not add frontends here. These should be configured through
+ [{option}`services.akkoma.frontends`](#opt-services.akkoma.frontends).
+ '';
+ default = null;
+ example = literalExpression ''
+ {
+ "emoji/blobs.gg" = pkgs.akkoma-emoji.blobs_gg;
+ "static/terms-of-service.html" = pkgs.writeText "terms-of-service.html" '''
+ …
+ ''';
+ "favicon.png" = let
+ rev = "697a8211b0f427a921e7935a35d14bb3e32d0a2c";
+ in pkgs.stdenvNoCC.mkDerivation {
+ name = "favicon.png";
+
+ src = pkgs.fetchurl {
+ url = "https://raw.githubusercontent.com/TilCreator/NixOwO/''${rev}/NixOwO_plain.svg";
+ hash = "sha256-tWhHMfJ3Od58N9H5yOKPMfM56hYWSOnr/TGCBi8bo9E=";
+ };
+
+ nativeBuildInputs = with pkgs; [ librsvg ];
+
+ dontUnpack = true;
+ installPhase = '''
+ rsvg-convert -o $out -w 96 -h 96 $src
+ ''';
+ };
+ }
+ '';
+ };
+
+ dist = {
+ address = mkOption {
+ type = ipAddress;
+ default = "127.0.0.1";
+ description = mdDoc ''
+ Listen address for Erlang distribution protocol and Port Mapper Daemon (epmd).
+ '';
+ };
+
+ epmdPort = mkOption {
+ type = types.port;
+ default = 4369;
+ description = mdDoc "TCP port to bind Erlang Port Mapper Daemon to.";
+ };
+
+ portMin = mkOption {
+ type = types.port;
+ default = 49152;
+ description = mdDoc "Lower bound for Erlang distribution protocol TCP port.";
+ };
+
+ portMax = mkOption {
+ type = types.port;
+ default = 65535;
+ description = mdDoc "Upper bound for Erlang distribution protocol TCP port.";
+ };
+
+ cookie = mkOption {
+ type = types.nullOr secret;
+ default = null;
+ example = { _secret = "/var/lib/secrets/akkoma/releaseCookie"; };
+ description = mdDoc ''
+ Erlang release cookie.
+
+ If set to `null`, a temporary random cookie will be generated.
+ '';
+ };
+ };
+
+ config = mkOption {
+ description = mdDoc ''
+ Configuration for Akkoma. The attributes are serialised to Elixir DSL.
+
+ Refer to <https://docs.akkoma.dev/stable/configuration/cheatsheet/> for
+ configuration options.
+
+ Settings containing secret data should be set to an attribute set containing the
+ attribute `_secret` - a string pointing to a file containing the value the option
+ should be set to.
+ '';
+ type = types.submodule {
+ freeformType = format.type;
+ options = {
+ ":pleroma" = {
+ ":instance" = {
+ name = mkOption {
+ type = types.nonEmptyStr;
+ description = mdDoc "Instance name.";
+ };
+
+ email = mkOption {
+ type = types.nonEmptyStr;
+ description = mdDoc "Instance administrator email.";
+ };
+
+ description = mkOption {
+ type = types.nonEmptyStr;
+ description = mdDoc "Instance description.";
+ };
+
+ static_dir = mkOption {
+ type = types.path;
+ default = toString staticFiles;
+ defaultText = literalMD ''
+ Derivation gathering the following paths into a directory:
+
+ - [{option}`services.akkoma.frontends`](#opt-services.akkoma.frontends)
+ - [{option}`services.akkoma.extraStatic`](#opt-services.akkoma.extraStatic)
+ '';
+ description = mdDoc ''
+ Directory of static files.
+
+ This directory can be built using a derivation, or it can be managed as mutable
+ state by setting the option to an absolute path.
+ '';
+ };
+
+ upload_dir = mkOption {
+ type = absolutePath;
+ default = "/var/lib/akkoma/uploads";
+ description = mdDoc ''
+ Directory where Akkoma will put uploaded files.
+ '';
+ };
+ };
+
+ "Pleroma.Repo" = mkOption {
+ type = elixirValue;
+ default = {
+ adapter = format.lib.mkRaw "Ecto.Adapters.Postgres";
+ socket_dir = "/run/postgresql";
+ username = cfg.user;
+ database = "akkoma";
+ };
+ defaultText = literalExpression ''
+ {
+ adapter = (pkgs.formats.elixirConf { }).lib.mkRaw "Ecto.Adapters.Postgres";
+ socket_dir = "/run/postgresql";
+ username = config.services.akkoma.user;
+ database = "akkoma";
+ }
+ '';
+ description = mdDoc ''
+ Database configuration.
+
+ Refer to
+ <https://hexdocs.pm/ecto_sql/Ecto.Adapters.Postgres.html#module-connection-options>
+ for options.
+ '';
+ };
+
+ "Pleroma.Web.Endpoint" = {
+ url = {
+ host = mkOption {
+ type = types.nonEmptyStr;
+ default = config.networking.fqdn;
+ defaultText = literalExpression "config.networking.fqdn";
+ description = mdDoc "Domain name of the instance.";
+ };
+
+ scheme = mkOption {
+ type = types.nonEmptyStr;
+ default = "https";
+ description = mdDoc "URL scheme.";
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = 443;
+ description = mdDoc "External port number.";
+ };
+ };
+
+ http = {
+ ip = mkOption {
+ type = types.either absolutePath ipAddress;
+ default = "/run/akkoma/socket";
+ example = "::1";
+ description = mdDoc ''
+ Listener IP address or Unix socket path.
+
+ The value is automatically converted to Elixir’s internal address
+ representation during serialisation.
+ '';
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = if isAbsolutePath web.http.ip then 0 else 4000;
+ defaultText = literalExpression ''
+ if isAbsolutePath config.services.akkoma.config.:pleroma"."Pleroma.Web.Endpoint".http.ip
+ then 0
+ else 4000;
+ '';
+ description = mdDoc ''
+ Listener port number.
+
+ Must be 0 if using a Unix socket.
+ '';
+ };
+ };
+
+ secret_key_base = mkOption {
+ type = secret;
+ default = { _secret = "/var/lib/secrets/akkoma/key-base"; };
+ description = mdDoc ''
+ Secret key used as a base to generate further secrets for encrypting and
+ signing data.
+
+ The attribute `_secret` should point to a file containing the secret.
+
+ This key can generated can be generated as follows:
+
+ ```ShellSession
+ $ tr -dc 'A-Za-z-._~' </dev/urandom | head -c 64
+ ```
+ '';
+ };
+
+ live_view = {
+ signing_salt = mkOption {
+ type = secret;
+ default = { _secret = "/var/lib/secrets/akkoma/liveview-salt"; };
+ description = mdDoc ''
+ LiveView signing salt.
+
+ The attribute `_secret` should point to a file containing the secret.
+
+ This salt can be generated as follows:
+
+ ```ShellSession
+ $ tr -dc 'A-Za-z0-9-._~' </dev/urandom | head -c 8
+ ```
+ '';
+ };
+ };
+
+ signing_salt = mkOption {
+ type = secret;
+ default = { _secret = "/var/lib/secrets/akkoma/signing-salt"; };
+ description = mdDoc ''
+ Signing salt.
+
+ The attribute `_secret` should point to a file containing the secret.
+
+ This salt can be generated as follows:
+
+ ```ShellSession
+ $ tr -dc 'A-Za-z0-9-._~' </dev/urandom | head -c 8
+ ```
+ '';
+ };
+ };
+
+ ":frontends" = mkOption {
+ type = elixirValue;
+ default = mapAttrs
+ (key: val: format.lib.mkMap { name = val.name; ref = val.ref; })
+ cfg.frontends;
+ defaultText = literalExpression ''
+ lib.mapAttrs (key: val:
+ (pkgs.formats.elixirConf { }).lib.mkMap { name = val.name; ref = val.ref; })
+ config.services.akkoma.frontends;
+ '';
+ description = mdDoc ''
+ Frontend configuration.
+
+ Users should rely on the default value and prefer to configure frontends through
+ [{option}`config.services.akkoma.frontends`](#opt-services.akkoma.frontends).
+ '';
+ };
+ };
+
+ ":web_push_encryption" = mkOption {
+ default = { };
+ description = mdDoc ''
+ Web Push Notifications configuration.
+
+ The necessary key pair can be generated as follows:
+
+ ```ShellSession
+ $ nix-shell -p nodejs --run 'npx web-push generate-vapid-keys'
+ ```
+ '';
+ type = types.submodule {
+ freeformType = elixirValue;
+ options = {
+ ":vapid_details" = {
+ subject = mkOption {
+ type = types.nonEmptyStr;
+ default = "mailto:${ex.":pleroma".":instance".email}";
+ defaultText = literalExpression ''
+ "mailto:''${config.services.akkoma.config.":pleroma".":instance".email}"
+ '';
+ description = mdDoc "mailto URI for administrative contact.";
+ };
+
+ public_key = mkOption {
+ type = with types; either nonEmptyStr secret;
+ default = { _secret = "/var/lib/secrets/akkoma/vapid-public"; };
+ description = mdDoc "base64-encoded public ECDH key.";
+ };
+
+ private_key = mkOption {
+ type = secret;
+ default = { _secret = "/var/lib/secrets/akkoma/vapid-private"; };
+ description = mdDoc ''
+ base64-encoded private ECDH key.
+
+ The attribute `_secret` should point to a file containing the secret.
+ '';
+ };
+ };
+ };
+ };
+ };
+
+ ":joken" = {
+ ":default_signer" = mkOption {
+ type = secret;
+ default = { _secret = "/var/lib/secrets/akkoma/jwt-signer"; };
+ description = mdDoc ''
+ JWT signing secret.
+
+ The attribute `_secret` should point to a file containing the secret.
+
+ This secret can be generated as follows:
+
+ ```ShellSession
+ $ tr -dc 'A-Za-z0-9-._~' </dev/urandom | head -c 64
+ ```
+ '';
+ };
+ };
+
+ ":logger" = {
+ ":backends" = mkOption {
+ type = types.listOf elixirValue;
+ visible = false;
+ default = with format.lib; [
+ (mkTuple [ (mkRaw "ExSyslogger") (mkAtom ":ex_syslogger") ])
+ ];
+ };
+
+ ":ex_syslogger" = {
+ ident = mkOption {
+ type = types.str;
+ visible = false;
+ default = "akkoma";
+ };
+
+ level = mkOption {
+ type = types.nonEmptyStr;
+ apply = format.lib.mkAtom;
+ default = ":info";
+ example = ":warning";
+ description = mdDoc ''
+ Log level.
+
+ Refer to
+ <https://hexdocs.pm/logger/Logger.html#module-levels>
+ for options.
+ '';
+ };
+ };
+ };
+
+ ":tzdata" = {
+ ":data_dir" = mkOption {
+ type = elixirValue;
+ internal = true;
+ default = format.lib.mkRaw ''
+ Path.join(System.fetch_env!("CACHE_DIRECTORY"), "tzdata")
+ '';
+ };
+ };
+ };
+ };
+ };
+
+ nginx = mkOption {
+ type = with types; nullOr (submodule
+ (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }));
+ default = null;
+ description = mdDoc ''
+ Extra configuration for the nginx virtual host of Akkoma.
+
+ If set to `null`, no virtual host will be added to the nginx configuration.
+ '';
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ warnings = optionals (!config.security.sudo.enable) [''
+ The pleroma_ctl wrapper enabled by the installWrapper option relies on
+ sudo, which appears to have been disabled through security.sudo.enable.
+ ''];
+
+ users = {
+ users."${cfg.user}" = {
+ description = "Akkoma user";
+ group = cfg.group;
+ isSystemUser = true;
+ };
+ groups."${cfg.group}" = { };
+ };
+
+ # Confinement of the main service unit requires separation of the
+ # configuration generation into a separate unit to permit access to secrets
+ # residing outside of the chroot.
+ systemd.services.akkoma-config = {
+ description = "Akkoma social network configuration";
+ reloadTriggers = [ configFile ] ++ secretPaths;
+
+ unitConfig.PropagatesReloadTo = [ "akkoma.service" ];
+ serviceConfig = {
+ Type = "oneshot";
+ RemainAfterExit = true;
+ UMask = "0077";
+
+ RuntimeDirectory = "akkoma";
+
+ ExecStart = mkMerge [
+ (mkIf (cfg.dist.cookie == null) [ genScript ])
+ (mkIf (cfg.dist.cookie != null) [ copyScript ])
+ (mkIf cfg.initSecrets [ initSecretsScript ])
+ [ configScript ]
+ ];
+
+ ExecReload = mkMerge [
+ (mkIf cfg.initSecrets [ initSecretsScript ])
+ [ configScript ]
+ ];
+ };
+ };
+
+ systemd.services.akkoma-initdb = mkIf cfg.initDb.enable {
+ description = "Akkoma social network database setup";
+ requires = [ "akkoma-config.service" ];
+ requiredBy = [ "akkoma.service" ];
+ after = [ "akkoma-config.service" "postgresql.service" ];
+ before = [ "akkoma.service" ];
+
+ serviceConfig = {
+ Type = "oneshot";
+ User = mkIf (db ? socket_dir || db ? socket)
+ cfg.initDb.username;
+ RemainAfterExit = true;
+ UMask = "0077";
+ ExecStart = initDbScript;
+ PrivateTmp = true;
+ };
+ };
+
+ systemd.services.akkoma = let
+ runtimeInputs = with pkgs; [ coreutils gawk gnused ] ++ cfg.extraPackages;
+ in {
+ description = "Akkoma social network";
+ documentation = [ "https://docs.akkoma.dev/stable/" ];
+
+ # This service depends on network-online.target and is sequenced after
+ # it because it requires access to the Internet to function properly.
+ bindsTo = [ "akkoma-config.service" ];
+ wants = [ "network-online.service" ];
+ wantedBy = [ "multi-user.target" ];
+ after = [
+ "akkoma-config.target"
+ "network.target"
+ "network-online.target"
+ "postgresql.service"
+ ];
+
+ confinement.packages = mkIf isConfined runtimeInputs;
+ path = runtimeInputs;
+
+ serviceConfig = {
+ Type = "exec";
+ User = cfg.user;
+ Group = cfg.group;
+ UMask = "0077";
+
+ # The run‐time directory is preserved as it is managed by the akkoma-config.service unit.
+ RuntimeDirectory = "akkoma";
+ RuntimeDirectoryPreserve = true;
+
+ CacheDirectory = "akkoma";
+
+ BindPaths = [ "${uploadDir}:${uploadDir}:norbind" ];
+ BindReadOnlyPaths = mkMerge [
+ (mkIf (!isStorePath staticDir) [ "${staticDir}:${staticDir}:norbind" ])
+ (mkIf isConfined (mkMerge [
+ [ "/etc/hosts" "/etc/resolv.conf" ]
+ (mkIf (isStorePath staticDir) (map (dir: "${dir}:${dir}:norbind")
+ (splitString "\n" (readFile ((pkgs.closureInfo { rootPaths = staticDir; }) + "/store-paths")))))
+ (mkIf (db ? socket_dir) [ "${db.socket_dir}:${db.socket_dir}:norbind" ])
+ (mkIf (db ? socket) [ "${db.socket}:${db.socket}:norbind" ])
+ ]))
+ ];
+
+ ExecStartPre = "${envWrapper}/bin/pleroma_ctl migrate";
+ ExecStart = "${envWrapper}/bin/pleroma start";
+ ExecStartPost = socketScript;
+ ExecStop = "${envWrapper}/bin/pleroma stop";
+ ExecStopPost = mkIf (isAbsolutePath web.http.ip)
+ "${pkgs.coreutils}/bin/rm -f '${web.http.ip}'";
+
+ ProtectProc = "noaccess";
+ ProcSubset = "pid";
+ ProtectSystem = mkIf (!isConfined) "strict";
+ ProtectHome = true;
+ PrivateTmp = true;
+ PrivateDevices = true;
+ PrivateIPC = true;
+ ProtectHostname = true;
+ ProtectClock = true;
+ ProtectKernelTunables = true;
+ ProtectKernelModules = true;
+ ProtectKernelLogs = true;
+ ProtectControlGroups = true;
+
+ RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+ RestrictNamespaces = true;
+ LockPersonality = true;
+ RestrictRealtime = true;
+ RestrictSUIDSGID = true;
+ RemoveIPC = true;
+
+ CapabilityBoundingSet = mkIf
+ (any (port: port > 0 && port < 1024)
+ [ web.http.port cfg.dist.epmdPort cfg.dist.portMin ])
+ [ "CAP_NET_BIND_SERVICE" ];
+
+ NoNewPrivileges = true;
+ SystemCallFilter = [ "@system-service" "~@privileged" "@chown" ];
+ SystemCallArchitectures = "native";
+
+ DeviceAllow = null;
+ DevicePolicy = "closed";
+
+ # SMTP adapter uses dynamic port 0 binding, which is incompatible with bind address filtering
+ SocketBindAllow = mkIf (!hasSmtp) (mkMerge [
+ [ "tcp:${toString cfg.dist.epmdPort}" "tcp:${toString cfg.dist.portMin}-${toString cfg.dist.portMax}" ]
+ (mkIf (web.http.port != 0) [ "tcp:${toString web.http.port}" ])
+ ]);
+ SocketBindDeny = mkIf (!hasSmtp) "any";
+ };
+ };
+
+ systemd.tmpfiles.rules = [
+ "d ${uploadDir} 0700 ${cfg.user} ${cfg.group} - -"
+ "Z ${uploadDir} ~0700 ${cfg.user} ${cfg.group} - -"
+ ];
+
+ environment.systemPackages = mkIf (cfg.installWrapper) [ userWrapper ];
+
+ services.nginx.virtualHosts = mkIf (cfg.nginx != null) {
+ ${web.url.host} = mkMerge [ cfg.nginx {
+ locations."/" = {
+ proxyPass =
+ if isAbsolutePath web.http.ip
+ then "http://unix:${web.http.ip}"
+ else if hasInfix ":" web.http.ip
+ then "http://[${web.http.ip}]:${toString web.http.port}"
+ else "http://${web.http.ip}:${toString web.http.port}";
+
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ };
+ }];
+ };
+ };
+
+ meta.maintainers = with maintainers; [ mvs ];
+ meta.doc = ./akkoma.xml;
+}
diff --git a/nixos/modules/services/web-apps/akkoma.xml b/nixos/modules/services/web-apps/akkoma.xml
new file mode 100644
index 000000000000..76e6b806f30f
--- /dev/null
+++ b/nixos/modules/services/web-apps/akkoma.xml
@@ -0,0 +1,396 @@
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-akkoma">
+ <title>Akkoma</title>
+ <para>
+ <link xlink:href="https://akkoma.dev/">Akkoma</link> is a
+ lightweight ActivityPub microblogging server forked from Pleroma.
+ </para>
+ <section xml:id="modules-services-akkoma-service-configuration">
+ <title>Service configuration</title>
+ <para>
+ The Elixir configuration file required by Akkoma is generated
+ automatically from
+ <link xlink:href="options.html#opt-services.akkoma.config"><option>services.akkoma.config</option></link>.
+ Secrets must be included from external files outside of the Nix
+ store by setting the configuration option to an attribute set
+ containing the attribute <option>_secret</option> – a string
+ pointing to the file containing the actual value of the option.
+ </para>
+ <para>
+ For the mandatory configuration settings these secrets will be
+ generated automatically if the referenced file does not exist
+ during startup, unless disabled through
+ <link xlink:href="options.html#opt-services.akkoma.initSecrets"><option>services.akkoma.initSecrets</option></link>.
+ </para>
+ <para>
+ The following configuration binds Akkoma to the Unix socket
+ <literal>/run/akkoma/socket</literal>, expecting to be run behind
+ a HTTP proxy on <literal>fediverse.example.com</literal>.
+ </para>
+ <programlisting language="nix">
+services.akkoma.enable = true;
+services.akkoma.config = {
+ &quot;:pleroma&quot; = {
+ &quot;:instance&quot; = {
+ name = &quot;My Akkoma instance&quot;;
+ description = &quot;More detailed description&quot;;
+ email = &quot;admin@example.com&quot;;
+ registration_open = false;
+ };
+
+ &quot;Pleroma.Web.Endpoint&quot; = {
+ url.host = &quot;fediverse.example.com&quot;;
+ };
+ };
+};
+</programlisting>
+ <para>
+ Please refer to the
+ <link xlink:href="https://docs.akkoma.dev/stable/configuration/cheatsheet/">configuration
+ cheat sheet</link> for additional configuration options.
+ </para>
+ </section>
+ <section xml:id="modules-services-akkoma-user-management">
+ <title>User management</title>
+ <para>
+ After the Akkoma service is running, the administration utility
+ can be used to
+ <link xlink:href="https://docs.akkoma.dev/stable/administration/CLI_tasks/user/">manage
+ users</link>. In particular an administrative user can be created
+ with
+ </para>
+ <programlisting>
+$ pleroma_ctl user new &lt;nickname&gt; &lt;email&gt; --admin --moderator --password &lt;password&gt;
+</programlisting>
+ </section>
+ <section xml:id="modules-services-akkoma-proxy-configuration">
+ <title>Proxy configuration</title>
+ <para>
+ Although it is possible to expose Akkoma directly, it is common
+ practice to operate it behind an HTTP reverse proxy such as nginx.
+ </para>
+ <programlisting language="nix">
+services.akkoma.nginx = {
+ enableACME = true;
+ forceSSL = true;
+};
+
+services.nginx = {
+ enable = true;
+
+ clientMaxBodySize = &quot;16m&quot;;
+ recommendedTlsSettings = true;
+ recommendedOptimisation = true;
+ recommendedGzipSettings = true;
+};
+</programlisting>
+ <para>
+ Please refer to <xref linkend="module-security-acme" /> for
+ details on how to provision an SSL/TLS certificate.
+ </para>
+ <section xml:id="modules-services-akkoma-media-proxy">
+ <title>Media proxy</title>
+ <para>
+ Without the media proxy function, Akkoma does not store any
+ remote media like pictures or video locally, and clients have to
+ fetch them directly from the source server.
+ </para>
+ <programlisting language="nix">
+# Enable nginx slice module distributed with Tengine
+services.nginx.package = pkgs.tengine;
+
+# Enable media proxy
+services.akkoma.config.&quot;:pleroma&quot;.&quot;:media_proxy&quot; = {
+ enabled = true;
+ proxy_opts.redirect_on_failure = true;
+};
+
+# Adjust the persistent cache size as needed:
+# Assuming an average object size of 128 KiB, around 1 MiB
+# of memory is required for the key zone per GiB of cache.
+# Ensure that the cache directory exists and is writable by nginx.
+services.nginx.commonHttpConfig = ''
+ proxy_cache_path /var/cache/nginx/cache/akkoma-media-cache
+ levels= keys_zone=akkoma_media_cache:16m max_size=16g
+ inactive=1y use_temp_path=off;
+'';
+
+services.akkoma.nginx = {
+ locations.&quot;/proxy&quot; = {
+ proxyPass = &quot;http://unix:/run/akkoma/socket&quot;;
+
+ extraConfig = ''
+ proxy_cache akkoma_media_cache;
+
+ # Cache objects in slices of 1 MiB
+ slice 1m;
+ proxy_cache_key $host$uri$is_args$args$slice_range;
+ proxy_set_header Range $slice_range;
+
+ # Decouple proxy and upstream responses
+ proxy_buffering on;
+ proxy_cache_lock on;
+ proxy_ignore_client_abort on;
+
+ # Default cache times for various responses
+ proxy_cache_valid 200 1y;
+ proxy_cache_valid 206 301 304 1h;
+
+ # Allow serving of stale items
+ proxy_cache_use_stale error timeout invalid_header updating;
+ '';
+ };
+};
+</programlisting>
+ <section xml:id="modules-services-akkoma-prefetch-remote-media">
+ <title>Prefetch remote media</title>
+ <para>
+ The following example enables the
+ <literal>MediaProxyWarmingPolicy</literal> MRF policy which
+ automatically fetches all media associated with a post through
+ the media proxy, as soon as the post is received by the
+ instance.
+ </para>
+ <programlisting language="nix">
+services.akkoma.config.&quot;:pleroma&quot;.&quot;:mrf&quot;.policies =
+ map (pkgs.formats.elixirConf { }).lib.mkRaw [
+ &quot;Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy&quot;
+];
+</programlisting>
+ </section>
+ <section xml:id="modules-services-akkoma-media-previews">
+ <title>Media previews</title>
+ <para>
+ Akkoma can generate previews for media.
+ </para>
+ <programlisting language="nix">
+services.akkoma.config.&quot;:pleroma&quot;.&quot;:media_preview_proxy&quot; = {
+ enabled = true;
+ thumbnail_max_width = 1920;
+ thumbnail_max_height = 1080;
+};
+</programlisting>
+ </section>
+ </section>
+ </section>
+ <section xml:id="modules-services-akkoma-frontend-management">
+ <title>Frontend management</title>
+ <para>
+ Akkoma will be deployed with the <literal>pleroma-fe</literal> and
+ <literal>admin-fe</literal> frontends by default. These can be
+ modified by setting
+ <link xlink:href="options.html#opt-services.akkoma.frontends"><option>services.akkoma.frontends</option></link>.
+ </para>
+ <para>
+ The following example overrides the primary frontend’s default
+ configuration using a custom derivation.
+ </para>
+ <programlisting language="nix">
+services.akkoma.frontends.primary.package = pkgs.runCommand &quot;pleroma-fe&quot; {
+ config = builtins.toJSON {
+ expertLevel = 1;
+ collapseMessageWithSubject = false;
+ stopGifs = false;
+ replyVisibility = &quot;following&quot;;
+ webPushHideIfCW = true;
+ hideScopeNotice = true;
+ renderMisskeyMarkdown = false;
+ hideSiteFavicon = true;
+ postContentType = &quot;text/markdown&quot;;
+ showNavShortcuts = false;
+ };
+ nativeBuildInputs = with pkgs; [ jq xorg.lndir ];
+ passAsFile = [ &quot;config&quot; ];
+} ''
+ mkdir $out
+ lndir ${pkgs.akkoma-frontends.pleroma-fe} $out
+
+ rm $out/static/config.json
+ jq -s add ${pkgs.akkoma-frontends.pleroma-fe}/static/config.json ${config} \
+ &gt;$out/static/config.json
+'';
+</programlisting>
+ </section>
+ <section xml:id="modules-services-akkoma-federation-policies">
+ <title>Federation policies</title>
+ <para>
+ Akkoma comes with a number of modules to police federation with
+ other ActivityPub instances. The most valuable for typical users
+ is the
+ <link xlink:href="https://docs.akkoma.dev/stable/configuration/cheatsheet/#mrf_simple"><literal>:mrf_simple</literal></link>
+ module which allows limiting federation based on instance
+ hostnames.
+ </para>
+ <para>
+ This configuration snippet provides an example on how these can be
+ used. Choosing an adequate federation policy is not trivial and
+ entails finding a balance between connectivity to the rest of the
+ fediverse and providing a pleasant experience to the users of an
+ instance.
+ </para>
+ <programlisting language="nix">
+services.akkoma.config.&quot;:pleroma&quot; = with (pkgs.formats.elixirConf { }).lib; {
+ &quot;:mrf&quot;.policies = map mkRaw [
+ &quot;Pleroma.Web.ActivityPub.MRF.SimplePolicy&quot;
+ ];
+
+ &quot;:mrf_simple&quot; = {
+ # Tag all media as sensitive
+ media_nsfw = mkMap {
+ &quot;nsfw.weird.kinky&quot; = &quot;Untagged NSFW content&quot;;
+ };
+
+ # Reject all activities except deletes
+ reject = mkMap {
+ &quot;kiwifarms.cc&quot; = &quot;Persistent harassment of users, no moderation&quot;;
+ };
+
+ # Force posts to be visible by followers only
+ followers_only = mkMap {
+ &quot;beta.birdsite.live&quot; = &quot;Avoid polluting timelines with Twitter posts&quot;;
+ };
+ };
+};
+</programlisting>
+ </section>
+ <section xml:id="modules-services-akkoma-upload-filters">
+ <title>Upload filters</title>
+ <para>
+ This example strips GPS and location metadata from uploads,
+ deduplicates them and anonymises the the file name.
+ </para>
+ <programlisting language="nix">
+services.akkoma.config.&quot;:pleroma&quot;.&quot;Pleroma.Upload&quot;.filters =
+ map (pkgs.formats.elixirConf { }).lib.mkRaw [
+ &quot;Pleroma.Upload.Filter.Exiftool&quot;
+ &quot;Pleroma.Upload.Filter.Dedupe&quot;
+ &quot;Pleroma.Upload.Filter.AnonymizeFilename&quot;
+ ];
+</programlisting>
+ </section>
+ <section xml:id="modules-services-akkoma-migration-pleroma">
+ <title>Migration from Pleroma</title>
+ <para>
+ Pleroma instances can be migrated to Akkoma either by copying the
+ database and upload data or by pointing Akkoma to the existing
+ data. The necessary database migrations are run automatically
+ during startup of the service.
+ </para>
+ <para>
+ The configuration has to be copy‐edited manually.
+ </para>
+ <para>
+ Depending on the size of the database, the initial migration may
+ take a long time and exceed the startup timeout of the system
+ manager. To work around this issue one may adjust the startup
+ timeout
+ <option>systemd.services.akkoma.serviceConfig.TimeoutStartSec</option>
+ or simply run the migrations manually:
+ </para>
+ <programlisting>
+pleroma_ctl migrate
+</programlisting>
+ <section xml:id="modules-services-akkoma-migration-pleroma-copy">
+ <title>Copying data</title>
+ <para>
+ Copying the Pleroma data instead of re‐using it in place may
+ permit easier reversion to Pleroma, but allows the two data sets
+ to diverge.
+ </para>
+ <para>
+ First disable Pleroma and then copy its database and upload
+ data:
+ </para>
+ <programlisting>
+# Create a copy of the database
+nix-shell -p postgresql --run 'createdb -T pleroma akkoma'
+
+# Copy upload data
+mkdir /var/lib/akkoma
+cp -R --reflink=auto /var/lib/pleroma/uploads /var/lib/akkoma/
+</programlisting>
+ <para>
+ After the data has been copied, enable the Akkoma service and
+ verify that the migration has been successful. If no longer
+ required, the original data may then be deleted:
+ </para>
+ <programlisting>
+# Delete original database
+nix-shell -p postgresql --run 'dropdb pleroma'
+
+# Delete original Pleroma state
+rm -r /var/lib/pleroma
+</programlisting>
+ </section>
+ <section xml:id="modules-services-akkoma-migration-pleroma-reuse">
+ <title>Re‐using data</title>
+ <para>
+ To re‐use the Pleroma data in place, disable Pleroma and enable
+ Akkoma, pointing it to the Pleroma database and upload
+ directory.
+ </para>
+ <programlisting language="nix">
+# Adjust these settings according to the database name and upload directory path used by Pleroma
+services.akkoma.config.&quot;:pleroma&quot;.&quot;Pleroma.Repo&quot;.database = &quot;pleroma&quot;;
+services.akkoma.config.&quot;:pleroma&quot;.&quot;:instance&quot;.upload_dir = &quot;/var/lib/pleroma/uploads&quot;;
+</programlisting>
+ <para>
+ Please keep in mind that after the Akkoma service has been
+ started, any migrations applied by Akkoma have to be rolled back
+ before the database can be used again with Pleroma. This can be
+ achieved through <literal>pleroma_ctl ecto.rollback</literal>.
+ Refer to the
+ <link xlink:href="https://hexdocs.pm/ecto_sql/Mix.Tasks.Ecto.Rollback.html">Ecto
+ SQL documentation</link> for details.
+ </para>
+ </section>
+ </section>
+ <section xml:id="modules-services-akkoma-advanced-deployment">
+ <title>Advanced deployment options</title>
+ <section xml:id="modules-services-akkoma-confinement">
+ <title>Confinement</title>
+ <para>
+ The Akkoma systemd service may be confined to a chroot with
+ </para>
+ <programlisting language="nix">
+services.systemd.akkoma.confinement.enable = true;
+</programlisting>
+ <para>
+ Confinement of services is not generally supported in NixOS and
+ therefore disabled by default. Depending on the Akkoma
+ configuration, the default confinement settings may be
+ insufficient and lead to subtle errors at run time, requiring
+ adjustment:
+ </para>
+ <para>
+ Use
+ <link xlink:href="options.html#opt-systemd.services._name_.confinement.packages"><option>services.systemd.akkoma.confinement.packages</option></link>
+ to make packages available in the chroot.
+ </para>
+ <para>
+ <option>services.systemd.akkoma.serviceConfig.BindPaths</option>
+ and
+ <option>services.systemd.akkoma.serviceConfig.BindReadOnlyPaths</option>
+ permit access to outside paths through bind mounts. Refer to
+ <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths="><link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html"><citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry></link></link>
+ for details.
+ </para>
+ </section>
+ <section xml:id="modules-services-akkoma-distributed-deployment">
+ <title>Distributed deployment</title>
+ <para>
+ Being an Elixir application, Akkoma can be deployed in a
+ distributed fashion.
+ </para>
+ <para>
+ This requires setting
+ <link xlink:href="options.html#opt-services.akkoma.dist.address"><option>services.akkoma.dist.address</option></link>
+ and
+ <link xlink:href="options.html#opt-services.akkoma.dist.cookie"><option>services.akkoma.dist.cookie</option></link>.
+ The specifics depend strongly on the deployment environment. For
+ more information please check the relevant
+ <link xlink:href="https://www.erlang.org/doc/reference_manual/distributed.html">Erlang
+ documentation</link>.
+ </para>
+ </section>
+ </section>
+</chapter>