1{
2 lib,
3 pkgs,
4 config,
5 ...
6}:
7let
8 cfg = config.services.pds;
9
10 inherit (lib)
11 getExe
12 mkEnableOption
13 mkIf
14 mkOption
15 mkPackageOption
16 escapeShellArgs
17 concatMapStringsSep
18 types
19 literalExpression
20 ;
21
22 pdsadminWrapper =
23 let
24 cfgSystemd = config.systemd.services.pds.serviceConfig;
25 in
26 pkgs.writeShellScriptBin "pdsadmin" ''
27 DUMMY_PDS_ENV_FILE="$(mktemp)"
28 trap 'rm -f "$DUMMY_PDS_ENV_FILE"' EXIT
29 env "PDS_ENV_FILE=$DUMMY_PDS_ENV_FILE" \
30 ${escapeShellArgs cfgSystemd.Environment} \
31 ${concatMapStringsSep " " (envFile: "$(cat ${envFile})") cfgSystemd.EnvironmentFile} \
32 ${getExe pkgs.pdsadmin} "$@"
33 '';
34in
35# All defaults are from https://github.com/bluesky-social/pds/blob/8b9fc24cec5f30066b0d0b86d2b0ba3d66c2b532/installer.sh
36{
37 options.services.pds = {
38 enable = mkEnableOption "pds";
39
40 package = mkPackageOption pkgs "pds" { };
41
42 settings = mkOption {
43 type = types.submodule {
44 freeformType = types.attrsOf (
45 types.oneOf [
46 (types.nullOr types.str)
47 types.port
48 ]
49 );
50 options = {
51 PDS_PORT = mkOption {
52 type = types.port;
53 default = 3000;
54 description = "Port to listen on";
55 };
56
57 PDS_HOSTNAME = mkOption {
58 type = types.str;
59 example = "pds.example.com";
60 description = "Instance hostname (base domain name)";
61 };
62
63 PDS_BLOB_UPLOAD_LIMIT = mkOption {
64 type = types.str;
65 default = "52428800";
66 description = "Size limit of uploaded blobs in bytes";
67 };
68
69 PDS_DID_PLC_URL = mkOption {
70 type = types.str;
71 default = "https://plc.directory";
72 description = "URL of DID PLC directory";
73 };
74
75 PDS_BSKY_APP_VIEW_URL = mkOption {
76 type = types.str;
77 default = "https://api.bsky.app";
78 description = "URL of bsky frontend";
79 };
80
81 PDS_BSKY_APP_VIEW_DID = mkOption {
82 type = types.str;
83 default = "did:web:api.bsky.app";
84 description = "DID of bsky frontend";
85 };
86
87 PDS_REPORT_SERVICE_URL = mkOption {
88 type = types.str;
89 default = "https://mod.bsky.app";
90 description = "URL of mod service";
91 };
92
93 PDS_REPORT_SERVICE_DID = mkOption {
94 type = types.str;
95 default = "did:plc:ar7c4by46qjdydhdevvrndac";
96 description = "DID of mod service";
97 };
98
99 PDS_CRAWLERS = mkOption {
100 type = types.str;
101 default = "https://bsky.network";
102 description = "URL of crawlers";
103 };
104
105 PDS_DATA_DIRECTORY = mkOption {
106 type = types.str;
107 default = "/var/lib/pds";
108 description = "Directory to store state";
109 };
110
111 PDS_BLOBSTORE_DISK_LOCATION = mkOption {
112 type = types.nullOr types.str;
113 default = "/var/lib/pds/blocks";
114 description = "Store blobs at this location, set to null to use e.g. S3";
115 };
116
117 LOG_ENABLED = mkOption {
118 type = types.nullOr types.str;
119 default = "true";
120 description = "Enable logging";
121 };
122 };
123 };
124
125 description = ''
126 Environment variables to set for the service. Secrets should be
127 specified using {option}`environmentFile`.
128
129 Refer to <https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/config/env.ts> for available environment variables.
130 '';
131 };
132
133 environmentFiles = mkOption {
134 type = types.listOf types.path;
135 default = [ ];
136 description = ''
137 File to load environment variables from. Loaded variables override
138 values set in {option}`environment`.
139
140 Use it to set values of `PDS_JWT_SECRET`, `PDS_ADMIN_PASSWORD`,
141 and `PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX` secrets.
142 `PDS_JWT_SECRET` and `PDS_ADMIN_PASSWORD` can be generated with
143 ```
144 openssl rand --hex 16
145 ```
146 `PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX` can be generated with
147 ```
148 openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32
149 ```
150 '';
151 };
152
153 pdsadmin = {
154 enable = mkOption {
155 type = types.bool;
156 default = cfg.enable;
157 defaultText = literalExpression "config.services.pds.enable";
158 description = "Add pdsadmin script to PATH";
159 };
160 };
161 };
162
163 config = mkIf cfg.enable {
164 environment = mkIf cfg.pdsadmin.enable {
165 systemPackages = [ pdsadminWrapper ];
166 };
167
168 systemd.services.pds = {
169 description = "pds";
170
171 after = [ "network-online.target" ];
172 wants = [ "network-online.target" ];
173 wantedBy = [ "multi-user.target" ];
174
175 serviceConfig = {
176 ExecStart = getExe cfg.package;
177 Environment = lib.mapAttrsToList (k: v: "${k}=${if builtins.isInt v then toString v else v}") (
178 lib.filterAttrs (_: v: v != null) cfg.settings
179 );
180
181 EnvironmentFile = cfg.environmentFiles;
182 User = "pds";
183 Group = "pds";
184 StateDirectory = "pds";
185 StateDirectoryMode = "0755";
186 Restart = "always";
187
188 # Hardening
189 RemoveIPC = true;
190 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
191 NoNewPrivileges = true;
192 PrivateDevices = true;
193 ProtectClock = true;
194 ProtectKernelLogs = true;
195 ProtectControlGroups = true;
196 ProtectKernelModules = true;
197 PrivateMounts = true;
198 SystemCallArchitectures = [ "native" ];
199 MemoryDenyWriteExecute = false; # required by V8 JIT
200 RestrictNamespaces = true;
201 RestrictSUIDSGID = true;
202 ProtectHostname = true;
203 LockPersonality = true;
204 ProtectKernelTunables = true;
205 RestrictAddressFamilies = [
206 "AF_UNIX"
207 "AF_INET"
208 "AF_INET6"
209 ];
210 RestrictRealtime = true;
211 DeviceAllow = [ "" ];
212 ProtectSystem = "strict";
213 ProtectProc = "invisible";
214 ProcSubset = "pid";
215 ProtectHome = true;
216 PrivateUsers = true;
217 PrivateTmp = true;
218 UMask = "0077";
219 };
220 };
221
222 users = {
223 users.pds = {
224 group = "pds";
225 isSystemUser = true;
226 };
227 groups.pds = { };
228 };
229
230 };
231
232 meta.maintainers = with lib.maintainers; [ t4ccer ];
233}