summaryrefslogtreecommitdiff
path: root/modules/nixos/cloudflare-proxy/default.nix
blob: 657722dfaddeefe4010206d9cff036e5fc2468da (plain)
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
# This module adds some extra configuration useful when running behid a Cloudflare Proxy.
#
{
  config,
  lib,
  pkgs,
  ...
}: let
  inherit (lib.options) mkEnableOption mkOption;
  inherit (lib.modules) mkIf;
  inherit (lib.types) listOf nonEmptyStr port;

  # TODO: What happens when these get out of date??? Huh??? You little pissbaby
  fileToList = x: lib.strings.splitString "\n" (builtins.readFile x);
  cfipv4 = fileToList (pkgs.fetchurl {
    url = "https://www.cloudflare.com/ips-v4";
    hash = "sha256-8Cxtg7wBqwroV3Fg4DbXAMdFU1m84FTfiE5dfZ5Onns=";
  });
  cfipv6 = fileToList (pkgs.fetchurl {
    url = "https://www.cloudflare.com/ips-v6";
    hash = "sha256-np054+g7rQDE3sr9U8Y/piAp89ldto3pN9K+KCNMoKk=";
  });

  cfg = config.modules.cloudflare-proxy;
in {
  options.modules.cloudflare-proxy = {
    enable = mkEnableOption "Cloudflare proxy IP extraction for NGINX";

    firewall = {
      IPv4Whitelist = mkOption {
        description = "List of IPv4 addresses (or ranges) added to the whitelist.";
        type = listOf nonEmptyStr;
        default = [];
      };

      IPv6Whitelist = mkOption {
        description = "List of IPv6 addresses (or ranges) added to the whitelist.";
        type = listOf nonEmptyStr;
        default = [];
      };
    };
  };

  config = mkIf cfg.enable {
    # Teach NGINX how to extract the proxied IP from proxied requests.
    #
    # See: https://nixos.wiki/wiki/Nginx#Using_realIP_when_behind_CloudFlare_or_other_CDN
    services.nginx.commonHttpConfig = let
      realIpsFromList = lib.strings.concatMapStringsSep "\n" (x: "set_real_ip_from  ${x};");
    in ''
      ${realIpsFromList cfipv4}
      ${realIpsFromList cfipv6}
      real_ip_header CF-Connecting-IP;
    '';

    # Block non-Cloudflare IP addresses.
    networking.firewall = let
      chain = "cloudflare-whitelist";
    in {
      extraCommands = let
        allow-interface = lib.strings.concatMapStringsSep "\n" (i: ''ip46tables --append ${chain} --in-interface ${i} --jump RETURN'');
        allow-ip = cmd: lib.strings.concatMapStringsSep "\n" (r: ''${cmd} --append ${chain} --source ${r} --jump RETURN'');
      in ''
        # Flush the old firewall rules. This behavior mirrors the default firewall service.
        # See: https://github.com/NixOS/nixpkgs/blob/ac911bf685eecc17c2df5b21bdf32678b9f88c92/nixos/modules/services/networking/firewall-iptables.nix#L59-L66
        # TEMP: Removed 2>/dev/null
        ip46tables --delete INPUT --protocol tcp --destination-port 80 --syn --jump ${chain} || true
        ip46tables --delete INPUT --protocol tcp --destination-port 443 --syn --jump ${chain} || true
        ip46tables --flush ${chain} || true
        ip46tables --delete-chain ${chain} || true

        # Create a chain that only allows whitelisted IPs through.
        ip46tables --new-chain ${chain}

        # Allow trusted interfaces through.
        ${allow-interface config.networking.firewall.trustedInterfaces}

        # Allow local whitelisted IPs through
        ${allow-ip "iptables" cfg.firewall.IPv4Whitelist}
        ${allow-ip "ip6tables" cfg.firewall.IPv6Whitelist}

        # Allow Cloudflare's IP ranges through.
        ${allow-ip "iptables" cfipv4}
        ${allow-ip "ip6tables" cfipv6}

        # Everything else is dropped.
        #
        # TODO: I would like to use `nixos-fw-log-refuse` here, but I keep
        #       running into weird issues when reloading the firewall.
        #       Something about the table not being deleted properly.
        ip46tables --append ${chain} --jump DROP

        # Inject our chain as the first check in INPUT (before nixos-fw).
        # We want to capture any new incomming TCP connections.
        ip46tables --insert INPUT 1 --protocol tcp --destination-port 80 --syn --jump ${chain}
        ip46tables --insert INPUT 1 --protocol tcp --destination-port 443 --syn --jump ${chain}
      '';
      extraStopCommands = ''
        # Clean up added rulesets (${chain}). This mirrors the behavior of the
        # default firewall at the time of writing.
        #
        # See: https://github.com/NixOS/nixpkgs/blob/ac911bf685eecc17c2df5b21bdf32678b9f88c92/nixos/modules/services/networking/firewall-iptables.nix#L218-L219
        # TEMP: Removed 2>/dev/null
        ip46tables --delete INPUT --protocol tcp --destination-port 80 --syn --jump ${chain}  || true
        ip46tables --delete INPUT --protocol tcp --destination-port 443 --syn --jump ${chain} || true
      '';
    };
  };
}