+30
firewall/default.nix
+30
firewall/default.nix
···
1
+
{lib, config, options, ...}: let
2
+
l = lib;
3
+
t = l.types;
4
+
cfg = config.networking.firewall.public;
5
+
6
+
portOptions = {
7
+
inherit (options.networking.firewall)
8
+
allowedTCPPorts
9
+
allowedUDPPorts
10
+
allowedTCPPortRanges
11
+
allowedUDPPortRanges;
12
+
};
13
+
in {
14
+
options = {
15
+
networking.firewall.public = l.mkOption {
16
+
default = { };
17
+
type = t.attrsOf (t.submodule [{ options = portOptions; }]);
18
+
description = "Tagged open port sets.";
19
+
};
20
+
};
21
+
22
+
config = let
23
+
concatAll = name: l.concatLists (l.mapAttrsToList (_: opts: opts.${name}) cfg);
24
+
in {
25
+
networking.firewall.allowedTCPPorts = concatAll "allowedTCPPorts";
26
+
networking.firewall.allowedTCPPortRanges = concatAll "allowedTCPPortRanges";
27
+
networking.firewall.allowedUDPPorts = concatAll "allowedUDPPorts";
28
+
networking.firewall.allowedUDPPortRanges = concatAll "allowedUDPPortRanges";
29
+
};
30
+
}
+37
firewall/provider/hetzner/app.nu
+37
firewall/provider/hetzner/app.nu
···
1
+
use std/log
2
+
3
+
let authHeader = ["authorization" $"Bearer ($env.HETZNER_API_TOKEN)"]
4
+
5
+
def makeApiUrl [path: string] {
6
+
return $"https://api.hetzner.cloud/v1($path)"
7
+
}
8
+
def post [path: string] {
9
+
let resp = $in | http post -e --full -H authHeader --content-type application/json (makeApiUrl path)
10
+
$resp.body = $resp.body | from json
11
+
$resp
12
+
}
13
+
def get [path: string] {
14
+
let resp = http get -e --full -H authHeader (makeApiUrl path)
15
+
$resp.body = $resp.body | from json
16
+
$resp
17
+
}
18
+
19
+
# first fetch firewall to see if it even exists
20
+
let resp = get $"/firewalls/($firewallId)"
21
+
if $resp.status == 404 {
22
+
log error $"provided firewall \(id ($firewallId)\) does not exist"
23
+
exit 1
24
+
}
25
+
let firewall = $resp.body | get firewall
26
+
27
+
# backup firewall
28
+
let backupPath = $".hetzner/($firewallId).json"
29
+
mkdir .hetzner; $firewall | to json | save $backupPath
30
+
log info $"backing up firewall ($firewallId) to ($backupPath)"
31
+
32
+
# apply rules
33
+
let resp = open $rulesFile | from json | post $"/firewalls/($firewallId)/actions/set_rules"
34
+
if $resp.status != 201 {
35
+
log error $"could not apply firewall \(id ($firewallId)\)"
36
+
}
37
+
log info $"applied firewall ($firewallId)"
+56
firewall/provider/hetzner/default.nix
+56
firewall/provider/hetzner/default.nix
···
1
+
{pkgs, lib, config, options, ...}: let
2
+
l = lib;
3
+
t = l.types;
4
+
taggedPorts = config.networking.firewall.public;
5
+
cfg = config.providers.hetzner;
6
+
in {
7
+
options = {
8
+
providers.hetzner.firewall = {
9
+
id = l.mkOption {
10
+
type = t.ints.unsigned;
11
+
description = "The ID of the firewall to update.";
12
+
};
13
+
app = l.mkOption {
14
+
type = t.package;
15
+
readOnly = true;
16
+
description = ''
17
+
The generated app for this provider, run it to apply the configuration.
18
+
19
+
For this to work, you need to set the `HETZNER_API_TOKEN` environment variable to a valid API token from Hetzner.
20
+
'';
21
+
};
22
+
};
23
+
};
24
+
25
+
config = let
26
+
mkRule = proto: tag: port: {
27
+
description = tag;
28
+
direction = "in";
29
+
protocol = proto;
30
+
port =
31
+
if l.isAttrs port
32
+
then l.concatMapStringsSep "-" toString [port.from port.to]
33
+
else toString port;
34
+
};
35
+
mkTcpRule = mkRule "tcp";
36
+
mkUdpRule = mkRule "udp";
37
+
firewallRules = pkgs.writers.writeJSON "hetzner-firewall-${toString cfg.id}-rules.json" {
38
+
rules = l.flatten (
39
+
l.mapAttrsToList
40
+
(tag: ports: [
41
+
(l.map (mkTcpRule tag) ports.allowedTCPPorts)
42
+
(l.map (mkTcpRule tag) ports.allowedTCPPortRanges)
43
+
(l.map (mkUdpRule tag) ports.allowedUDPPorts)
44
+
(l.map (mkUdpRule tag) ports.allowedUDPPortRanges)
45
+
])
46
+
taggedPorts
47
+
);
48
+
};
49
+
in {
50
+
providers.hetzner.firewall.app = pkgs.writers.writeNu "apply-hetzner" ''
51
+
let firewallId = ${toString cfg.id}
52
+
let rulesFile = ${firewallRules}
53
+
${l.fileContents ./app.nu}
54
+
'';
55
+
};
56
+
}
+27
flake.lock
+27
flake.lock
···
1
+
{
2
+
"nodes": {
3
+
"nixpkgs": {
4
+
"locked": {
5
+
"lastModified": 1752480373,
6
+
"narHash": "sha256-JHQbm+OcGp32wAsXTE/FLYGNpb+4GLi5oTvCxwSoBOA=",
7
+
"owner": "nixos",
8
+
"repo": "nixpkgs",
9
+
"rev": "62e0f05ede1da0d54515d4ea8ce9c733f12d9f08",
10
+
"type": "github"
11
+
},
12
+
"original": {
13
+
"owner": "nixos",
14
+
"ref": "nixos-unstable",
15
+
"repo": "nixpkgs",
16
+
"type": "github"
17
+
}
18
+
},
19
+
"root": {
20
+
"inputs": {
21
+
"nixpkgs": "nixpkgs"
22
+
}
23
+
}
24
+
},
25
+
"root": "root",
26
+
"version": 7
27
+
}
+12
flake.nix
+12
flake.nix
···
1
+
{
2
+
description = "nixos modules for convenient deployment of cloud resources";
3
+
4
+
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
5
+
6
+
outputs = inp: {
7
+
nixosModules = {
8
+
firewall = ./firewall/default.nix;
9
+
firewall-hetzner = ./firewall/provider/hetzner/default.nix;
10
+
};
11
+
};
12
+
}