summary refs log tree commit diff
path: root/nixos/modules/services/web-servers/pomerium.nix
blob: 0b460755f50ef9ee9e13d871ddcfbf8b16084111 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
{ config, lib, pkgs, ... }:

with lib;

let
  format = pkgs.formats.yaml {};
in
{
  options.services.pomerium = {
    enable = mkEnableOption "the Pomerium authenticating reverse proxy";

    configFile = mkOption {
      type = with types; nullOr path;
      default = null;
      description = "Path to Pomerium config YAML. If set, overrides services.pomerium.settings.";
    };

    useACMEHost = mkOption {
      type = with types; nullOr str;
      default = null;
      description = ''
        If set, use a NixOS-generated ACME certificate with the specified name.

        Note that this will require you to use a non-HTTP-based challenge, or
        disable Pomerium's in-built HTTP redirect server by setting
        http_redirect_addr to null and use a different HTTP server for serving
        the challenge response.

        If you're using an HTTP-based challenge, you should use the
        Pomerium-native autocert option instead.
      '';
    };

    settings = mkOption {
      description = ''
        The contents of Pomerium's config.yaml, in Nix expressions.

        Specifying configFile will override this in its entirety.

        See <link xlink:href="https://pomerium.io/reference/">the Pomerium
        configuration reference</link> for more information about what to put
        here.
      '';
      default = {};
      type = format.type;
    };

    secretsFile = mkOption {
      type = with types; nullOr path;
      default = null;
      description = ''
        Path to file containing secrets for Pomerium, in systemd
        EnvironmentFile format. See the systemd.exec(5) man page.
      '';
    };
  };

  config = let
    cfg = config.services.pomerium;
    cfgFile = if cfg.configFile != null then cfg.configFile else (format.generate "pomerium.yaml" cfg.settings);
  in mkIf cfg.enable ({
    systemd.services.pomerium = {
      description = "Pomerium authenticating reverse proxy";
      wants = [ "network.target" ] ++ (optional (cfg.useACMEHost != null) "acme-finished-${cfg.useACMEHost}.target");
      after = [ "network.target" ] ++ (optional (cfg.useACMEHost != null) "acme-finished-${cfg.useACMEHost}.target");
      wantedBy = [ "multi-user.target" ];
      environment = optionalAttrs (cfg.useACMEHost != null) {
        CERTIFICATE_FILE = "fullchain.pem";
        CERTIFICATE_KEY_FILE = "key.pem";
      };
      startLimitIntervalSec = 60;
      script = ''
        if [[ -v CREDENTIALS_DIRECTORY ]]; then
          cd "$CREDENTIALS_DIRECTORY"
        fi
        exec "${pkgs.pomerium}/bin/pomerium" -config "${cfgFile}"
      '';

      serviceConfig = {
        DynamicUser = true;
        StateDirectory = [ "pomerium" ];

        PrivateUsers = false;  # breaks CAP_NET_BIND_SERVICE
        MemoryDenyWriteExecute = false;  # breaks LuaJIT

        NoNewPrivileges = true;
        PrivateTmp = true;
        PrivateDevices = true;
        DevicePolicy = "closed";
        ProtectSystem = "strict";
        ProtectHome = true;
        ProtectControlGroups = true;
        ProtectKernelModules = true;
        ProtectKernelTunables = true;
        ProtectKernelLogs = true;
        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
        RestrictNamespaces = true;
        RestrictRealtime = true;
        RestrictSUIDSGID = true;
        LockPersonality = true;
        SystemCallArchitectures = "native";

        EnvironmentFile = cfg.secretsFile;
        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];

        LoadCredential = optionals (cfg.useACMEHost != null) [
          "fullchain.pem:/var/lib/acme/${cfg.useACMEHost}/fullchain.pem"
          "key.pem:/var/lib/acme/${cfg.useACMEHost}/key.pem"
        ];
      };
    };

    # postRun hooks on cert renew can't be used to restart Nginx since renewal
    # runs as the unprivileged acme user. sslTargets are added to wantedBy + before
    # which allows the acme-finished-$cert.target to signify the successful updating
    # of certs end-to-end.
    systemd.services.pomerium-config-reload = mkIf (cfg.useACMEHost != null) {
      # TODO(lukegb): figure out how to make config reloading work with credentials.

      wantedBy = [ "acme-finished-${cfg.useACMEHost}.target" "multi-user.target" ];
      # Before the finished targets, after the renew services.
      before = [ "acme-finished-${cfg.useACMEHost}.target" ];
      after = [ "acme-${cfg.useACMEHost}.service" ];
      # Block reloading if not all certs exist yet.
      unitConfig.ConditionPathExists = [ "${config.security.acme.certs.${cfg.useACMEHost}.directory}/fullchain.pem" ];
      serviceConfig = {
        Type = "oneshot";
        TimeoutSec = 60;
        ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active pomerium.service";
        ExecStart = "/run/current-system/systemd/bin/systemctl --no-block restart pomerium.service";
      };
    };
  });
}