Kieran's opinionated (and probably slightly dumb) nix config
1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.atelier.services.battleship-arena;
7in
8{
9 options.atelier.services.battleship-arena = {
10 enable = mkEnableOption "battleship-arena service";
11
12 domain = mkOption {
13 type = types.str;
14 default = "battleship.dunkirk.sh";
15 description = "Domain name for the web interface";
16 };
17
18 sshPort = mkOption {
19 type = types.port;
20 default = 2222;
21 description = "SSH port for battleship arena";
22 };
23
24 webPort = mkOption {
25 type = types.port;
26 default = 8081;
27 description = "Web interface port";
28 };
29
30 uploadDir = mkOption {
31 type = types.str;
32 default = "/var/lib/battleship-arena/submissions";
33 description = "Directory for uploaded submissions";
34 };
35
36 resultsDb = mkOption {
37 type = types.str;
38 default = "/var/lib/battleship-arena/results.db";
39 description = "Path to results database";
40 };
41
42 adminPasscode = mkOption {
43 type = types.str;
44 default = "battleship-admin-override";
45 description = "Admin passcode for batch uploads";
46 };
47
48 secretsFile = mkOption {
49 type = types.nullOr types.path;
50 default = null;
51 description = "Path to agenix secrets file containing BATTLESHIP_ADMIN_PASSCODE";
52 };
53
54 package = mkOption {
55 type = types.package;
56 description = "The battleship-arena package to use";
57 };
58 };
59
60 config = mkIf cfg.enable {
61 users.users.battleship-arena = {
62 isSystemUser = true;
63 group = "battleship-arena";
64 home = "/var/lib/battleship-arena";
65 createHome = true;
66 };
67
68 users.groups.battleship-arena = {};
69
70 systemd.services.battleship-arena = {
71 description = "Battleship Arena SSH/Web Service";
72 after = [ "network.target" ];
73 wantedBy = [ "multi-user.target" ];
74
75 environment = {
76 BATTLESHIP_HOST = "0.0.0.0";
77 BATTLESHIP_SSH_PORT = toString cfg.sshPort;
78 BATTLESHIP_WEB_PORT = toString cfg.webPort;
79 BATTLESHIP_UPLOAD_DIR = cfg.uploadDir;
80 BATTLESHIP_RESULTS_DB = cfg.resultsDb;
81 BATTLESHIP_ADMIN_PASSCODE = cfg.adminPasscode;
82 BATTLESHIP_EXTERNAL_URL = "https://${cfg.domain}";
83 BATTLESHIP_ENGINE_PATH = "/var/lib/battleship-arena/battleship-engine";
84 CPLUS_INCLUDE_PATH = "/var/lib/battleship-arena/battleship-engine/include";
85 };
86
87 path = [ pkgs.gcc pkgs.coreutils ];
88
89 serviceConfig = {
90 Type = "simple";
91 User = "battleship-arena";
92 Group = "battleship-arena";
93 WorkingDirectory = "/var/lib/battleship-arena";
94 ExecStart = "${cfg.package}/bin/battleship-arena";
95 Restart = "always";
96 RestartSec = "10s";
97
98 # Load secrets if provided
99 EnvironmentFile = mkIf (cfg.secretsFile != null) cfg.secretsFile;
100
101 # Security hardening
102 NoNewPrivileges = true;
103 PrivateTmp = true;
104 ProtectSystem = "strict";
105 ProtectHome = true;
106 ReadWritePaths = [ "/var/lib/battleship-arena" ];
107 };
108
109 preStart = ''
110 mkdir -p ${cfg.uploadDir}
111 mkdir -p $(dirname ${cfg.resultsDb})
112 chown -R battleship-arena:battleship-arena ${cfg.uploadDir}
113 chmod -R u+rwX ${cfg.uploadDir}
114
115 # Generate SSH host key if it doesn't exist
116 if [ ! -f /var/lib/battleship-arena/.ssh/battleship_arena ]; then
117 mkdir -p /var/lib/battleship-arena/.ssh
118 ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f /var/lib/battleship-arena/.ssh/battleship_arena -N ""
119 chown -R battleship-arena:battleship-arena /var/lib/battleship-arena/.ssh
120 fi
121
122 # Copy battleship-engine to writable directory
123 chmod -R u+w /var/lib/battleship-arena/battleship-engine 2>/dev/null || true
124 rm -rf /var/lib/battleship-arena/battleship-engine
125 cp -r ${cfg.package}/share/battleship-arena/battleship-engine /var/lib/battleship-arena/
126 chown -R battleship-arena:battleship-arena /var/lib/battleship-arena/battleship-engine
127 chmod -R u+rwX /var/lib/battleship-arena/battleship-engine
128 '';
129 };
130
131 # Service to recalculate Glicko-2 ratings (manual trigger only)
132 # Ratings automatically recalculate after each round-robin
133 # Use: sudo systemctl start battleship-arena-recalculate
134 systemd.services.battleship-arena-recalculate = {
135 description = "Recalculate Battleship Arena Glicko-2 Ratings";
136
137 environment = {
138 BATTLESHIP_RESULTS_DB = cfg.resultsDb;
139 };
140
141 serviceConfig = {
142 Type = "oneshot";
143 User = "battleship-arena";
144 Group = "battleship-arena";
145 WorkingDirectory = "/var/lib/battleship-arena";
146 ExecStart = "${cfg.package}/bin/battleship-arena recalculate-ratings";
147 };
148 };
149
150 # Allow battleship-arena user to create transient systemd units for sandboxing
151 security.polkit.extraConfig = ''
152 polkit.addRule(function(action, subject) {
153 if (action.id == "org.freedesktop.systemd1.manage-units" &&
154 subject.user == "battleship-arena") {
155 return polkit.Result.YES;
156 }
157 });
158 '';
159
160 networking.firewall.allowedTCPPorts = [ cfg.sshPort ];
161 };
162}