1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 inherit (lib)
10 mkEnableOption
11 mkPackageOption
12 mkOption
13 types
14 literalExpression
15 mkIf
16 mkDefault
17 ;
18 cfg = config.services.miniflux;
19
20 boolToInt = b: if b then 1 else 0;
21
22 pgbin = "${config.services.postgresql.package}/bin";
23 preStart = pkgs.writeScript "miniflux-pre-start" ''
24 #!${pkgs.runtimeShell}
25 ${pgbin}/psql "miniflux" -c "CREATE EXTENSION IF NOT EXISTS hstore"
26 '';
27in
28
29{
30 options = {
31 services.miniflux = {
32 enable = mkEnableOption "miniflux";
33
34 package = mkPackageOption pkgs "miniflux" { };
35
36 createDatabaseLocally = mkOption {
37 type = types.bool;
38 default = true;
39 description = ''
40 Whether a PostgreSQL database should be automatically created and
41 configured on the local host. If set to `false`, you need provision a
42 database yourself and make sure to create the hstore extension in it.
43 '';
44 };
45
46 config = mkOption {
47 type = types.submodule {
48 freeformType =
49 with types;
50 attrsOf (oneOf [
51 str
52 int
53 ]);
54 options = {
55 LISTEN_ADDR = mkOption {
56 type = types.str;
57 default = "localhost:8080";
58 description = ''
59 Address to listen on. Use absolute path for a Unix socket.
60 Multiple addresses can be specified, separated by commas.
61 '';
62 example = "127.0.0.1:8080, 127.0.0.1:8081";
63 };
64 DATABASE_URL = mkOption {
65 type = types.str;
66 defaultText = "user=miniflux host=/run/postgresql dbname=miniflux";
67 description = ''
68 Postgresql connection parameters.
69 See [lib/pq](https://pkg.go.dev/github.com/lib/pq#hdr-Connection_String_Parameters) for more details.
70 '';
71 };
72 RUN_MIGRATIONS = mkOption {
73 type = with types; coercedTo bool boolToInt int;
74 default = true;
75 description = "Run database migrations.";
76 };
77 CREATE_ADMIN = mkOption {
78 type = with types; coercedTo bool boolToInt int;
79 default = true;
80 description = "Create an admin user from environment variables.";
81 };
82 WATCHDOG = mkOption {
83 type = with types; coercedTo bool boolToInt int;
84 default = true;
85 description = "Enable or disable Systemd watchdog.";
86 };
87 };
88 };
89 default = { };
90 description = ''
91 Configuration for Miniflux, refer to
92 <https://miniflux.app/docs/configuration.html>
93 for documentation on the supported values.
94 '';
95 };
96
97 adminCredentialsFile = mkOption {
98 type = types.nullOr types.path;
99 default = null;
100 description = ''
101 File containing the ADMIN_USERNAME and
102 ADMIN_PASSWORD (length >= 6) in the format of
103 an EnvironmentFile=, as described by {manpage}`systemd.exec(5)`.
104 '';
105 example = "/etc/nixos/miniflux-admin-credentials";
106 };
107 };
108 };
109
110 config = mkIf cfg.enable {
111 assertions = [
112 {
113 assertion = cfg.config.CREATE_ADMIN == 0 || cfg.adminCredentialsFile != null;
114 message = "services.miniflux.adminCredentialsFile must be set if services.miniflux.config.CREATE_ADMIN is 1";
115 }
116 ];
117 services.miniflux.config = {
118 DATABASE_URL = lib.mkIf cfg.createDatabaseLocally "user=miniflux host=/run/postgresql dbname=miniflux";
119 };
120
121 services.postgresql = lib.mkIf cfg.createDatabaseLocally {
122 enable = true;
123 ensureUsers = [
124 {
125 name = "miniflux";
126 ensureDBOwnership = true;
127 }
128 ];
129 ensureDatabases = [ "miniflux" ];
130 };
131
132 systemd.services.miniflux-dbsetup = lib.mkIf cfg.createDatabaseLocally {
133 description = "Miniflux database setup";
134 requires = [ "postgresql.target" ];
135 after = [
136 "network.target"
137 "postgresql.target"
138 ];
139 serviceConfig = {
140 Type = "oneshot";
141 User = config.services.postgresql.superUser;
142 ExecStart = preStart;
143 };
144 };
145
146 systemd.services.miniflux = {
147 description = "Miniflux service";
148 wantedBy = [ "multi-user.target" ];
149 requires = lib.optional cfg.createDatabaseLocally "miniflux-dbsetup.service";
150 after = [
151 "network.target"
152 ]
153 ++ lib.optionals cfg.createDatabaseLocally [
154 "postgresql.target"
155 "miniflux-dbsetup.service"
156 ];
157
158 serviceConfig = {
159 Type = "notify";
160 ExecStart = lib.getExe cfg.package;
161 User = "miniflux";
162 DynamicUser = true;
163 RuntimeDirectory = "miniflux";
164 RuntimeDirectoryMode = "0750";
165 EnvironmentFile = lib.mkIf (cfg.adminCredentialsFile != null) cfg.adminCredentialsFile;
166 WatchdogSec = 60;
167 WatchdogSignal = "SIGKILL";
168 Restart = "always";
169 RestartSec = 5;
170
171 # Hardening
172 CapabilityBoundingSet = [ "" ];
173 DeviceAllow = [ "" ];
174 LockPersonality = true;
175 MemoryDenyWriteExecute = true;
176 PrivateDevices = true;
177 PrivateUsers = true;
178 ProcSubset = "pid";
179 ProtectClock = true;
180 ProtectControlGroups = true;
181 ProtectHome = true;
182 ProtectHostname = true;
183 ProtectKernelLogs = true;
184 ProtectKernelModules = true;
185 ProtectKernelTunables = true;
186 ProtectProc = "invisible";
187 RestrictAddressFamilies = [
188 "AF_INET"
189 "AF_INET6"
190 "AF_UNIX"
191 ];
192 RestrictNamespaces = true;
193 RestrictRealtime = true;
194 RestrictSUIDSGID = true;
195 SystemCallArchitectures = "native";
196 SystemCallFilter = [
197 "@system-service"
198 "~@privileged"
199 ];
200 UMask = "0077";
201 };
202
203 environment = lib.mapAttrs (_: toString) cfg.config;
204 };
205 environment.systemPackages = [ cfg.package ];
206
207 security.apparmor.policies."bin.miniflux".profile = ''
208 include <tunables/global>
209 ${cfg.package}/bin/miniflux {
210 include <abstractions/base>
211 include <abstractions/nameservice>
212 include <abstractions/ssl_certs>
213 include "${pkgs.apparmorRulesFromClosure { name = "miniflux"; } cfg.package}"
214 r ${cfg.package}/bin/miniflux,
215 r @{sys}/kernel/mm/transparent_hugepage/hpage_pmd_size,
216 rw /run/miniflux/**,
217 }
218 '';
219 };
220}