Compare commits

..

7 Commits

17 changed files with 760 additions and 451 deletions

View File

@@ -3,10 +3,6 @@
systemd.network.enable = true;
networking.domain = "pvv.ntnu.no";
networking.useDHCP = false;
# networking.search = [ "pvv.ntnu.no" "pvv.org" ];
# networking.nameservers = lib.mkDefault [ "129.241.0.200" "129.241.0.201" ];
# networking.tempAddresses = lib.mkDefault "disabled";
# networking.defaultGateway = values.hosts.gateway;
# The rest of the networking configuration is usually sourced from /values.nix

54
flake.lock generated
View File

@@ -1,5 +1,27 @@
{
"nodes": {
"bro": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1779629827,
"narHash": "sha256-nrlB50/oelB8oFx9DhOoXI5z0VoTZGEA6XxYvkvpqDA=",
"ref": "main",
"rev": "7d0f35e12e4dec39f981c08fc33515589f41f4a5",
"revCount": 3,
"type": "git",
"url": "https://git.pvv.ntnu.no/Projects/bro.git"
},
"original": {
"ref": "main",
"type": "git",
"url": "https://git.pvv.ntnu.no/Projects/bro.git"
}
},
"crane": {
"locked": {
"lastModified": 1776635034,
@@ -101,7 +123,7 @@
"nixpkgs": [
"nixpkgs-unstable"
],
"rust-overlay": "rust-overlay"
"rust-overlay": "rust-overlay_2"
},
"locked": {
"lastModified": 1777019032,
@@ -165,7 +187,7 @@
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay_2"
"rust-overlay": "rust-overlay_3"
},
"locked": {
"lastModified": 1767906976,
@@ -352,6 +374,7 @@
},
"root": {
"inputs": {
"bro": "bro",
"dibbler": "dibbler",
"disko": "disko",
"gergle": "gergle",
@@ -377,7 +400,7 @@
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay_3"
"rust-overlay": "rust-overlay_4"
},
"locked": {
"lastModified": 1778600367,
@@ -396,6 +419,27 @@
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"bro",
"nixpkgs"
]
},
"locked": {
"lastModified": 1779419951,
"narHash": "sha256-dMX0PUslUHPajP6o8FEoRdFv9afq/dec4POR0vVfjK4=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "5b5c521d6cae9ef4aa32f888eb2c0ce595c9be52",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"rust-overlay_2": {
"inputs": {
"nixpkgs": [
"greg-ng",
@@ -416,7 +460,7 @@
"type": "github"
}
},
"rust-overlay_2": {
"rust-overlay_3": {
"inputs": {
"nixpkgs": [
"minecraft-heatmap",
@@ -437,7 +481,7 @@
"type": "github"
}
},
"rust-overlay_3": {
"rust-overlay_4": {
"inputs": {
"nixpkgs": [
"roowho2",

View File

@@ -47,6 +47,9 @@
qotd.url = "git+https://git.pvv.ntnu.no/Projects/qotd.git?ref=main";
qotd.inputs.nixpkgs.follows = "nixpkgs";
bro.url = "git+https://git.pvv.ntnu.no/Projects/bro.git?ref=main";
bro.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = {
@@ -212,10 +215,16 @@
];
overlays = [inputs.dibbler.overlays.default];
};
dagali = stableNixosConfig "dagali" { };
shark = stableNixosConfig "shark" {};
wenche = stableNixosConfig "wenche" {};
temmie = stableNixosConfig "temmie" {};
temmie = stableNixosConfig "temmie" {
overlays = [
inputs.bro.overlays.default
];
modules = [
inputs.bro.nixosModules.default
];
};
gluttony = stableNixosConfig "gluttony" {
overlays = [
(final: prev: { bluemap = final.callPackage ./packages/bluemap.nix {}; })

View File

@@ -1,78 +0,0 @@
# Tracking document for new PVV kerberos auth stack
![Bensinstasjon på heimdal](https://bydelsnytt.no/wp-content/uploads/2022/08/esso_heimdal003.jpg)
<div align="center">
Bensinstasjon på heimdal
</div>
### TODO:
- [ ] setup heimdal
- [x] ensure running with systemd
- [x] compile smbk5pwd (part of openldap)
- [ ] set `modify -a -disallow-all-tix,requires-pre-auth default` declaratively
- [ ] fully initialize PVV.NTNU.NO
- [x] `kadmin -l init PVV.NTNU.NO`
- [x] add oysteikt/admin@PVV.NTNU.NO principal
- [x] add oysteikt@PVV.NTNU.NO principal
- [x] add krbtgt/PVV.NTNU.NO@PVV.NTNU.NO principal?
- why is this needed, and where is it documented?
- `kadmin check` seems to work under sudo?
- (it is included by default, just included as error message
in a weird state)
- [x] Ensure client is working correctly
- [x] Ensure kinit works on darbu
- [x] Ensure kpasswd works on darbu
- [x] Ensure kadmin get <user> (and other restricted commands) works on darbu
- [ ] Ensure kdc is working correctly
- [x] Ensure kinit works on dagali
- [x] Ensure kpasswd works on dagali
- [ ] Ensure kadmin get <user> (and other restricte commands) works on dagali
- [x] Fix FQDN
- https://github.com/NixOS/nixpkgs/issues/94011
- https://github.com/NixOS/nixpkgs/issues/261269
- Possibly fixed by disabling systemd-resolved
- [ ] setup cyrus sasl
- [x] ensure running with systemd
- [x] verify GSSAPI support plugin is installed
- `nix-shell -p cyrus_sasl --command pluginviewer`
- [x] create "host/localhost@PVV.NTNU.NO" and export to keytab
- [x] verify cyrus sasl is able to talk to heimdal
- `sudo testsaslauthd -u oysteikt -p <password>`
- [ ] provide ldap principal to cyrus sasl through keytab
- [ ] setup openldap
- [x] ensure running with systemd
- [ ] verify openldap is able to talk to cyrus sasl
- [ ] create user for oysteikt in openldap
- [ ] authenticate openldap login through sasl
- does this require creating an ldap user?
- [ ] fix smbk5pwd integration
- [x] add smbk5pwd schemas to openldap
- [x] create openldap db for smbk5pwd with overlays
- [ ] test to ensure that user sync is working
- [ ] test as user source (replace passwd)
- [ ] test as PAM auth source
- [ ] test as auth source for 3rd party appliation
- [ ] Set up ldap administration panel
- Doesn't seem like there are many good ones out there. Maybe phpLDAPAdmin?
- [ ] Set up kerberos SRV DNS entry
### Information and URLS
- OpenLDAP SASL: https://www.openldap.org/doc/admin24/sasl.html
- Use a keytab: https://kb.iu.edu/d/aumh
- 2 ways for openldap to auth: https://security.stackexchange.com/questions/65093/how-to-test-ldap-that-authenticates-with-kerberos
- Cyrus guide OpenLDAP + SASL + GSSAPI: https://www.cyrusimap.org/sasl/sasl/faqs/openldap-sasl-gssapi.html
- Configuring GSSAPI and Cyrus SASL: https://web.mit.edu/darwin/src/modules/passwordserver_sasl/cyrus_sasl/doc/gssapi.html
- PVV Kerberos docs: https://wiki.pvv.ntnu.no/wiki/Drift/Kerberos
- OpenLDAP smbk5pwd source: https://git.openldap.org/nivanova/openldap/-/tree/master/contrib/slapd-modules/smbk5pwd
- saslauthd(8): https://linux.die.net/man/8/saslauthd

View File

@@ -1,51 +0,0 @@
{ config, pkgs, values, lib, ... }:
{
imports = [
./hardware-configuration.nix
../../base.nix
../../misc/metrics-exporters.nix
./services/heimdal.nix
#./services/openldap.nix
./services/cyrus-sasl.nix
];
# buskerud does not support efi?
# boot.loader.systemd-boot.enable = true;
# boot.loader.efi.canTouchEfiVariables = true;
boot.loader.grub.enable = true;
boot.loader.grub.device = "/dev/sda";
# resolved messes up FQDN coming from nscd
services.resolved.enable = false;
networking.hostName = "dagali";
networking.domain = lib.mkForce "pvv.local";
networking.hosts = {
"129.241.210.185" = [ "dagali.pvv.local" ];
};
#networking.search = [ "pvv.ntnu.no" "pvv.org" ];
networking.nameservers = [ "129.241.0.200" "129.241.0.201" ];
networking.tempAddresses = "disabled";
networking.networkmanager.enable = true;
systemd.network.networks."ens18" = values.defaultNetworkConfig // {
matchConfig.Name = "ens18";
address = with values.hosts.dagali; [ (ipv4 + "/25") (ipv6 + "/64") ];
};
# List packages installed in system profile
environment.systemPackages = with pkgs; [
# TODO: consider adding to base.nix
nix-output-monitor
];
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. Its perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "24.05"; # Did you read the comment?
}

View File

@@ -1,33 +0,0 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/profiles/qemu-guest.nix")
];
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ ];
boot.extraModulePackages = [ ];
fileSystems."/" =
{ device = "/dev/disk/by-uuid/4de345e2-be41-4d10-9b90-823b2c77e9b3";
fsType = "ext4";
};
swapDevices =
[ { device = "/dev/disk/by-uuid/aa4b9a97-a7d8-4608-9f67-4ad084f1baf7"; }
];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.ens18.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}

View File

@@ -1,21 +0,0 @@
{ config, ... }:
let
cfg = config.services.saslauthd;
in
{
# TODO: This is seemingly required for openldap to authenticate
# against kerberos, but I have no idea how to configure it as
# such. Does it need a keytab? There's a binary "testsaslauthd"
# that follows with `pkgs.cyrus_sasl` that might be useful.
services.saslauthd = {
enable = true;
mechanism = "kerberos5";
config = ''
mech_list: gs2-krb5 gssapi
keytab: /etc/krb5.keytab
'';
};
# TODO: maybe the upstream module should consider doing this?
environment.systemPackages = [ cfg.package ];
}

View File

@@ -1,100 +0,0 @@
{ config, pkgs, lib, ... }:
let
realm = "PVV.LOCAL";
cfg = config.security.krb5;
in
{
security.krb5 = {
enable = true;
# NOTE: This is required in order to build smbk5pwd, because of some nested includes.
# We should open an issue upstream (heimdal, not nixpkgs), but this patch
# will do for now.
package = pkgs.heimdal.overrideAttrs (prev: {
postInstall = prev.postInstall + ''
cp include/heim_threads.h $dev/include
'';
});
settings = {
realms.${realm} = {
kdc = [ "dagali.${lib.toLower realm}" ];
admin_server = "dagali.${lib.toLower realm}";
kpasswd_server = "dagali.${lib.toLower realm}";
default_domain = lib.toLower realm;
primary_kdc = "dagali.${lib.toLower realm}";
};
kadmin.default_keys = lib.concatStringsSep " " [
"aes256-cts-hmac-sha1-96:pw-salt"
"aes128-cts-hmac-sha1-96:pw-salt"
];
libdefaults.default_etypes = lib.concatStringsSep " " [
"aes256-cts-hmac-sha1-96"
"aes128-cts-hmac-sha1-96"
];
libdefaults = {
default_realm = realm;
dns_lookup_kdc = false;
dns_lookup_realm = false;
};
domain_realm = {
"${lib.toLower realm}" = realm;
".${lib.toLower realm}" = realm;
};
logging = {
# kdc = "CONSOLE";
kdc = "SYSLOG:DEBUG:AUTH";
admin_server = "SYSLOG:DEBUG:AUTH";
default = "SYSLOG:DEBUG:AUTH";
};
};
};
services.kerberos_server = {
enable = true;
settings = {
realms.${realm} = {
dbname = "/var/lib/heimdal/heimdal";
mkey = "/var/lib/heimdal/m-key";
acl = [
{
principal = "kadmin/admin";
access = "all";
}
{
principal = "felixalb/admin";
access = "all";
}
{
principal = "oysteikt/admin";
access = "all";
}
];
};
# kadmin.default_keys = lib.concatStringsSep " " [
# "aes256-cts-hmac-sha1-96:pw-salt"
# "aes128-cts-hmac-sha1-96:pw-salt"
# ];
# libdefaults.default_etypes = lib.concatStringsSep " " [
# "aes256-cts-hmac-sha1-96"
# "aes128-cts-hmac-sha1-96"
# ];
# password_quality.min_length = 8;
};
};
networking.firewall.allowedTCPPorts = [ 88 464 749 ];
networking.firewall.allowedUDPPorts = [ 88 464 749 ];
networking.hosts = {
"127.0.0.2" = lib.mkForce [ ];
"::1" = lib.mkForce [ ];
};
}

View File

@@ -1,121 +0,0 @@
{ config, pkgs, lib, ... }:
{
services.openldap = let
dn = "dc=pvv,dc=ntnu,dc=no";
cfg = config.services.openldap;
heimdal = config.security.krb5.package;
in {
enable = true;
# NOTE: this is a custom build of openldap with support for
# perl and kerberos.
package = pkgs.openldap.overrideAttrs (prev: {
# https://github.com/openldap/openldap/blob/master/configure
configureFlags = prev.configureFlags ++ [
# Connect to slapd via UNIX socket
"--enable-local"
# Cyrus SASL
"--enable-spasswd"
# Reverse hostname lookups
"--enable-rlookups"
# perl
"--enable-perl"
];
buildInputs = prev.buildInputs ++ [
pkgs.perl
# NOTE: do not upstream this, it might not work with
# MIT in the same way
heimdal
];
extraContribModules = prev.extraContribModules ++ [
# https://git.openldap.org/openldap/openldap/-/tree/master/contrib/slapd-modules
"smbk5pwd"
];
});
settings = {
attrs = {
olcLogLevel = [ "stats" "config" "args" ];
# olcAuthzRegexp = ''
# gidNumber=.*\\\+uidNumber=0,cn=peercred,cn=external,cn=auth
# "uid=heimdal,${dn2}"
# '';
# olcSaslSecProps = "minssf=0";
};
children = {
"cn=schema".includes = let
# NOTE: needed for smbk5pwd.so module
schemaToLdif = name: path: pkgs.runCommandNoCC name {
buildInputs = with pkgs; [ schema2ldif ];
} ''
schema2ldif "${path}" > $out
'';
hdb-ldif = schemaToLdif "hdb.ldif" "${heimdal.src}/lib/hdb/hdb.schema";
samba-ldif = schemaToLdif "samba.ldif" "${heimdal.src}/tests/ldap/samba.schema";
in [
"${cfg.package}/etc/schema/core.ldif"
"${cfg.package}/etc/schema/cosine.ldif"
"${cfg.package}/etc/schema/nis.ldif"
"${cfg.package}/etc/schema/inetorgperson.ldif"
"${hdb-ldif}"
"${samba-ldif}"
];
# NOTE: installation of smbk5pwd.so module
# https://git.openldap.org/openldap/openldap/-/tree/master/contrib/slapd-modules/smbk5pwd
"cn=module{0}".attrs = {
objectClass = [ "olcModuleList" ];
olcModuleLoad = [ "${cfg.package}/lib/modules/smbk5pwd.so" ];
};
# NOTE: activation of smbk5pwd.so module for {1}mdb
"olcOverlay={0}smbk5pwd,olcDatabase={1}mdb".attrs = {
objectClass = [ "olcOverlayConfig" "olcSmbK5PwdConfig" ];
olcOverlay = "{0}smbk5pwd";
olcSmbK5PwdEnable = [ "krb5" "samba" ];
olcSmbK5PwdMustChange = toString (60 * 60 * 24 * 10000);
};
"olcDatabase={1}mdb".attrs = {
objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
olcDatabase = "{1}mdb";
olcSuffix = dn;
# TODO: PW is supposed to be a secret, but it's probably fine for testing
olcRootDN = "cn=users,${dn}";
# TODO: replace with proper secret
olcRootPW.path = pkgs.writeText "olcRootPW" "pass";
olcDbDirectory = "/var/lib/openldap/test-smbk5pwd-db";
olcDbIndex = "objectClass eq";
olcAccess = [
''{0}to attrs=userPassword,shadowLastChange
by dn.exact=cn=users,${dn} write
by self write
by anonymous auth
by * none''
''{1}to dn.base=""
by * read''
/* allow read on anything else */
# ''{2}to *
# by cn=users,${dn} write by dn.exact=gidNumber=0+uidNumber=0+cn=peercred,cn=external write
# by * read''
];
};
};
};
};
}

View File

@@ -0,0 +1 @@
target

View File

@@ -0,0 +1,171 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "apache-log-processor"
version = "0.1.0"
dependencies = [
"nix",
"time",
]
[[package]]
name = "bitflags"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "nix"
version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "num-conv"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "time"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"

View File

@@ -0,0 +1,19 @@
[package]
name = "apache-log-processor"
version = "0.1.0"
edition = "2024"
autobins = false
license = "MIT"
authors = [
"projects@pvv.ntnu.no",
]
[dependencies]
nix = { version = "0.31.3", features = ["event", "fs", "user"] }
time = { version = "0.3.47", features = ["formatting", "local-offset"] }
[[bin]]
name = "apache-log-processor"
bench = false
path = "src/main.rs"

View File

@@ -0,0 +1,33 @@
{
lib
, rustPlatform
, stdenv
}:
let
cargoToml = fromTOML (builtins.readFile ./Cargo.toml);
cargoLock = ./Cargo.lock;
mainProgram = (lib.head cargoToml.bin).name;
pname = cargoToml.package.name;
in
rustPlatform.buildRustPackage {
inherit pname;
inherit (cargoToml.package) version;
src = lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [
./Cargo.toml
./Cargo.lock
./src
];
};
cargoLock.lockFile = cargoLock;
doCheck = true;
meta = with lib; {
license = licenses.mit;
platforms = platforms.linux;
inherit mainProgram;
};
}

View File

@@ -0,0 +1,322 @@
use nix::{
errno::Errno,
fcntl::{FcntlArg, OFlag, fcntl, open},
sys::{
epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags, EpollTimeout},
stat::Mode,
},
// unistd::{User, getegid, geteuid, read, setegid, seteuid, write},
unistd::{User, read, write},
};
use std::{
collections::VecDeque,
os::fd::{AsFd, BorrowedFd, OwnedFd},
path::PathBuf,
process::exit,
};
use time::{OffsetDateTime, format_description};
const READ_BUFFER_SIZE: usize = 8 * 1024;
#[derive(Debug, Clone, Copy)]
enum LogMode {
Access,
Error,
}
fn main() -> Result<(), String> {
let log_mode = match std::env::args().nth(1).as_deref() {
Some("access") => LogMode::Access,
Some("error") => LogMode::Error,
Some(other) => {
return Err(format!(
"invalid log mode `{other}`; expected `access` or `error`"
));
}
None => return Err("missing log mode argument; expected `access` or `error`".to_string()),
};
let tee_file = match log_mode {
LogMode::Access => None,
LogMode::Error => Some(
open(
&PathBuf::from("/var/log/httpd/error.log"),
OFlag::O_WRONLY | OFlag::O_APPEND | OFlag::O_CREAT | OFlag::O_CLOEXEC,
Mode::S_IRUSR | Mode::S_IWUSR,
)
.map_err(|error| format!("failed to open error log for teeing: {error}"))?,
),
};
let stdin = std::io::stdin();
fcntl(stdin.as_fd(), FcntlArg::F_GETFL)
.map(OFlag::from_bits_retain)
.map(|flags| FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK))
.and_then(|flags| fcntl(stdin.as_fd(), flags))
.map_err(|error| format!("failed to make stdin nonblocking: {error}"))?;
let epoll = Epoll::new(EpollCreateFlags::EPOLL_CLOEXEC)
.map_err(|error| format!("failed to create epoll instance: {error}"))?;
epoll
.add(
stdin.as_fd(),
EpollEvent::new(
EpollFlags::EPOLLIN | EpollFlags::EPOLLERR | EpollFlags::EPOLLHUP,
0,
),
)
.map_err(|error| format!("failed to register stdin with epoll: {error}"))?;
if let Err(error) = event_loop(log_mode, epoll, stdin.as_fd(), tee_file) {
eprintln!("Error: {error}");
exit(1);
}
Ok(())
}
fn event_loop(
log_mode: LogMode,
epoll: Epoll,
stdin_fd: BorrowedFd<'_>,
mut tee_file: Option<OwnedFd>,
) -> Result<(), String> {
let mut events = [EpollEvent::empty(); 1];
let mut pending = VecDeque::new();
loop {
let ready = loop {
match epoll.wait(&mut events, EpollTimeout::NONE) {
Ok(ready) => break ready,
Err(Errno::EINTR) => continue,
Err(error) => {
return Err(format!("epoll wait failed: {error}"));
}
}
};
if ready == 0 {
continue;
}
let mut scratch = [0u8; READ_BUFFER_SIZE];
let eof = loop {
match read(stdin_fd, &mut scratch) {
Ok(0) => break true,
Ok(read_bytes) => pending.extend(scratch[..read_bytes].iter().copied()),
Err(Errno::EINTR) => continue,
Err(Errno::EAGAIN) => break false,
Err(error) => {
return Err(format!("failed to read from stdin: {error}"));
}
}
};
while let Some(newline_index) = pending.iter().position(|byte| *byte == b'\n') {
let line = pending.make_contiguous();
process_line(log_mode, &line[..=newline_index], &mut tee_file)?;
pending.drain(..=newline_index);
}
if eof {
if !pending.is_empty() {
process_line(log_mode, pending.make_contiguous(), &mut tee_file)?;
pending.clear();
}
return Ok(());
}
}
}
fn process_line(
log_mode: LogMode,
line: &[u8],
tee_file: &mut Option<OwnedFd>,
) -> Result<(), String> {
if let Some(tee_file) = tee_file.as_ref() {
write_all_fd(tee_file, line).map_err(|error| {
format!("failed to append to APACHE_LOG_PROCESSOR_TEE_FILE: {error}")
})?;
}
if let Some(user) =
parse_username_from_line(line).and_then(|name| User::from_name(name).ok().flatten())
{
// let identity = EffectiveIdentity::switch_to(&user).map_err(|error| {
// format!(
// "failed to switch effective identity to {} (uid {}, gid {}): {error}",
// user.name, user.uid, user.gid
// )
// })?;
let result: Result<(), String> = (|| {
let dir = user.dir.join("nobackup/weblogs");
if !dir.is_dir() {
return Err(format!(
"logs directory {} does not exist for user {}",
dir.display(),
user.name
));
}
let now = OffsetDateTime::now_local()
.unwrap_or_else(|_| OffsetDateTime::now_utc())
.format(&format_description::parse("[year]-[month]-[day]").unwrap())
.map_err(|error| {
format!("failed to format current date for log file name: {error}")
})?;
let logfile = dir.join(match log_mode {
LogMode::Access => format!("access-{now}.log"),
LogMode::Error => format!("error-{now}.log"),
});
let fd = open(
&logfile,
OFlag::O_WRONLY | OFlag::O_APPEND | OFlag::O_CREAT | OFlag::O_CLOEXEC,
Mode::S_IRUSR
| Mode::S_IWUSR
| Mode::S_IRGRP
| Mode::S_IROTH
| Mode::S_IWGRP
| Mode::S_IWOTH,
)
.map_err(|error| format!("failed to open log file for user {}: {error}", user.name))?;
write_all_fd(fd.as_fd(), line).map_err(|error| {
format!(
"failed to append to log file for user {}: {error}",
user.name
)
})?;
Ok(())
})();
if let Err(error) = result {
eprintln!("Error processing log line for user {}: {error}", user.name);
}
// identity.restore().map_err(|error| {
// format!(
// "failed to restore original effective identity after handling {}: {error}",
// user.name
// )
// })?;
}
Ok(())
}
fn parse_username_from_line(line: &[u8]) -> Option<&str> {
line.splitn(8, |&b| b == b' ')
.nth(6)
.and_then(|path| {
path.strip_prefix(b"/~")
.and_then(|rest| rest.split(|&b| b == b'/').next())
})
.or_else(|| {
line.windows(b"/home/pvv/".len())
.enumerate()
.find_map(|(start, window)| {
(window == b"/home/pvv/")
.then_some(start + b"/home/pvv/".len())
.and_then(|start| line.get(start..))
.filter(|rest| rest.get(1) == Some(&b'/'))
.and_then(|rest| rest.get(2..))
.and_then(|rest| rest.split(|&b| b == b'/').next())
})
})
.filter(|segment| !segment.is_empty())
.and_then(|segment| std::str::from_utf8(segment).ok())
}
fn write_all_fd<Fd: AsFd>(fd: Fd, mut buffer: &[u8]) -> nix::Result<()> {
while !buffer.is_empty() {
match write(fd.as_fd(), buffer) {
Ok(0) => return Err(Errno::EIO),
Ok(written) => buffer = &buffer[written..],
Err(Errno::EINTR) => continue,
Err(error) => return Err(error),
}
}
Ok(())
}
// struct EffectiveIdentity {
// saved_euid: nix::unistd::Uid,
// saved_egid: nix::unistd::Gid,
// restored: bool,
// }
// impl EffectiveIdentity {
// fn switch_to(user: &User) -> nix::Result<Self> {
// let guard = Self {
// saved_euid: geteuid(),
// saved_egid: getegid(),
// restored: false,
// };
// setegid(user.gid)?;
// if let Err(error) = seteuid(user.uid) {
// let _ = setegid(guard.saved_egid);
// return Err(error);
// }
// Ok(guard)
// }
// fn restore(mut self) -> nix::Result<()> {
// let restore_uid = seteuid(self.saved_euid);
// let restore_gid = setegid(self.saved_egid);
// self.restored = true;
// restore_uid?;
// restore_gid?;
// Ok(())
// }
// }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_user_from_access_log() {
let inputs = [(
"1.2.3.4 - - [25/May/2026:10:07:24 +0200] \"GET /~oysteikt/ HTTP/2.0\" 200 3708",
"oysteikt",
)];
for (line, expected_user) in inputs {
let parsed_user = parse_username_from_line(line.as_bytes());
assert_eq!(
parsed_user,
Some(expected_user),
"Failed to parse user from line: {line}"
);
}
}
#[test]
fn test_parse_user_from_error_log() {
let inputs = [(
"[Sat May 09 20:45:21.480016 2026] [authz_core:error] [pid 3555:tid 3617] [remote 1::2:42000] AH01630: client denied by server configuration: /home/pvv/d/oysteikt/web-docs/.git",
"oysteikt",
)];
for (line, expected_user) in inputs {
let parsed_user = parse_username_from_line(line.as_bytes());
assert_eq!(
parsed_user,
Some(expected_user),
"Failed to parse user from line: {line}"
);
}
}
}

View File

@@ -11,6 +11,8 @@ let
upload_max_filesize = "40M";
});
apache-log-processor = pkgs.callPackage ./apache-log-processor { };
# https://nixos.org/manual/nixpkgs/stable/#ssec-php-user-guide-installing-with-extensions
phpEnv = pkgs.php.buildEnv {
extensions = { all, ... }: with all; [
@@ -39,7 +41,7 @@ let
extraConfig = phpOptions;
};
perlEnv = pkgs.perl.withPackages (ps: with ps; [
perlEnv = (pkgs.perl.withPackages (ps: with ps; [
pkgs.exiftool
pkgs.ikiwiki
pkgs.irssi
@@ -54,7 +56,14 @@ let
ImageMagick
JSON
TemplateToolkit
]);
])).overrideAttrs (prev: {
# NOTE: `pkgs.perl.propagatedBuildInputs` don't actually propagate through the
# wrapper derivation created by `withPackages`. This should compensate
# for that.
postBuild = prev.postBuild + ''
cp -r '${pkgs.perl}/nix-support' "$out"/nix-support
'';
});
# https://nixos.org/manual/nixpkgs/stable/#python.buildenv-function
pythonEnv = pkgs.python3.buildEnv.override {
@@ -67,21 +76,6 @@ let
ignoreCollisions = true;
};
sendmailWrapper = pkgs.writeShellApplication {
name = "sendmail";
runtimeInputs = [ ];
text = ''
args=("$@")
if [[ -z "$USERDIR_USER" ]] && [[ "$USERDIR_USER" != "pvv" ]]; then
# Prepend -fusername to the argument list, so bounces go to the user
args=("-f$USERDIR_USER" "''${args[@]}")
fi
exec '${lib.getExe pkgs.system-sendmail}' "''${args[@]}"
'';
};
# https://nixos.org/manual/nixpkgs/stable/#sec-building-environment
fhsEnv = pkgs.buildEnv {
name = "userweb-env";
@@ -89,7 +83,7 @@ let
paths = with pkgs; [
bash
sendmailWrapper
config.services.bro.instances.userweb-sendmail.client.package
perlEnv
pythonEnv
@@ -184,31 +178,44 @@ in
extraModules = [
"systemd"
"userdir"
# TODO: I think the compilation steps of pkgs.apacheHttpdPackages.mod_perl might have some
# incorrect or restrictive assumptions upstream, either nixpkgs or source
# {
# name = "perl";
# path = let
# mod_perl = pkgs.apacheHttpdPackages.mod_perl.override {
# apacheHttpd = cfg.package.out;
# perl = perlEnv;
# };
# in "${mod_perl}/modules/mod_perl.so";
# }
{
name = "perl";
path = let
mod_perl = pkgs.symlinkJoin {
name = "userweb_modperl_with_custom_perl_env";
ignoreCollisions = true;
paths = [
(pkgs.apacheHttpdPackages.mod_perl.override {
apacheHttpd = cfg.package.out;
})
perlEnv
];
};
in "${mod_perl}/modules/mod_perl.so";
}
];
logPerVirtualHost = false;
extraConfig = ''
TraceEnable on
LogLevel warn rewrite:trace3
ScriptLog ${cfg.logDir}/cgi.log
'';
# virtualHosts."userweb.pvv.ntnu.no" = {
virtualHosts."temmie.pvv.ntnu.no" = {
forceSSL = true;
enableACME = true;
serverAliases = [
"www2.pvv.ntnu.no"
];
extraConfig = ''
CustomLog "${cfg.logDir}/access.log" combined
CustomLog "|${lib.getExe apache-log-processor} access" combined
ErrorLog "|${lib.getExe apache-log-processor} error"
ScriptLog "${cfg.logDir}/cgi.log"
UserDir ${lib.concatMapStringsSep " " (l: "/home/pvv/${l}/*/web-docs") homeLetters}
UserDir disabled root
AddHandler cgi-script .cgi
@@ -258,6 +265,14 @@ in
# ];
# };
# NOTE: 54 -> 33, this is the UID/GID we used for www-data on tom in the past.
# Any files accessed by or created by httpd will do so over NFS with this
# UID/GID pair as its credentials.
# This overlaps with the hardcoded `disnix` uid in nixpkgs, but we *probably*
# won't be using that for the foreseeable future.
users.users."wwwrun".uid = lib.mkForce 33;
users.groups."wwwrun".gid = lib.mkForce 33;
systemd.services.httpd = {
after = [ "pvv-homedirs.target" ];
requires = [ "pvv-homedirs.target" ];

View File

@@ -1,4 +1,4 @@
{ config, lib, ... }:
{ config, lib, pkgs, ... }:
{
services.postfix.enable = lib.mkForce false;
@@ -9,4 +9,111 @@
remotes = "mail.pvv.ntnu.no smtp --port=25";
};
};
services.bro = {
enable = true;
instances.userweb-sendmail = {
enable = true;
client = {
settings.BRO_FILE_FLAGS = [
"-C"
];
};
server = {
settings = {
executable = let
sendmailWrapper = pkgs.writeShellApplication {
name = "sendmail";
runtimeInputs = [ ];
bashOptions = [
"errexit"
"pipefail"
];
text = ''
args=("$@")
if [[ -z "$USERDIR_USER" ]] && [[ "$USERDIR_USER" != "pvv" ]]; then
# Prepend -fusername to the argument list, so bounces go to the user
args=("-f$USERDIR_USER" "''${args[@]}")
fi
exec '${lib.getExe pkgs.system-sendmail}' -t -i "''${args[@]}"
'';
};
in lib.getExe sendmailWrapper;
allowed-env = [ "USERDIR_USER" ];
};
};
};
};
environment.systemPackages = [
(config.services.bro.instances.userweb-sendmail.client.package.overrideAttrs (prev: {
buildCommand = prev.buildCommand + ''
mv "$out/bin/sendmail" "$out/bin/bro-sendmail"
'';
}))
];
users.users.nullmailer-user = {
enable = true;
isSystemUser = true;
group = "nullmailer-user";
};
users.groups.nullmailer-user = { };
systemd.services.bro-userweb-sendmail = {
serviceConfig = {
User = "nullmailer-user";
Group = "nullmailer-user";
ReadWritePaths = [
"/var/spool/nullmailer"
];
AmbientCapabilities = "";
CapabilityBoundingSet = "";
NoNewPrivileges = false;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateUsers = false;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
LockPersonality = true;
MemoryDenyWriteExecute = true;
PrivateMounts = true;
ProcSubset = "pid";
ProtectProc = "invisible";
RemoveIPC = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@resources"
];
UMask = "0077";
};
};
systemd.services.httpd.serviceConfig = {
BindPaths = [ (lib.head config.systemd.sockets.bro-userweb-sendmail.listenStreams) ];
};
}

View File

@@ -36,10 +36,6 @@ in rec {
ipv4 = pvv-ipv4 168;
ipv6 = pvv-ipv6 168;
};
dagali = {
ipv4 = pvv-ipv4 185;
ipv6 = pvv-ipv6 185;
};
ildkule = {
ipv4 = "129.241.100.145";
ipv4_internal = "192.168.1.17";