-
Notifications
You must be signed in to change notification settings - Fork 0
/
static-websites.nix
206 lines (169 loc) · 6.68 KB
/
static-websites.nix
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# This module is used by all machines with static websites that come from Git repos.
# REPLACES hologram-nginx
{ config, pkgs, lib, ... }:
with lib;
let
pull-repo-sh = pkgs.writeScript "shove-pull-repo.sh" ''
#!/bin/sh
set -euo pipefail
SOURCE_PROVIDER="$1"
SOURCE_NAME="$2"
CHECKOUT_DIR="$3"
KEY_FILE="${homeDir}/keys/deploy-key-''${SOURCE_NAME/\//-}"
if [ -f "$KEY_FILE" ]; then
SOURCE_URL="git@$SOURCE_PROVIDER:$SOURCE_NAME"
export GIT_SSH_COMMAND="ssh -i $KEY_FILE"
else
SOURCE_URL="https://$SOURCE_PROVIDER/$SOURCE_NAME"
fi
if [ -d "$CHECKOUT_DIR" ]; then
git -C "$CHECKOUT_DIR" pull
else
git clone "$SOURCE_URL" "$CHECKOUT_DIR"
fi
'';
link-repo-sh = pkgs.writeScript "shove-link-repo.sh" ''
#!/bin/sh
set -euo pipefail
CHECKOUT_DIR="$1"
TARGET_DIR="$2"
ln -sTf "''$(readlink -f "$CHECKOUT_DIR")" "$TARGET_DIR"
'';
optionsPerWebsite = {
repositoryProvider = mkOption {
description = "the hostname where the repository is stored";
default = "github.com";
type = types.str;
};
repositoryName = mkOption {
description = "the repository name, usually in the form owner-name/repo-name";
type = types.str;
};
buildCommand = mkOption {
description = "path to alternate build script, invoked with arguments: repository directory, target directory";
default = toString link-repo-sh;
type = types.str;
};
extraCSPs = mkOption {
description = "extra Content-Security-Policy directives to apply to this domain";
default = [];
type = types.listOf types.str;
};
};
cfg = config.my.services.staticweb;
# things that could be options if this were to move to a generic location
homeDir = "/var/lib/shove";
docroot = "${homeDir}/docroot";
secretKeyFile = toString /nix/my/unpacked/generated-shove-secret;
shoveListenPort = 30482;
shove = (pkgs.callPackage ./pkgs/shove/default.nix {});
in {
options.my.services.staticweb = {
sites = mkOption {
description = "static websites that are served from Git repos";
example = {
"doc.example.com" = { repositoryName = "example.com/doc-website"; };
};
default = {};
type = with types; attrsOf (submodule { options = optionsPerWebsite; });
};
};
config = mkIf (cfg.sites != {}) {
environment.systemPackages = with pkgs; [
shove
git # used by /etc/shove/pull-static-website.sh
];
# note that JSON is a subset of YAML
environment.etc."shove/shove.yaml".text = builtins.toJSON {
actions = mapAttrsToList (domainName: { repositoryProvider, repositoryName, buildCommand, ... }: {
name = "pull ${repositoryProvider}/${repositoryName} into ${docroot}/${domainName}";
on = [
{ events = ["shove-startup"]; }
{ events = ["push"]; repos = [repositoryName]; }
];
run = {
command = [
"/bin/sh" "-c"
''"${pull-repo-sh}" "$1" "$2" "$3" && "${buildCommand}" "$3" "$4"''
"shove-update-${domainName}" # this is $0
repositoryProvider
repositoryName
"${homeDir}/checkout/${domainName}" # Git repo directory
"${docroot}/${domainName}" # target directory
];
};
}) cfg.sites;
};
users.groups.shove = {};
users.users.shove = {
group = "shove";
isSystemUser = true;
description = "system user for shove (GitHub webhook handler)";
home = homeDir; # need to be able to write .ssh/known_hosts
};
systemd.services."shove-early" = {
description = "Pre-start script for shove.service that runs with root perms";
serviceConfig = {
Type = "oneshot";
};
path = with pkgs; [ coreutils ];
script = ''
chmod 0755 ${homeDir}
install -d -m 0700 -o shove -g shove ${homeDir}/keys
install -D -m 0600 -o shove -g shove /nix/my/unpacked/deploy-key-* ${homeDir}/keys/
'';
};
systemd.services.shove = {
description = "GitHub webhook handler for static websites";
requires = [ "network-online.target" "shove-early.service" ];
after = [ "network.target" "network-online.target" "shove-early.service" ];
wantedBy = [ "multi-user.target" ];
restartTriggers = [ config.environment.etc."shove/shove.yaml".source ];
path = with pkgs; [ git shove openssh ];
environment = {
SHOVE_PORT = toString shoveListenPort;
SHOVE_CONFIG = "/etc/shove/shove.yaml";
};
serviceConfig = {
ExecStart = "${shove}/bin/shove";
EnvironmentFile = "${secretKeyFile}";
# security hardening
User = "shove";
Group = "shove";
WorkingDirectory = homeDir;
ReadOnlyPaths = "/";
ReadWritePaths = homeDir;
PrivateDevices = "yes";
PrivateTmp = "yes";
};
};
services.nginx.virtualHosts = mapAttrs (domainName: domainOpts: {
forceSSL = true;
enableACME = true;
locations."/.git/".extraConfig = ''
deny all;
return 404;
'';
locations."/".root = "${docroot}/${domainName}";
# for TLDs like example.com, support the alias www.example.com
serverAliases = if (builtins.length (splitString "." domainName)) == 2 then [ "www.${domainName}" ] else [];
extraConfig = let
defaultCSPs = ["default-src 'self' 'unsafe-inline';" "img-src 'self' data:;"];
in ''
charset utf-8;
# recommended HTTP headers according to https://securityheaders.io
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains" always; # six months
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;
add_header Feature-Policy "accelerometer 'none', ambient-light-sensor 'none', autoplay 'none', camera 'none', document-domain 'none', encrypted-media 'none', fullscreen 'none', geolocation 'none', gyroscope 'none', magnetometer 'none', microphone 'none', midi 'none', payment 'none', picture-in-picture 'none', sync-xhr 'none', usb 'none', vibrate 'none', vr 'none'" always;
# CSP includes unsafe-inline to allow <style> tags in hand-written HTML
add_header Content-Security-Policy "${concatStringsSep " " (defaultCSPs ++ domainOpts.extraCSPs)}" always;
# hamper Google surveillance
add_header Permissions-Policy "interest-cohort=()" always;
'';
}) cfg.sites;
my.services.nginx.fqdnLocations."/shove".proxyPass = "http://127.0.0.1:${toString shoveListenPort}";
};
}