1{ config, pkgs, lib, ... }:
2let
3 inherit (lib) mkOption types mdDoc mkIf;
4 cfg = config.services.atuin;
5in
6{
7 options = {
8 services.atuin = {
9 enable = lib.mkEnableOption (mdDoc "Atuin server for shell history sync");
10
11 openRegistration = mkOption {
12 type = types.bool;
13 default = false;
14 description = mdDoc "Allow new user registrations with the atuin server.";
15 };
16
17 path = mkOption {
18 type = types.str;
19 default = "";
20 description = mdDoc "A path to prepend to all the routes of the server.";
21 };
22
23 host = mkOption {
24 type = types.str;
25 default = "127.0.0.1";
26 description = mdDoc "The host address the atuin server should listen on.";
27 };
28
29 maxHistoryLength = mkOption {
30 type = types.int;
31 default = 8192;
32 description = mdDoc "The max length of each history item the atuin server should store.";
33 };
34
35 port = mkOption {
36 type = types.port;
37 default = 8888;
38 description = mdDoc "The port the atuin server should listen on.";
39 };
40
41 openFirewall = mkOption {
42 type = types.bool;
43 default = false;
44 description = mdDoc "Open ports in the firewall for the atuin server.";
45 };
46
47 database = {
48 createLocally = mkOption {
49 type = types.bool;
50 default = true;
51 description = mdDoc "Create the database and database user locally.";
52 };
53
54 uri = mkOption {
55 type = types.str;
56 default = "postgresql:///atuin?host=/run/postgresql";
57 example = "postgresql://atuin@localhost:5432/atuin";
58 description = mdDoc "URI to the database";
59 };
60 };
61 };
62 };
63
64 config = mkIf cfg.enable {
65 assertions = [
66 {
67 assertion = cfg.database.createLocally -> config.services.postgresql.enable;
68 message = "Postgresql must be enabled to create a local database";
69 }
70 ];
71
72 services.postgresql = mkIf cfg.database.createLocally {
73 enable = true;
74 ensureUsers = [{
75 name = "atuin";
76 ensureDBOwnership = true;
77 }];
78 ensureDatabases = [ "atuin" ];
79 };
80
81 systemd.services.atuin = {
82 description = "atuin server";
83 requires = lib.optionals cfg.database.createLocally [ "postgresql.service" ];
84 after = [ "network.target" ] ++ lib.optionals cfg.database.createLocally [ "postgresql.service" ];
85 wantedBy = [ "multi-user.target" ];
86
87 serviceConfig = {
88 ExecStart = "${pkgs.atuin}/bin/atuin server start";
89 RuntimeDirectory = "atuin";
90 RuntimeDirectoryMode = "0700";
91 DynamicUser = true;
92
93 # Hardening
94 CapabilityBoundingSet = "";
95 LockPersonality = true;
96 NoNewPrivileges = true;
97 MemoryDenyWriteExecute = true;
98 PrivateDevices = true;
99 PrivateMounts = true;
100 PrivateTmp = true;
101 PrivateUsers = true;
102 ProcSubset = "pid";
103 ProtectClock = true;
104 ProtectControlGroups = true;
105 ProtectHome = true;
106 ProtectHostname = true;
107 ProtectKernelLogs = true;
108 ProtectKernelModules = true;
109 ProtectKernelTunables = true;
110 ProtectProc = "invisible";
111 ProtectSystem = "full";
112 RemoveIPC = true;
113 RestrictAddressFamilies = [
114 "AF_INET"
115 "AF_INET6"
116 # Required for connecting to database sockets,
117 "AF_UNIX"
118 ];
119 RestrictNamespaces = true;
120 RestrictRealtime = true;
121 RestrictSUIDSGID = true;
122 SystemCallArchitectures = "native";
123 SystemCallFilter = [
124 "@system-service"
125 "~@privileged"
126 ];
127 UMask = "0077";
128 };
129
130 environment = {
131 ATUIN_HOST = cfg.host;
132 ATUIN_PORT = toString cfg.port;
133 ATUIN_MAX_HISTORY_LENGTH = toString cfg.maxHistoryLength;
134 ATUIN_OPEN_REGISTRATION = lib.boolToString cfg.openRegistration;
135 ATUIN_DB_URI = cfg.database.uri;
136 ATUIN_PATH = cfg.path;
137 ATUIN_CONFIG_DIR = "/run/atuin"; # required to start, but not used as configuration is via environment variables
138 };
139 };
140
141 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
142 };
143}