nixos/warpgate: init

This commit is contained in:
Lemon Lam
2025-08-05 12:53:43 +08:00
parent 16c01fbc4d
commit 6753792aff
5 changed files with 497 additions and 0 deletions

View File

@@ -80,6 +80,8 @@
- [Corteza](https://cortezaproject.org/), a low-code platform. Available as [services.corteza](#opt-services.corteza.enable). - [Corteza](https://cortezaproject.org/), a low-code platform. Available as [services.corteza](#opt-services.corteza.enable).
- [Warpgate](https://warpgate.null.page), a SSH, HTTPS, MySQL and Postgres bastion. Available as [services.warpgate](#opt-services.warpgate.enable). Note that you need to run `warpgate recover-access` to recover builtin admin account, as the initialisation script uses a throwaway value to initialise its database.
- [TuneD](https://tuned-project.org/), a system tuning service for Linux. Available as [services.tuned](#opt-services.tuned.enable). - [TuneD](https://tuned-project.org/), a system tuning service for Linux. Available as [services.tuned](#opt-services.tuned.enable).
- [yubikey-manager](https://github.com/Yubico/yubikey-manager), a tool for configuring YubiKey devices. Available as [programs.yubikey-manager](#opt-programs.yubikey-manager.enable). - [yubikey-manager](https://github.com/Yubico/yubikey-manager), a tool for configuring YubiKey devices. Available as [programs.yubikey-manager](#opt-programs.yubikey-manager.enable).

View File

@@ -1497,6 +1497,7 @@
./services/security/vault-agent.nix ./services/security/vault-agent.nix
./services/security/vault.nix ./services/security/vault.nix
./services/security/vaultwarden/default.nix ./services/security/vaultwarden/default.nix
./services/security/warpgate.nix
./services/security/yubikey-agent.nix ./services/security/yubikey-agent.nix
./services/system/automatic-timezoned.nix ./services/system/automatic-timezoned.nix
./services/system/bpftune.nix ./services/system/bpftune.nix

View File

@@ -0,0 +1,444 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.warpgate;
yaml = pkgs.formats.yaml { };
in
{
options.services.warpgate =
let
inherit (lib.types)
nullOr
enum
str
bool
port
listOf
attrsOf
submodule
;
inherit (lib.options) mkOption mkPackageOption literalExpression;
in
{
enable = mkOption {
description = ''
Whether to enable Warpgate.
This module will initialize Warpgate base on your config automatically. Please run `warpgate recover-access` to gain access.
'';
type = bool;
default = false;
};
package = mkPackageOption pkgs "warpgate" { };
databaseUrlFile = mkOption {
description = ''
Path to file containing database connection string with credentials.
Should be a one line file with: `database_url: <protocol>://<username>:<password>@<host>/<database>`.
See [SeaORM documentation](https://www.sea-ql.org/SeaORM/docs/install-and-config/connection/).
'';
type = nullOr str;
default = null;
};
settings = mkOption {
description = "Warpgate configuration.";
type = submodule {
freeformType = yaml.type;
options = {
sso_providers = mkOption {
description = "Configure OIDC single sign-on providers.";
default = [ ];
type = listOf (submodule {
freeformType = yaml.type;
options = {
name = mkOption {
description = "Internal identifier of SSO provider.";
type = str;
};
label = mkOption {
description = "SSO provider name displayed on login page.";
type = str;
};
provider = mkOption {
description = "SSO provider configurations.";
type = attrsOf yaml.type;
};
};
});
example = literalExpression ''
[
{
name = "3rd party SSO";
label = "ACME SSO";
provider = {
type = "custom";
client_id = "123...";
client_secret = "BC...";
issuer_url = "https://sso.acme.inc";
scopes = ["email"];
};
}
{
...
}
]
'';
};
recordings = {
enable = mkOption {
description = "Whether to enable session recording.";
default = true;
type = bool;
};
path = mkOption {
description = "Path to store session recordings.";
default = "/var/lib/warpgate/recordings";
type = str;
};
};
external_host = mkOption {
description = ''
Configure the domain name of this Warpgate instance.
See [HTTP domain binding](https://warpgate.null.page/http-domain-binding/).
'';
default = null;
type = nullOr str;
};
database_url = mkOption {
description = ''
Database connection string.
See [SeaORM documentation](https://www.sea-ql.org/SeaORM/docs/install-and-config/connection/).
'';
default = "sqlite:/var/lib/warpgate/db";
type = nullOr str;
};
ssh = {
enable = mkOption {
description = "Whether to enable SSH listener.";
default = false;
type = bool;
};
listen = mkOption {
description = "Listen endpoint of SSH listener.";
default = "[::]:2222";
type = str;
};
external_port = mkOption {
description = "The SSH listener is reachable via this port externally.";
default = null;
type = nullOr port;
};
keys = mkOption {
description = "Path to store SSH host & client keys.";
default = "/var/lib/warpgate/ssh-keys";
type = str;
};
host_key_verification = mkOption {
description = "Specify host key verification action when connecting to a SSH target with unknown/differing host key.";
default = "prompt";
type = enum [
"prompt"
"auto_accept"
"auto_reject"
];
};
inactivity_timeout = mkOption {
description = "How long can user be inactive until Warpgate terminates the connection.";
default = "5m";
type = str;
};
keepalive_interval = mkOption {
description = "If nothing is received from the client for this amount of time, server will send a keepalive message.";
default = null;
type = nullOr str;
};
};
http = {
listen = mkOption {
description = "Listen endpoint of HTTP listener.";
default = "[::]:8888";
type = str;
};
external_port = mkOption {
description = "The HTTP listener is reachable via this port externally.";
default = null;
type = nullOr port;
};
certificate = mkOption {
description = "Path to HTTPS listener certificate.";
default = "/var/lib/warpgate/tls.certificate.pem";
type = str;
};
key = mkOption {
description = "Path to HTTPS listener private key.";
default = "/var/lib/warpgate/tls.key.pem";
type = str;
};
sni_certificates = mkOption {
description = "Certificates for additional domains.";
default = [ ];
type = listOf (submodule {
freeformType = yaml.type;
options = {
certificate = mkOption {
description = "Path to certificate.";
default = "";
type = str;
};
key = mkOption {
description = "Path to private key.";
default = "";
type = str;
};
};
});
example = literalExpression ''
[
{
certificate = "/var/lib/warpgate/example.tld.pem";
key = "/var/lib/warpgate/example.tld.key.pem";
}
{
...
}
]
'';
};
trust_x_forwarded_headers = mkOption {
description = ''
Trust X-Forwarded-* headers. Required when being reverse proxied.
See [Running behind a reverse proxy](https://warpgate.null.page/reverse-proxy/).
'';
default = false;
type = bool;
};
session_max_age = mkOption {
description = "How long until a logged in session expires.";
default = "30m";
type = str;
};
cookie_max_age = mkOption {
description = "How long until logged in cookie expires.";
default = "1day";
type = str;
};
};
mysql = {
enable = mkOption {
description = "Whether to enable MySQL listener.";
default = false;
type = bool;
};
listen = mkOption {
description = "Listen endpoint of MySQL listener.";
default = "[::]:33306";
type = str;
};
external_port = mkOption {
description = "The MySQL listener is reachable via this port externally.";
default = null;
type = nullOr port;
};
certificate = mkOption {
description = "Path to MySQL listener certificate.";
default = "/var/lib/warpgate/tls.certificate.pem";
type = str;
};
key = mkOption {
description = "Path to MySQL listener private key.";
default = "/var/lib/warpgate/tls.key.pem";
type = str;
};
};
postgres = {
enable = mkOption {
description = "Whether to enable PostgreSQL listener.";
default = false;
type = bool;
};
listen = mkOption {
description = "Listen endpoint of PostgreSQL listener.";
default = "[::]:55432";
type = str;
};
external_port = mkOption {
description = "The PostgreSQL listener is reachable via this port externally.";
default = null;
type = nullOr str;
};
certificate = mkOption {
description = "Path to PostgreSQL listener certificate.";
default = "/var/lib/warpgate/tls.certificate.pem";
type = str;
};
key = mkOption {
description = "Path to PostgreSQL listener private key.";
default = "/var/lib/warpgate/tls.key.pem";
type = str;
};
};
log = {
retention = mkOption {
description = "How long Warpgate keep its logs.";
default = "7days";
type = str;
};
send_to = mkOption {
description = ''
Path of UNIX socket of log forwarder.
See [Log forwarding](https://warpgate.null.page/log-forwarding/);
'';
default = null;
type = nullOr str;
};
};
config_provider = mkOption {
description = ''
Source of truth of users.
DO NOT change this, Warpgate only implemented database provider.
'';
default = "database";
type = enum [
"file"
"database"
];
};
};
};
default = { };
example = {
ssh = {
enable = true;
listen = "[::]:2211";
};
http = {
listen = "[::]:8011";
};
};
};
};
config =
let
inherit (lib.lists)
any
map
head
reverseList
;
inherit (lib.strings) splitString toIntBase10;
preStartScript = pkgs.writers.writeBash "warpgate-init" ''
CFGFILE=/var/lib/warpgate/config.yaml
if [ ! -O $CFGFILE ] || [ ! -s $CFGFILE ]; then
INITPWD=$(tr -dc 'A-Za-z0-9!?%=' </dev/urandom 2>/dev/null | head -c 16)
${lib.getExe cfg.package} \
--config $CFGFILE unattended-setup \
--data-path /var/lib/warpgate \
--http-port 8888 \
--admin-password $INITPWD
fi
${
if cfg.databaseUrlFile != null then
''
sed -e '/^database_url: null/d' ${yaml.generate "warpgate-config" cfg.settings} > $CFGFILE
cat /run/credentials/warpgate.service/databaseUrl >> $CFGFILE
''
else
"cp --no-preserve=ownership ${yaml.generate "warpgate-config" cfg.settings} $CFGFILE"
}
'';
bindOnPrivilegedPorts = any (x: toIntBase10 x < 1025) (
map (x: head (reverseList (splitString ":" x))) (
[ cfg.settings.http.listen ]
++ lib.optional cfg.settings.ssh.enable cfg.settings.ssh.listen
++ lib.optional cfg.settings.mysql.enable cfg.settings.mysql.listen
++ lib.optional cfg.settings.postgres.enable cfg.settings.postgres.listen
)
);
in
lib.mkIf cfg.enable {
assertions = [
{
assertion = !((cfg.databaseUrlFile != null) && (cfg.settings.database_url != null));
message = "You cannot configure databaseUrlFile and settings.database_url at the same time.";
}
{
assertion = !((cfg.databaseUrlFile == null) && (cfg.settings.database_url == null));
message = "Either databaseUrlFile or settings.database_url must be set; Set the other to null.";
}
];
environment.systemPackages = [ cfg.package ];
systemd.services.warpgate = {
description = "Warpgate smart bastion";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
startLimitBurst = 5;
serviceConfig = {
LoadCredential = "${
if cfg.databaseUrlFile != null then "databaseUrl:${cfg.databaseUrlFile}" else ""
}";
ExecStartPre = preStartScript;
ExecStart = "${lib.getExe cfg.package} --config /var/lib/warpgate/config.yaml run";
DynamicUser = true;
RestartSec = 3;
Restart = "on-failure";
StateDirectory = "warpgate";
StateDirectoryMode = "0700";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateTmp = true;
ProtectHome = true;
PrivateDevices = true;
DeviceAllow = [
"/dev/null rw"
"/dev/urandom r"
];
DevicePolicy = "strict";
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
RestrictNamespaces = true;
ProtectProc = "invisible";
ProtectSystem = "full";
ProtectClock = true;
ProtectControlGroups = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
}
// (
if bindOnPrivilegedPorts then
{
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
}
else
{
PrivateUsers = true;
}
);
};
};
meta.maintainers = with lib.maintainers; [ alemonmk ];
}

View File

@@ -1609,6 +1609,7 @@ in
vsftpd = runTest ./vsftpd.nix; vsftpd = runTest ./vsftpd.nix;
waagent = runTest ./waagent.nix; waagent = runTest ./waagent.nix;
wakapi = runTest ./wakapi.nix; wakapi = runTest ./wakapi.nix;
warpgate = runTest ./warpgate.nix;
warzone2100 = runTest ./warzone2100.nix; warzone2100 = runTest ./warzone2100.nix;
wasabibackend = runTest ./wasabibackend.nix; wasabibackend = runTest ./wasabibackend.nix;
wastebin = runTest ./wastebin.nix; wastebin = runTest ./wastebin.nix;

49
nixos/tests/warpgate.nix Normal file
View File

@@ -0,0 +1,49 @@
{
name = "warpgate";
nodes = {
machine = {
services.warpgate = {
enable = true;
};
};
machine2 = {
environment.etc."warpgate-db-url".text = "database: sqlite:/var/lib/warpgate/db/";
services.warpgate = {
enable = true;
databaseUrlFile = "/etc/warpgate-db-url";
settings = {
database_url = null;
};
};
};
machine3 = {
services.warpgate = {
enable = true;
settings = {
http.listen = "[::]:443";
};
};
};
};
testScript = ''
machine.wait_for_unit("warpgate.service")
machine.wait_for_open_port(8888)
machine.succeed("stat /var/lib/warpgate/db/db.sqlite3")
machine.succeed("curl -k --fail https://localhost:8888/@warpgate")
machine.shutdown()
machine2.wait_for_unit("warpgate.service")
machine2.wait_for_open_port(8888)
machine2.succeed("curl -k --fail https://localhost:8888/@warpgate")
machine2.shutdown()
machine3.wait_for_unit("warpgate.service")
machine3.wait_for_open_port(443)
machine3.succeed("curl -k --fail https://localhost/@warpgate")
machine3.shutdown()
'';
}