1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 inherit (lib)
10 literalExpression
11 mkEnableOption
12 mkIf
13 mkOption
14 mkPackageOption
15 optionalAttrs
16 optional
17 types
18 ;
19
20 cfg = config.services.legit;
21
22 yaml = pkgs.formats.yaml { };
23 configFile = yaml.generate "legit.yaml" cfg.settings;
24
25 defaultStateDir = "/var/lib/legit";
26 defaultStaticDir = "${cfg.settings.repo.scanPath}/static";
27 defaultTemplatesDir = "${cfg.settings.repo.scanPath}/templates";
28in
29{
30 options.services.legit = {
31 enable = mkEnableOption "legit git web frontend";
32
33 package = mkPackageOption pkgs "legit-web" { };
34
35 user = mkOption {
36 type = types.str;
37 default = "legit";
38 description = "User account under which legit runs.";
39 };
40
41 group = mkOption {
42 type = types.str;
43 default = "legit";
44 description = "Group account under which legit runs.";
45 };
46
47 settings = mkOption {
48 default = { };
49 description = ''
50 The primary legit configuration. See the
51 [sample configuration](https://github.com/icyphox/legit/blob/master/config.yaml)
52 for possible values.
53 '';
54 type = types.submodule {
55 options.repo = {
56 scanPath = mkOption {
57 type = types.path;
58 default = defaultStateDir;
59 description = "Directory where legit will scan for repositories.";
60 };
61 readme = mkOption {
62 type = types.listOf types.str;
63 default = [ ];
64 description = "Readme files to look for.";
65 };
66 mainBranch = mkOption {
67 type = types.listOf types.str;
68 default = [
69 "main"
70 "master"
71 ];
72 description = "Main branch to look for.";
73 };
74 ignore = mkOption {
75 type = types.listOf types.str;
76 default = [ ];
77 description = "Repositories to ignore.";
78 };
79 };
80 options.dirs = {
81 templates = mkOption {
82 type = types.path;
83 default = "${pkgs.legit-web}/lib/legit/templates";
84 defaultText = literalExpression ''"''${pkgs.legit-web}/lib/legit/templates"'';
85 description = "Directories where template files are located.";
86 };
87 static = mkOption {
88 type = types.path;
89 default = "${pkgs.legit-web}/lib/legit/static";
90 defaultText = literalExpression ''"''${pkgs.legit-web}/lib/legit/static"'';
91 description = "Directories where static files are located.";
92 };
93 };
94 options.meta = {
95 title = mkOption {
96 type = types.str;
97 default = "legit";
98 description = "Website title.";
99 };
100 description = mkOption {
101 type = types.str;
102 default = "git frontend";
103 description = "Website description.";
104 };
105 };
106 options.server = {
107 name = mkOption {
108 type = types.str;
109 default = "localhost";
110 description = "Server name.";
111 };
112 host = mkOption {
113 type = types.str;
114 default = "127.0.0.1";
115 description = "Host address.";
116 };
117 port = mkOption {
118 type = types.port;
119 default = 5555;
120 description = "Legit port.";
121 };
122 };
123 };
124 };
125 };
126
127 config = mkIf cfg.enable {
128 users.groups = optionalAttrs (cfg.group == "legit") {
129 "${cfg.group}" = { };
130 };
131
132 users.users = optionalAttrs (cfg.user == "legit") {
133 "${cfg.user}" = {
134 group = cfg.group;
135 isSystemUser = true;
136 };
137 };
138
139 systemd.services.legit = {
140 description = "legit git frontend";
141
142 after = [ "network.target" ];
143 wantedBy = [ "multi-user.target" ];
144 restartTriggers = [ configFile ];
145
146 serviceConfig = {
147 Type = "simple";
148 User = cfg.user;
149 Group = cfg.group;
150 ExecStart = "${cfg.package}/bin/legit -config ${configFile}";
151 Restart = "always";
152
153 WorkingDirectory = cfg.settings.repo.scanPath;
154 StateDirectory =
155 [ ]
156 ++ optional (cfg.settings.repo.scanPath == defaultStateDir) "legit"
157 ++ optional (cfg.settings.dirs.static == defaultStaticDir) "legit/static"
158 ++ optional (cfg.settings.dirs.templates == defaultTemplatesDir) "legit/templates";
159
160 # Hardening
161 CapabilityBoundingSet = [ "" ];
162 DeviceAllow = [ "" ];
163 LockPersonality = true;
164 MemoryDenyWriteExecute = true;
165 NoNewPrivileges = true;
166 PrivateDevices = true;
167 PrivateTmp = true;
168 PrivateUsers = true;
169 ProcSubset = "pid";
170 ProtectClock = true;
171 ProtectControlGroups = true;
172 ProtectHome = true;
173 ProtectHostname = true;
174 ProtectKernelLogs = true;
175 ProtectKernelModules = true;
176 ProtectKernelTunables = true;
177 ProtectProc = "invisible";
178 ProtectSystem = "strict";
179 ReadWritePaths = cfg.settings.repo.scanPath;
180 RemoveIPC = true;
181 RestrictAddressFamilies = [
182 "AF_INET"
183 "AF_INET6"
184 ];
185 RestrictNamespaces = true;
186 RestrictRealtime = true;
187 RestrictSUIDSGID = true;
188 SystemCallArchitectures = "native";
189 SystemCallFilter = [
190 "@system-service"
191 "~@privileged"
192 ];
193 UMask = "0077";
194 };
195 };
196 };
197}