silx: 2.1.1 -> 2.1.2 (#361612)
[NixPkgs.git] / nixos / modules / services / databases / postgresql.nix
blob004e8130ad6ea29443ad31f37ed8ba7372bd56b1
1 { config, lib, pkgs, ... }:
3 let
4   inherit (lib)
5     any
6     attrValues
7     concatMapStrings
8     concatStringsSep
9     const
10     elem
11     escapeShellArgs
12     filterAttrs
13     getName
14     isString
15     literalExpression
16     mapAttrs
17     mapAttrsToList
18     mkAfter
19     mkBefore
20     mkDefault
21     mkEnableOption
22     mkIf
23     mkMerge
24     mkOption
25     mkPackageOption
26     mkRemovedOptionModule
27     mkRenamedOptionModule
28     optionalString
29     types
30     versionAtLeast
31     warn
32     ;
34   cfg = config.services.postgresql;
36   # ensure that
37   #   services.postgresql = {
38   #     enableJIT = true;
39   #     package = pkgs.postgresql_<major>;
40   #   };
41   # works.
42   basePackage = if cfg.enableJIT
43     then cfg.package.withJIT
44     else cfg.package.withoutJIT;
46   postgresql = if cfg.extensions == []
47     then basePackage
48     else basePackage.withPackages cfg.extensions;
50   toStr = value:
51     if true == value then "yes"
52     else if false == value then "no"
53     else if isString value then "'${lib.replaceStrings ["'"] ["''"] value}'"
54     else builtins.toString value;
56   # The main PostgreSQL configuration file.
57   configFile = pkgs.writeTextDir "postgresql.conf" (concatStringsSep "\n" (mapAttrsToList (n: v: "${n} = ${toStr v}") (filterAttrs (const (x: x != null)) cfg.settings)));
59   configFileCheck = pkgs.runCommand "postgresql-configfile-check" {} ''
60     ${cfg.package}/bin/postgres -D${configFile} -C config_file >/dev/null
61     touch $out
62   '';
64   groupAccessAvailable = versionAtLeast postgresql.version "11.0";
66   extensionNames = map getName postgresql.installedExtensions;
67   extensionInstalled = extension: elem extension extensionNames;
71   imports = [
72     (mkRemovedOptionModule [ "services" "postgresql" "extraConfig" ] "Use services.postgresql.settings instead.")
74     (mkRenamedOptionModule [ "services" "postgresql" "logLinePrefix" ] [ "services" "postgresql" "settings" "log_line_prefix" ])
75     (mkRenamedOptionModule [ "services" "postgresql" "port" ] [ "services" "postgresql" "settings" "port" ])
76     (mkRenamedOptionModule [ "services" "postgresql" "extraPlugins" ] [ "services" "postgresql" "extensions" ])
77   ];
79   ###### interface
81   options = {
83     services.postgresql = {
85       enable = mkEnableOption "PostgreSQL Server";
87       enableJIT = mkEnableOption "JIT support";
89       package = mkPackageOption pkgs "postgresql" {
90         example = "postgresql_15";
91       };
93       checkConfig = mkOption {
94         type = types.bool;
95         default = true;
96         description = "Check the syntax of the configuration file at compile time";
97       };
99       dataDir = mkOption {
100         type = types.path;
101         defaultText = literalExpression ''"/var/lib/postgresql/''${config.services.postgresql.package.psqlSchema}"'';
102         example = "/var/lib/postgresql/15";
103         description = ''
104           The data directory for PostgreSQL. If left as the default value
105           this directory will automatically be created before the PostgreSQL server starts, otherwise
106           the sysadmin is responsible for ensuring the directory exists with appropriate ownership
107           and permissions.
108         '';
109       };
111       authentication = mkOption {
112         type = types.lines;
113         default = "";
114         description = ''
115           Defines how users authenticate themselves to the server. See the
116           [PostgreSQL documentation for pg_hba.conf](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html)
117           for details on the expected format of this option. By default,
118           peer based authentication will be used for users connecting
119           via the Unix socket, and md5 password authentication will be
120           used for users connecting via TCP. Any added rules will be
121           inserted above the default rules. If you'd like to replace the
122           default rules entirely, you can use `lib.mkForce` in your
123           module.
124         '';
125       };
127       identMap = mkOption {
128         type = types.lines;
129         default = "";
130         example = ''
131           map-name-0 system-username-0 database-username-0
132           map-name-1 system-username-1 database-username-1
133         '';
134         description = ''
135           Defines the mapping from system users to database users.
137           See the [auth doc](https://postgresql.org/docs/current/auth-username-maps.html).
138         '';
139       };
141       initdbArgs = mkOption {
142         type = with types; listOf str;
143         default = [];
144         example = [ "--data-checksums" "--allow-group-access" ];
145         description = ''
146           Additional arguments passed to `initdb` during data dir
147           initialisation.
148         '';
149       };
151       initialScript = mkOption {
152         type = types.nullOr types.path;
153         default = null;
154         example = literalExpression ''
155           pkgs.writeText "init-sql-script" '''
156             alter user postgres with password 'myPassword';
157           ''';'';
159         description = ''
160           A file containing SQL statements to execute on first startup.
161         '';
162       };
164       ensureDatabases = mkOption {
165         type = types.listOf types.str;
166         default = [];
167         description = ''
168           Ensures that the specified databases exist.
169           This option will never delete existing databases, especially not when the value of this
170           option is changed. This means that databases created once through this option or
171           otherwise have to be removed manually.
172         '';
173         example = [
174           "gitea"
175           "nextcloud"
176         ];
177       };
179       ensureUsers = mkOption {
180         type = types.listOf (types.submodule {
181           options = {
182             name = mkOption {
183               type = types.str;
184               description = ''
185                 Name of the user to ensure.
186               '';
187             };
189             ensureDBOwnership = mkOption {
190               type = types.bool;
191               default = false;
192               description = ''
193                 Grants the user ownership to a database with the same name.
194                 This database must be defined manually in
195                 [](#opt-services.postgresql.ensureDatabases).
196               '';
197             };
199             ensureClauses = mkOption {
200               description = ''
201                 An attrset of clauses to grant to the user. Under the hood this uses the
202                 [ALTER USER syntax](https://www.postgresql.org/docs/current/sql-alteruser.html) for each attrName where
203                 the attrValue is true in the attrSet:
204                 `ALTER USER user.name WITH attrName`
205               '';
206               example = literalExpression ''
207                 {
208                   superuser = true;
209                   createrole = true;
210                   createdb = true;
211                 }
212               '';
213               default = {};
214               defaultText = lib.literalMD ''
215                 The default, `null`, means that the user created will have the default permissions assigned by PostgreSQL. Subsequent server starts will not set or unset the clause, so imperative changes are preserved.
216               '';
217               type = types.submodule {
218                 options = let
219                   defaultText = lib.literalMD ''
220                     `null`: do not set. For newly created roles, use PostgreSQL's default. For existing roles, do not touch this clause.
221                   '';
222                 in {
223                   superuser = mkOption {
224                     type = types.nullOr types.bool;
225                     description = ''
226                       Grants the user, created by the ensureUser attr, superuser permissions. From the postgres docs:
228                       A database superuser bypasses all permission checks,
229                       except the right to log in. This is a dangerous privilege
230                       and should not be used carelessly; it is best to do most
231                       of your work as a role that is not a superuser. To create
232                       a new database superuser, use CREATE ROLE name SUPERUSER.
233                       You must do this as a role that is already a superuser.
235                       More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
236                     '';
237                     default = null;
238                     inherit defaultText;
239                   };
240                   createrole = mkOption {
241                     type = types.nullOr types.bool;
242                     description = ''
243                       Grants the user, created by the ensureUser attr, createrole permissions. From the postgres docs:
245                       A role must be explicitly given permission to create more
246                       roles (except for superusers, since those bypass all
247                       permission checks). To create such a role, use CREATE
248                       ROLE name CREATEROLE. A role with CREATEROLE privilege
249                       can alter and drop other roles, too, as well as grant or
250                       revoke membership in them. However, to create, alter,
251                       drop, or change membership of a superuser role, superuser
252                       status is required; CREATEROLE is insufficient for that.
254                       More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
255                     '';
256                     default = null;
257                     inherit defaultText;
258                   };
259                   createdb = mkOption {
260                     type = types.nullOr types.bool;
261                     description = ''
262                       Grants the user, created by the ensureUser attr, createdb permissions. From the postgres docs:
264                       A role must be explicitly given permission to create
265                       databases (except for superusers, since those bypass all
266                       permission checks). To create such a role, use CREATE
267                       ROLE name CREATEDB.
269                       More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
270                     '';
271                     default = null;
272                     inherit defaultText;
273                   };
274                   "inherit" = mkOption {
275                     type = types.nullOr types.bool;
276                     description = ''
277                       Grants the user created inherit permissions. From the postgres docs:
279                       A role is given permission to inherit the privileges of
280                       roles it is a member of, by default. However, to create a
281                       role without the permission, use CREATE ROLE name
282                       NOINHERIT.
284                       More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
285                     '';
286                     default = null;
287                     inherit defaultText;
288                   };
289                   login = mkOption {
290                     type = types.nullOr types.bool;
291                     description = ''
292                       Grants the user, created by the ensureUser attr, login permissions. From the postgres docs:
294                       Only roles that have the LOGIN attribute can be used as
295                       the initial role name for a database connection. A role
296                       with the LOGIN attribute can be considered the same as a
297                       “database user”. To create a role with login privilege,
298                       use either:
300                       CREATE ROLE name LOGIN; CREATE USER name;
302                       (CREATE USER is equivalent to CREATE ROLE except that
303                       CREATE USER includes LOGIN by default, while CREATE ROLE
304                       does not.)
306                       More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
307                     '';
308                     default = null;
309                     inherit defaultText;
310                   };
311                   replication = mkOption {
312                     type = types.nullOr types.bool;
313                     description = ''
314                       Grants the user, created by the ensureUser attr, replication permissions. From the postgres docs:
316                       A role must explicitly be given permission to initiate
317                       streaming replication (except for superusers, since those
318                       bypass all permission checks). A role used for streaming
319                       replication must have LOGIN permission as well. To create
320                       such a role, use CREATE ROLE name REPLICATION LOGIN.
322                       More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
323                     '';
324                     default = null;
325                     inherit defaultText;
326                   };
327                   bypassrls = mkOption {
328                     type = types.nullOr types.bool;
329                     description = ''
330                       Grants the user, created by the ensureUser attr, replication permissions. From the postgres docs:
332                       A role must be explicitly given permission to bypass
333                       every row-level security (RLS) policy (except for
334                       superusers, since those bypass all permission checks). To
335                       create such a role, use CREATE ROLE name BYPASSRLS as a
336                       superuser.
338                       More information on postgres roles can be found [here](https://www.postgresql.org/docs/current/role-attributes.html)
339                     '';
340                     default = null;
341                     inherit defaultText;
342                   };
343                 };
344               };
345             };
346           };
347         });
348         default = [];
349         description = ''
350           Ensures that the specified users exist.
351           The PostgreSQL users will be identified using peer authentication. This authenticates the Unix user with the
352           same name only, and that without the need for a password.
353           This option will never delete existing users or remove DB ownership of databases
354           once granted with `ensureDBOwnership = true;`. This means that this must be
355           cleaned up manually when changing after changing the config in here.
356         '';
357         example = literalExpression ''
358           [
359             {
360               name = "nextcloud";
361             }
362             {
363               name = "superuser";
364               ensureDBOwnership = true;
365             }
366           ]
367         '';
368       };
370       enableTCPIP = mkOption {
371         type = types.bool;
372         default = false;
373         description = ''
374           Whether PostgreSQL should listen on all network interfaces.
375           If disabled, the database can only be accessed via its Unix
376           domain socket or via TCP connections to localhost.
377         '';
378       };
380       extensions = mkOption {
381         type = with types; coercedTo (listOf path) (path: _ignorePg: path) (functionTo (listOf path));
382         default = _: [];
383         example = literalExpression "ps: with ps; [ postgis pg_repack ]";
384         description = ''
385           List of PostgreSQL extensions to install.
386         '';
387       };
389       settings = mkOption {
390         type = with types; submodule {
391           freeformType = attrsOf (oneOf [ bool float int str ]);
392           options = {
393             shared_preload_libraries = mkOption {
394               type = nullOr (coercedTo (listOf str) (concatStringsSep ", ") str);
395               default = null;
396               example = literalExpression ''[ "auto_explain" "anon" ]'';
397               description = ''
398                 List of libraries to be preloaded.
399               '';
400             };
402             log_line_prefix = mkOption {
403               type = types.str;
404               default = "[%p] ";
405               example = "%m [%p] ";
406               description = ''
407                 A printf-style string that is output at the beginning of each log line.
408                 Upstream default is `'%m [%p] '`, i.e. it includes the timestamp. We do
409                 not include the timestamp, because journal has it anyway.
410               '';
411             };
413             port = mkOption {
414               type = types.port;
415               default = 5432;
416               description = ''
417                 The port on which PostgreSQL listens.
418               '';
419             };
420           };
421         };
422         default = {};
423         description = ''
424           PostgreSQL configuration. Refer to
425           <https://www.postgresql.org/docs/current/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE>
426           for an overview of `postgresql.conf`.
428           ::: {.note}
429           String values will automatically be enclosed in single quotes. Single quotes will be
430           escaped with two single quotes as described by the upstream documentation linked above.
431           :::
432         '';
433         example = literalExpression ''
434           {
435             log_connections = true;
436             log_statement = "all";
437             logging_collector = true;
438             log_disconnections = true;
439             log_destination = lib.mkForce "syslog";
440           }
441         '';
442       };
444       recoveryConfig = mkOption {
445         type = types.nullOr types.lines;
446         default = null;
447         description = ''
448           Contents of the {file}`recovery.conf` file.
449         '';
450       };
452       superUser = mkOption {
453         type = types.str;
454         default = "postgres";
455         internal = true;
456         readOnly = true;
457         description = ''
458           PostgreSQL superuser account to use for various operations. Internal since changing
459           this value would lead to breakage while setting up databases.
460         '';
461         };
462     };
464   };
467   ###### implementation
469   config = mkIf cfg.enable {
471     assertions = map ({ name, ensureDBOwnership, ... }: {
472       assertion = ensureDBOwnership -> elem name cfg.ensureDatabases;
473       message = ''
474         For each database user defined with `services.postgresql.ensureUsers` and
475         `ensureDBOwnership = true;`, a database with the same name must be defined
476         in `services.postgresql.ensureDatabases`.
478         Offender: ${name} has not been found among databases.
479       '';
480     }) cfg.ensureUsers;
482     services.postgresql.settings =
483       {
484         hba_file = "${pkgs.writeText "pg_hba.conf" cfg.authentication}";
485         ident_file = "${pkgs.writeText "pg_ident.conf" cfg.identMap}";
486         log_destination = "stderr";
487         listen_addresses = if cfg.enableTCPIP then "*" else "localhost";
488         jit = mkDefault (if cfg.enableJIT then "on" else "off");
489       };
491     services.postgresql.package = let
492         mkThrow = ver: throw "postgresql_${ver} was removed, please upgrade your postgresql version.";
493         mkWarn = ver: warn ''
494           The postgresql package is not pinned and selected automatically by
495           `system.stateVersion`. Right now this is `pkgs.postgresql_${ver}`, the
496           oldest postgresql version available and thus the next that will be
497           removed when EOL on the next stable cycle.
499           See also https://endoflife.date/postgresql
500         '';
501         base = if versionAtLeast config.system.stateVersion "24.11" then pkgs.postgresql_16
502             else if versionAtLeast config.system.stateVersion "23.11" then pkgs.postgresql_15
503             else if versionAtLeast config.system.stateVersion "22.05" then pkgs.postgresql_14
504             else if versionAtLeast config.system.stateVersion "21.11" then mkWarn "13" pkgs.postgresql_13
505             else if versionAtLeast config.system.stateVersion "20.03" then mkThrow "11"
506             else if versionAtLeast config.system.stateVersion "17.09" then mkThrow "9_6"
507             else mkThrow "9_5";
508     in
509       # Note: when changing the default, make it conditional on
510       # ‘system.stateVersion’ to maintain compatibility with existing
511       # systems!
512       mkDefault (if cfg.enableJIT then base.withJIT else base);
514     services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}";
516     services.postgresql.authentication = mkMerge [
517       (mkBefore "# Generated file; do not edit!")
518       (mkAfter
519       ''
520         # default value of services.postgresql.authentication
521         local all all              peer
522         host  all all 127.0.0.1/32 md5
523         host  all all ::1/128      md5
524       '')
525     ];
527     users.users.postgres =
528       { name = "postgres";
529         uid = config.ids.uids.postgres;
530         group = "postgres";
531         description = "PostgreSQL server user";
532         home = "${cfg.dataDir}";
533         useDefaultShell = true;
534       };
536     users.groups.postgres.gid = config.ids.gids.postgres;
538     environment.systemPackages = [ postgresql ];
540     environment.pathsToLink = [
541      "/share/postgresql"
542     ];
544     system.checks = lib.optional (cfg.checkConfig && pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) configFileCheck;
546     systemd.services.postgresql =
547       { description = "PostgreSQL Server";
549         wantedBy = [ "multi-user.target" ];
550         after = [ "network.target" ];
552         environment.PGDATA = cfg.dataDir;
554         path = [ postgresql ];
556         preStart =
557           ''
558             if ! test -e ${cfg.dataDir}/PG_VERSION; then
559               # Cleanup the data directory.
560               rm -f ${cfg.dataDir}/*.conf
562               # Initialise the database.
563               initdb -U ${cfg.superUser} ${escapeShellArgs cfg.initdbArgs}
565               # See postStart!
566               touch "${cfg.dataDir}/.first_startup"
567             fi
569             ln -sfn "${configFile}/postgresql.conf" "${cfg.dataDir}/postgresql.conf"
570             ${optionalString (cfg.recoveryConfig != null) ''
571               ln -sfn "${pkgs.writeText "recovery.conf" cfg.recoveryConfig}" \
572                 "${cfg.dataDir}/recovery.conf"
573             ''}
574           '';
576         # Wait for PostgreSQL to be ready to accept connections.
577         postStart =
578           ''
579             PSQL="psql --port=${builtins.toString cfg.settings.port}"
581             while ! $PSQL -d postgres -c "" 2> /dev/null; do
582                 if ! kill -0 "$MAINPID"; then exit 1; fi
583                 sleep 0.1
584             done
586             if test -e "${cfg.dataDir}/.first_startup"; then
587               ${optionalString (cfg.initialScript != null) ''
588                 $PSQL -f "${cfg.initialScript}" -d postgres
589               ''}
590               rm -f "${cfg.dataDir}/.first_startup"
591             fi
592           '' + optionalString (cfg.ensureDatabases != []) ''
593             ${concatMapStrings (database: ''
594               $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${database}'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "${database}"'
595             '') cfg.ensureDatabases}
596           '' + ''
597             ${
598               concatMapStrings
599               (user:
600               let
601                   dbOwnershipStmt = optionalString
602                     user.ensureDBOwnership
603                     ''$PSQL -tAc 'ALTER DATABASE "${user.name}" OWNER TO "${user.name}";' '';
605                   filteredClauses = filterAttrs (name: value: value != null) user.ensureClauses;
607                   clauseSqlStatements = attrValues (mapAttrs (n: v: if v then n else "no${n}") filteredClauses);
609                   userClauses = ''$PSQL -tAc 'ALTER ROLE "${user.name}" ${concatStringsSep " " clauseSqlStatements}' '';
610                 in ''
611                   $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || $PSQL -tAc 'CREATE USER "${user.name}"'
612                   ${userClauses}
614                   ${dbOwnershipStmt}
615                 ''
616               )
617               cfg.ensureUsers
618             }
619           '';
621         serviceConfig = mkMerge [
622           { ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
623             User = "postgres";
624             Group = "postgres";
625             RuntimeDirectory = "postgresql";
626             Type = if versionAtLeast cfg.package.version "9.6"
627                    then "notify"
628                    else "simple";
630             # Shut down Postgres using SIGINT ("Fast Shutdown mode").  See
631             # https://www.postgresql.org/docs/current/server-shutdown.html
632             KillSignal = "SIGINT";
633             KillMode = "mixed";
635             # Give Postgres a decent amount of time to clean up after
636             # receiving systemd's SIGINT.
637             TimeoutSec = 120;
639             ExecStart = "${postgresql}/bin/postgres";
641             # Hardening
642             CapabilityBoundingSet = [ "" ];
643             DevicePolicy = "closed";
644             PrivateTmp = true;
645             ProtectHome = true;
646             ProtectSystem = "strict";
647             MemoryDenyWriteExecute = lib.mkDefault (cfg.settings.jit == "off" && (!any extensionInstalled [ "plv8" ]));
648             NoNewPrivileges = true;
649             LockPersonality = true;
650             PrivateDevices = true;
651             PrivateMounts = true;
652             ProcSubset = "pid";
653             ProtectClock = true;
654             ProtectControlGroups = true;
655             ProtectHostname = true;
656             ProtectKernelLogs = true;
657             ProtectKernelModules = true;
658             ProtectKernelTunables = true;
659             ProtectProc = "invisible";
660             RemoveIPC = true;
661             RestrictAddressFamilies = [
662               "AF_INET"
663               "AF_INET6"
664               "AF_NETLINK" # used for network interface enumeration
665               "AF_UNIX"
666             ];
667             RestrictNamespaces = true;
668             RestrictRealtime = true;
669             RestrictSUIDSGID = true;
670             SystemCallArchitectures = "native";
671             SystemCallFilter =
672               [
673                 "@system-service"
674                 "~@privileged @resources"
675               ]
676               ++ lib.optionals (any extensionInstalled [ "plv8" ]) [ "@pkey" ];
677             UMask = if groupAccessAvailable then "0027" else "0077";
678           }
679           (mkIf (cfg.dataDir != "/var/lib/postgresql") {
680             ReadWritePaths = [ cfg.dataDir ];
681           })
682           (mkIf (cfg.dataDir == "/var/lib/postgresql/${cfg.package.psqlSchema}") {
683             StateDirectory = "postgresql postgresql/${cfg.package.psqlSchema}";
684             StateDirectoryMode = if groupAccessAvailable then "0750" else "0700";
685           })
686         ];
688         unitConfig.RequiresMountsFor = "${cfg.dataDir}";
689       };
691   };
693   meta.doc = ./postgresql.md;
694   meta.maintainers = with lib.maintainers; [ thoughtpolice danbst ];