-
-
Notifications
You must be signed in to change notification settings - Fork 14.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
nixos/manual: add archtika module to 25.05 release notes nixos/archtika: fix module formatting, add description and remove trailing whitespace nixos/archtika: refactor module
- Loading branch information
Showing
3 changed files
with
312 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,309 @@ | ||
{ | ||
config, | ||
lib, | ||
pkgs, | ||
... | ||
}: | ||
|
||
let | ||
inherit (lib) | ||
mkEnableOption | ||
mkOption | ||
mkIf | ||
mkPackageOption | ||
types | ||
; | ||
cfg = config.services.archtika; | ||
baseHardenedSystemdOptions = { | ||
CapabilityBoundingSet = ""; | ||
LockPersonality = true; | ||
NoNewPrivileges = true; | ||
PrivateDevices = true; | ||
PrivateTmp = true; | ||
ProtectClock = true; | ||
ProtectControlGroups = true; | ||
ProtectHome = true; | ||
ProtectHostname = true; | ||
ProtectKernelLogs = true; | ||
ProtectKernelModules = true; | ||
ProtectKernelTunables = true; | ||
ProtectSystem = "strict"; | ||
RemoveIPC = true; | ||
RestrictNamespaces = true; | ||
RestrictRealtime = true; | ||
RestrictSUIDSGID = true; | ||
SystemCallArchitectures = "native"; | ||
SystemCallFilter = [ | ||
"@system-service" | ||
"~@privileged" | ||
"~@resources" | ||
]; | ||
|
||
ReadWritePaths = [ "/var/www/archtika-websites" ]; | ||
}; | ||
in | ||
{ | ||
options.services.archtika = { | ||
enable = mkEnableOption "Whether to enable the archtika service"; | ||
|
||
package = mkPackageOption pkgs "archtika" { }; | ||
|
||
user = mkOption { | ||
type = types.str; | ||
default = "archtika"; | ||
description = "User account under which archtika runs."; | ||
}; | ||
|
||
group = mkOption { | ||
type = types.str; | ||
default = "archtika"; | ||
description = "Group under which archtika runs."; | ||
}; | ||
|
||
databaseName = mkOption { | ||
type = types.str; | ||
default = "archtika"; | ||
description = "Name of the PostgreSQL database for archtika."; | ||
}; | ||
|
||
apiPort = mkOption { | ||
type = types.port; | ||
default = 5000; | ||
description = "Port on which the API runs."; | ||
}; | ||
|
||
apiAdminPort = mkOption { | ||
type = types.port; | ||
default = 7500; | ||
description = "Port on which the API admin server runs."; | ||
}; | ||
|
||
webAppPort = mkOption { | ||
type = types.port; | ||
default = 10000; | ||
description = "Port on which the web application runs."; | ||
}; | ||
|
||
domain = mkOption { | ||
type = types.str; | ||
description = "Domain to use for the application."; | ||
}; | ||
|
||
acmeEmail = mkOption { | ||
type = types.str; | ||
description = "Email to notify for the SSL certificate renewal process."; | ||
}; | ||
|
||
dnsProvider = mkOption { | ||
type = types.str; | ||
description = "DNS provider for the DNS-01 challenge (required for wildcard domains)."; | ||
}; | ||
|
||
dnsEnvironmentFile = mkOption { | ||
type = types.path; | ||
description = "API secrets for the DNS-01 challenge (required for wildcard domains)."; | ||
}; | ||
|
||
settings = mkOption { | ||
description = "Settings for the running archtika application."; | ||
type = types.submodule { | ||
options = { | ||
disableRegistration = mkOption { | ||
type = types.bool; | ||
default = false; | ||
description = "By default any user can create an account. That behavior can be disabled by using this option."; | ||
}; | ||
maxUserWebsites = mkOption { | ||
type = types.int; | ||
default = 2; | ||
description = "Maximum number of websites allowed per user by default."; | ||
}; | ||
maxWebsiteStorageSize = mkOption { | ||
type = types.int; | ||
default = 50; | ||
description = "Maximum amount of disk space in MB allowed per user website by default."; | ||
}; | ||
}; | ||
}; | ||
}; | ||
}; | ||
|
||
config = mkIf cfg.enable { | ||
users.users.${cfg.user} = { | ||
isSystemUser = true; | ||
group = cfg.group; | ||
}; | ||
|
||
users.groups.${cfg.group} = { | ||
members = [ | ||
"nginx" | ||
"postgres" | ||
]; | ||
}; | ||
|
||
systemd.tmpfiles.rules = [ | ||
"d /var/www 0755 root root -" | ||
"d /var/www/archtika-websites 0770 ${cfg.user} ${cfg.group} -" | ||
]; | ||
|
||
systemd.services.archtika-api = { | ||
description = "archtika API service"; | ||
wantedBy = [ "multi-user.target" ]; | ||
after = [ | ||
"network.target" | ||
"postgresql.service" | ||
]; | ||
|
||
serviceConfig = baseHardenedSystemdOptions // { | ||
User = cfg.user; | ||
Group = cfg.group; | ||
Restart = "always"; | ||
WorkingDirectory = "${cfg.package}/rest-api"; | ||
|
||
RestrictAddressFamilies = [ | ||
"AF_INET" | ||
"AF_INET6" | ||
"AF_UNIX" | ||
]; | ||
}; | ||
|
||
script = '' | ||
JWT_SECRET=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c64) | ||
${pkgs.postgresql_16}/bin/psql postgres://postgres@localhost:5432/${cfg.databaseName} -c "ALTER DATABASE ${cfg.databaseName} SET \"app.jwt_secret\" TO '$JWT_SECRET'" | ||
${pkgs.postgresql_16}/bin/psql postgres://postgres@localhost:5432/${cfg.databaseName} -c "ALTER DATABASE ${cfg.databaseName} SET \"app.website_max_storage_size\" TO ${toString cfg.settings.maxWebsiteStorageSize}" | ||
${pkgs.postgresql_16}/bin/psql postgres://postgres@localhost:5432/${cfg.databaseName} -c "ALTER DATABASE ${cfg.databaseName} SET \"app.website_max_number_user\" TO ${toString cfg.settings.maxUserWebsites}" | ||
${pkgs.dbmate}/bin/dbmate --url postgres://postgres@localhost:5432/archtika?sslmode=disable --migrations-dir ${cfg.package}/rest-api/db/migrations up | ||
PGRST_SERVER_CORS_ALLOWED_ORIGINS="https://${cfg.domain}" PGRST_ADMIN_SERVER_PORT=${toString cfg.apiAdminPort} PGRST_SERVER_PORT=${toString cfg.apiPort} PGRST_DB_SCHEMAS="api" PGRST_DB_ANON_ROLE="anon" PGRST_OPENAPI_MODE="ignore-privileges" PGRST_DB_URI="postgres://authenticator@localhost:5432/${cfg.databaseName}" PGRST_JWT_SECRET="$JWT_SECRET" ${pkgs.postgrest}/bin/postgrest | ||
''; | ||
}; | ||
|
||
systemd.services.archtika-web = { | ||
description = "archtika Web App service"; | ||
wantedBy = [ "multi-user.target" ]; | ||
after = [ "network.target" ]; | ||
|
||
serviceConfig = baseHardenedSystemdOptions // { | ||
User = cfg.user; | ||
Group = cfg.group; | ||
Restart = "always"; | ||
WorkingDirectory = "${cfg.package}/web-app"; | ||
|
||
RestrictAddressFamilies = [ | ||
"AF_INET" | ||
"AF_INET6" | ||
]; | ||
}; | ||
|
||
script = '' | ||
REGISTRATION_IS_DISABLED=${toString cfg.settings.disableRegistration} BODY_SIZE_LIMIT=10M ORIGIN=https://${cfg.domain} PORT=${toString cfg.webAppPort} ${pkgs.nodejs_22}/bin/node ${cfg.package}/web-app | ||
''; | ||
}; | ||
|
||
services.postgresql = { | ||
enable = true; | ||
package = pkgs.postgresql_16; | ||
ensureDatabases = [ cfg.databaseName ]; | ||
authentication = lib.mkOverride 51 '' | ||
# IPv4 local connections: | ||
host all all 127.0.0.1/32 trust | ||
# IPv6 local connections: | ||
host all all ::1/128 trust | ||
# Local socket connections: | ||
local all all trust | ||
''; | ||
extraPlugins = [ pkgs.postgresql16Packages.pgjwt ]; | ||
}; | ||
|
||
systemd.services.postgresql.path = builtins.attrValues { | ||
inherit (pkgs) gnutar gzip; | ||
}; | ||
|
||
services.nginx = { | ||
enable = true; | ||
recommendedProxySettings = true; | ||
recommendedTlsSettings = true; | ||
recommendedZstdSettings = true; | ||
recommendedOptimisation = true; | ||
|
||
appendHttpConfig = '' | ||
limit_req_zone $binary_remote_addr zone=requestLimit:10m rate=5r/s; | ||
limit_req_status 429; | ||
limit_req zone=requestLimit burst=20 nodelay; | ||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; | ||
add_header X-Frame-Options "SAMEORIGIN" always; | ||
add_header X-Content-Type-Options "nosniff" always; | ||
add_header Referrer-Policy "strict-origin-when-cross-origin" always; | ||
add_header Permissions-Policy "accelerometer=(),autoplay=(),camera=(),cross-origin-isolated=(),display-capture=(),encrypted-media=(),fullscreen=(self),geolocation=(),gyroscope=(),keyboard-map=(),magnetometer=(),microphone=(),midi=(),payment=(),picture-in-picture=(self),publickey-credentials-get=(),screen-wake-lock=(),sync-xhr=(self),usb=(),xr-spatial-tracking=(),clipboard-read=(self),clipboard-write=(self),gamepad=(),hid=(),idle-detection=(),interest-cohort=(),serial=(),unload=()" always; | ||
map $http_cookie $auth_header { | ||
default ""; | ||
"~*session_token=([^;]+)" "Bearer $1"; | ||
} | ||
''; | ||
|
||
virtualHosts = { | ||
"${cfg.domain}" = { | ||
useACMEHost = cfg.domain; | ||
forceSSL = true; | ||
locations = { | ||
"/" = { | ||
proxyPass = "http://localhost:${toString cfg.webAppPort}"; | ||
}; | ||
"/previews/" = { | ||
alias = "/var/www/archtika-websites/previews/"; | ||
index = "index.html"; | ||
tryFiles = "$uri $uri/ $uri.html =404"; | ||
}; | ||
"/api/rpc/export_articles_zip" = { | ||
proxyPass = "http://localhost:${toString cfg.apiPort}/rpc/export_articles_zip"; | ||
extraConfig = '' | ||
default_type application/json; | ||
proxy_set_header Authorization $auth_header; | ||
''; | ||
}; | ||
"/api/" = { | ||
proxyPass = "http://localhost:${toString cfg.apiPort}/"; | ||
extraConfig = '' | ||
default_type application/json; | ||
''; | ||
}; | ||
"/api/rpc/register" = mkIf cfg.settings.disableRegistration { | ||
extraConfig = '' | ||
deny all; | ||
''; | ||
}; | ||
}; | ||
}; | ||
"~^(?<subdomain>.+)\\.${cfg.domain}$" = { | ||
useACMEHost = cfg.domain; | ||
forceSSL = true; | ||
locations = { | ||
"/" = { | ||
root = "/var/www/archtika-websites/$subdomain"; | ||
index = "index.html"; | ||
tryFiles = "$uri $uri/ $uri.html =404"; | ||
}; | ||
}; | ||
}; | ||
}; | ||
}; | ||
|
||
security.acme = { | ||
acceptTerms = true; | ||
defaults.email = cfg.acmeEmail; | ||
certs."${cfg.domain}" = { | ||
domain = cfg.domain; | ||
extraDomainNames = [ "*.${cfg.domain}" ]; | ||
dnsProvider = cfg.dnsProvider; | ||
environmentFile = cfg.dnsEnvironmentFile; | ||
group = config.services.nginx.group; | ||
}; | ||
}; | ||
}; | ||
|
||
meta.maintainers = [ lib.maintainers.thiloho ]; | ||
} |