diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index e9b9664f8e7..1a1dbc16ab8 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -949,6 +949,7 @@
./services/web-servers/nginx/default.nix
./services/web-servers/nginx/gitweb.nix
./services/web-servers/phpfpm/default.nix
+ ./services/web-servers/pomerium.nix
./services/web-servers/unit/default.nix
./services/web-servers/shellinabox.nix
./services/web-servers/tomcat.nix
diff --git a/nixos/modules/services/web-servers/pomerium.nix b/nixos/modules/services/web-servers/pomerium.nix
new file mode 100644
index 00000000000..a96df1dbf6d
--- /dev/null
+++ b/nixos/modules/services/web-servers/pomerium.nix
@@ -0,0 +1,131 @@
+{ 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 the Pomerium
+ configuration reference 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;
+
+ serviceConfig = {
+ DynamicUser = true;
+ StateDirectory = [ "pomerium" ];
+ ExecStart = "${pkgs.pomerium}/bin/pomerium -config ${cfgFile}";
+
+ 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" ];
+
+ WorkingDirectory = mkIf (cfg.useACMEHost != null) "$CREDENTIALS_DIRECTORY";
+ 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 = [ "${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 restart pomerium.service";
+ };
+ };
+ });
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index f7f5841b9ac..c851ae9cefb 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -319,6 +319,7 @@ in
plikd = handleTest ./plikd.nix {};
plotinus = handleTest ./plotinus.nix {};
podman = handleTestOn ["x86_64-linux"] ./podman.nix {};
+ pomerium = handleTestOn ["x86_64-linux"] ./pomerium.nix {};
postfix = handleTest ./postfix.nix {};
postfix-raise-smtpd-tls-security-level = handleTest ./postfix-raise-smtpd-tls-security-level.nix {};
postgis = handleTest ./postgis.nix {};
diff --git a/nixos/tests/pomerium.nix b/nixos/tests/pomerium.nix
new file mode 100644
index 00000000000..933614bb7d8
--- /dev/null
+++ b/nixos/tests/pomerium.nix
@@ -0,0 +1,102 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+ name = "pomerium";
+ meta = with pkgs.stdenv.lib.maintainers; {
+ maintainers = [ lukegb ];
+ };
+
+ nodes = let base = myIP: { pkgs, lib, ... }: {
+ virtualisation.vlans = [ 1 ];
+ networking = {
+ dhcpcd.enable = false;
+ firewall.allowedTCPPorts = [ 80 443 ];
+ hosts = {
+ "192.168.1.1" = [ "pomerium" "pom-auth" ];
+ "192.168.1.2" = [ "backend" "dummy-oidc" ];
+ };
+ interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+ { address = myIP; prefixLength = 24; }
+ ];
+ };
+ }; in {
+ pomerium = { pkgs, lib, ... }: {
+ imports = [ (base "192.168.1.1") ];
+ services.pomerium = {
+ enable = true;
+ settings = {
+ address = ":80";
+ insecure_server = true;
+ authenticate_service_url = "http://pom-auth";
+
+ idp_provider = "oidc";
+ idp_scopes = [ "oidc" ];
+ idp_client_id = "dummy";
+ idp_provider_url = "http://dummy-oidc";
+
+ policy = [{
+ from = "https://my.website";
+ to = "http://192.168.1.2";
+ allow_public_unauthenticated_access = true;
+ preserve_host_header = true;
+ } {
+ from = "https://login.required";
+ to = "http://192.168.1.2";
+ allowed_domains = [ "my.domain" ];
+ preserve_host_header = true;
+ }];
+ };
+ secretsFile = pkgs.writeText "pomerium-secrets" ''
+ # 12345678901234567890123456789012 in base64
+ COOKIE_SECRET=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=
+ IDP_CLIENT_SECRET=dummy
+ '';
+ };
+ };
+ backend = { pkgs, lib, ... }: {
+ imports = [ (base "192.168.1.2") ];
+ services.nginx.enable = true;
+ services.nginx.virtualHosts."my.website" = {
+ root = pkgs.runCommand "testdir" {} ''
+ mkdir "$out"
+ echo hello world > "$out/index.html"
+ '';
+ };
+ services.nginx.virtualHosts."dummy-oidc" = {
+ root = pkgs.runCommand "testdir" {} ''
+ mkdir -p "$out/.well-known"
+ cat <"$out/.well-known/openid-configuration"
+ {
+ "issuer": "http://dummy-oidc",
+ "authorization_endpoint": "http://dummy-oidc/auth.txt",
+ "token_endpoint": "http://dummy-oidc/token",
+ "jwks_uri": "http://dummy-oidc/jwks.json",
+ "userinfo_endpoint": "http://dummy-oidc/userinfo",
+ "id_token_signing_alg_values_supported": ["RS256"]
+ }
+ EOF
+ echo hello I am login page >"$out/auth.txt"
+ '';
+ };
+ };
+ };
+
+ testScript = { ... }: ''
+ backend.wait_for_unit("nginx")
+ backend.wait_for_open_port(80)
+
+ pomerium.wait_for_unit("pomerium")
+ pomerium.wait_for_open_port(80)
+
+ with subtest("no authentication required"):
+ pomerium.succeed(
+ "curl --resolve my.website:80:127.0.0.1 http://my.website | grep -q 'hello world'"
+ )
+
+ with subtest("login required"):
+ pomerium.succeed(
+ "curl -I --resolve login.required:80:127.0.0.1 http://login.required | grep -q pom-auth"
+ )
+ pomerium.succeed(
+ "curl -L --resolve login.required:80:127.0.0.1 http://login.required | grep -q 'hello I am login page'"
+ )
+ '';
+})
diff --git a/pkgs/servers/http/envoy/default.nix b/pkgs/servers/http/envoy/default.nix
index 3a453528151..e6ecbb86860 100644
--- a/pkgs/servers/http/envoy/default.nix
+++ b/pkgs/servers/http/envoy/default.nix
@@ -6,6 +6,7 @@
, go
, ninja
, python3
+, nixosTests
}:
let
@@ -110,6 +111,11 @@ buildBazelPackage rec {
"--cxxopt=-Wno-uninitialized"
];
+ passthru.tests = {
+ # No tests for Envoy itself (yet), but it's tested as a core component of Pomerium.
+ inherit (nixosTests) pomerium;
+ };
+
meta = with lib; {
homepage = "https://envoyproxy.io";
description = "Cloud-native edge and service proxy";
diff --git a/pkgs/servers/http/pomerium/default.nix b/pkgs/servers/http/pomerium/default.nix
new file mode 100644
index 00000000000..0605a12eca4
--- /dev/null
+++ b/pkgs/servers/http/pomerium/default.nix
@@ -0,0 +1,80 @@
+{ buildGoModule
+, fetchFromGitHub
+, lib
+, envoy
+, zip
+, nixosTests
+}:
+
+let
+ inherit (lib) concatStringsSep mapAttrsToList;
+in
+buildGoModule rec {
+ pname = "pomerium";
+ version = "0.13.3";
+ src = fetchFromGitHub {
+ owner = "pomerium";
+ repo = "pomerium";
+ rev = "v${version}";
+ hash = "sha256-g0w1aIHvf2rJANvGWHeUxdnyCDsvy/PQ9Kp8nDdT/0w=";
+ };
+
+ vendorSha256 = "sha256-grihU85OcGyf9/KKrv87xZonX5r+Z1oHQTf84Ya61fg=";
+ subPackages = [
+ "cmd/pomerium"
+ "cmd/pomerium-cli"
+ ];
+
+ buildFlagsArray = let
+ # Set a variety of useful meta variables for stamping the build with.
+ setVars = {
+ Version = "v${version}";
+ BuildMeta = "nixpkgs";
+ ProjectName = "pomerium";
+ ProjectURL = "github.com/pomerium/pomerium";
+ };
+ varFlags = concatStringsSep " " (mapAttrsToList (name: value: "-X github.com/pomerium/pomerium/internal/version.${name}=${value}") setVars);
+ in [
+ "-ldflags=${varFlags}"
+ ];
+
+ nativeBuildInputs = [
+ zip
+ ];
+
+ # Pomerium expects to have envoy append to it in a zip.
+ # We use a store-only (-0) zip, so that the Nix scanner can find any store references we had in the envoy binary.
+ postBuild = ''
+ # Append Envoy
+ pushd $NIX_BUILD_TOP
+ mkdir -p envoy
+ cd envoy
+ cp ${envoy}/bin/envoy envoy
+ zip -0 envoy.zip envoy
+ popd
+
+ mv $GOPATH/bin/pomerium $GOPATH/bin/pomerium.old
+ cat $GOPATH/bin/pomerium.old $NIX_BUILD_TOP/envoy/envoy.zip >$GOPATH/bin/pomerium
+ zip --adjust-sfx $GOPATH/bin/pomerium
+ '';
+
+ # We also need to set dontStrip to avoid having the envoy ZIP stripped off the end.
+ dontStrip = true;
+
+ installPhase = ''
+ install -Dm0755 $GOPATH/bin/pomerium $out/bin/pomerium
+ install -Dm0755 $GOPATH/bin/pomerium-cli $out/bin/pomerium-cli
+ '';
+
+ passthru.tests = {
+ inherit (nixosTests) pomerium;
+ };
+
+ meta = with lib; {
+ homepage = "https://pomerium.io";
+ description = "Authenticating reverse proxy";
+ license = licenses.asl20;
+ maintainers = with maintainers; [ lukegb ];
+ platforms = [ "x86_64-linux" ]; # Envoy derivation is x86_64-linux only.
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index a146a81dd4d..881d5abd3ce 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -18535,6 +18535,8 @@ in
};
pflogsumm = callPackage ../servers/mail/postfix/pflogsumm.nix { };
+ pomerium = callPackage ../servers/http/pomerium { };
+
postgrey = callPackage ../servers/mail/postgrey { };
pshs = callPackage ../servers/http/pshs { };