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