{ lib, pkgs, config, ... }: let cfg = config.myServices.databases.postgresql; in { options.myServices.databases = { postgresql = { enable = lib.mkOption { default = false; example = true; description = "Whether to enable postgresql database"; type = lib.types.bool; }; package = lib.mkOption { type = lib.types.package; default = pkgs.postgresql; description = '' Postgresql package to use. ''; }; ldapConfig = lib.mkOption { description = "LDAP configuration to allow PAM identification via LDAP"; type = lib.types.submodule { options = { host = lib.mkOption { type = lib.types.str; }; base = lib.mkOption { type = lib.types.str; }; dn = lib.mkOption { type = lib.types.str; }; password = lib.mkOption { type = lib.types.str; }; filter = lib.mkOption { type = lib.types.str; }; }; }; }; replicationLdapConfig = lib.mkOption { description = "LDAP configuration to allow replication"; type = lib.types.submodule { options = { host = lib.mkOption { type = lib.types.str; }; base = lib.mkOption { type = lib.types.str; }; dn = lib.mkOption { type = lib.types.str; }; password = lib.mkOption { type = lib.types.str; }; }; }; }; authorizedHosts = lib.mkOption { default = {}; description = "Hosts to allow connections from"; type = lib.types.attrsOf (lib.types.listOf (lib.types.submodule { options = { method = lib.mkOption { default = "md5"; type = lib.types.str; }; username = lib.mkOption { default = "all"; type = lib.types.str; }; database = lib.mkOption { default = "all"; type = lib.types.str; }; ip4 = lib.mkOption { default = []; type = lib.types.listOf lib.types.str; }; ip6 = lib.mkOption { default = []; type = lib.types.listOf lib.types.str; }; }; })); }; replicationHosts = lib.mkOption { default = {}; description = "Hosts to allow replication from"; type = lib.types.attrsOf (lib.types.submodule { options = { ip4 = lib.mkOption { type = lib.types.listOf lib.types.str; }; ip6 = lib.mkOption { type = lib.types.listOf lib.types.str; }; }; }); }; # Output variables socketsDir = lib.mkOption { type = lib.types.path; default = "/run/postgresql"; description = '' The directory where Postgresql puts sockets. ''; readOnly = true; }; }; }; config = lib.mkIf cfg.enable { networking.firewall.allowedTCPPorts = [ 5432 ]; security.acme.certs."postgresql" = { group = "postgres"; domain = "db-1.immae.eu"; postRun = '' systemctl reload postgresql.service ''; }; systemd.services.postgresql.serviceConfig = { SupplementaryGroups = "keys"; ExecStartPre = [ ("+" + (pkgs.writeShellScript "postgresql-fix-cert" '' # postgresql complains: # private key file "${config.security.acme.certs.postgresql.directory}/key.pem" must be owned by the database user or root cp -f "${config.security.acme.certs.postgresql.directory}/key.pem" "${config.services.postgresql.dataDir}/key.pem" chown postgres:postgres "${config.services.postgresql.dataDir}/key.pem" chmod go-r "${config.services.postgresql.dataDir}/key.pem" '')) ]; }; systemd.services.postgresql.postStart = lib.mkAfter '' # This line is already defined in 19.09 PSQL="psql --port=5432" ${builtins.concatStringsSep "\n" (lib.mapAttrsToList (role: _: '' $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${role}'" \ | grep -q 1 \ || $PSQL -tAc 'CREATE USER "${role}" WITH REPLICATION' '') cfg.replicationHosts)} ${builtins.concatStringsSep "\n" (lib.mapAttrsToList (role: _: let sname = builtins.replaceStrings ["-"] ["_"] role; in '' $PSQL -tAc "SELECT 1 FROM pg_replication_slots WHERE slot_name='${sname}'" \ | grep -q 1 \ || $PSQL -tAc "SELECT * FROM pg_create_physical_replication_slot('${sname}')" '') cfg.replicationHosts)} ''; services.postgresql = { enable = true; package = cfg.package; enableTCPIP = true; checkConfig = false; logLinePrefix = "%h %q%u@%d "; # Default: '%m [%p] ', already stored independently by journald. %h needed for fail2ban settings = { max_connections = 300; wal_level = "logical"; shared_buffers = "512MB"; work_mem = "10MB"; max_wal_size = "1GB"; min_wal_size = "80MB"; log_timezone = "Europe/Paris"; datestyle = "iso, mdy"; timezone = "Europe/Paris"; lc_messages = "en_US.UTF-8"; lc_monetary = "en_US.UTF-8"; lc_numeric = "en_US.UTF-8"; lc_time = "en_US.UTF-8"; default_text_search_config = "pg_catalog.english"; # this introduces a small delay before storing on disk, but # makes it order of magnitudes quicker synchronous_commit = "off"; ssl = "on"; ssl_cert_file = "${config.security.acme.certs.postgresql.directory}/fullchain.pem"; ssl_key_file = "${config.services.postgresql.dataDir}/key.pem"; }; authentication = let hosts = builtins.concatStringsSep "\n" ( lib.lists.flatten (lib.mapAttrsToList (k: vs: map (v: map (ip6: "hostssl ${v.database} ${v.username} ${ip6} ${v.method}") v.ip6 ++ map (ip4: "hostssl ${v.database} ${v.username} ${ip4}/32 ${v.method}") v.ip4 ) vs) cfg.authorizedHosts )); replication = builtins.concatStringsSep "\n" ( lib.lists.flatten (lib.mapAttrsToList (k: v: map (ip6: "hostssl replication ${k} ${ip6}/128 pam pamservice=postgresql_replication") v.ip6 ++ map (ip4: "hostssl replication ${k} ${ip4}/32 pam pamservice=postgresql_replication") v.ip4 ) cfg.replicationHosts )); in '' local all postgres ident local all all md5 ${hosts} hostssl all all all pam ${replication} ''; }; secrets.keys = { "postgresql/pam" = { permissions = "0400"; group = "postgres"; user = "postgres"; text = with cfg.ldapConfig; '' host ${host} base ${base} binddn ${dn} bindpw ${password} pam_filter ${filter} ssl start_tls ''; }; "postgresql/pam_replication" = { permissions = "0400"; group = "postgres"; user = "postgres"; text = with cfg.replicationLdapConfig; '' host ${host} base ${base} binddn ${dn} bindpw ${password} pam_login_attribute cn ssl start_tls ''; }; }; security.pam.services = let pam_ldap = "${pkgs.pam_ldap}/lib/security/pam_ldap.so"; in { postgresql = { text = '' auth required ${pam_ldap} config=${config.secrets.fullPaths."postgresql/pam"} account required ${pam_ldap} config=${config.secrets.fullPaths."postgresql/pam"} ''; }; postgresql_replication = { text = '' auth required ${pam_ldap} config=${config.secrets.fullPaths."postgresql/pam_replication"} account required ${pam_ldap} config=${config.secrets.fullPaths."postgresql/pam_replication"} ''; }; }; }; }