feat: init ap01 #178

Merged
rlahfa merged 19 commits from colmena-liminix-nng into main 2024-12-08 22:52:09 +01:00
23 changed files with 594 additions and 52 deletions

View file

@ -1,4 +1,15 @@
jobs: jobs:
ap01:
runs-on: nix
steps:
- uses: actions/checkout@v3
- env:
BUILD_NODE: ap01
STORE_ENDPOINT: https://tvix-store.dgnum.eu/infra-signing/
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
STORE_USER: admin
name: Build and cache ap01
run: nix-shell -A eval-nodes --run cache-node
bridge01: bridge01:
runs-on: nix runs-on: nix
steps: steps:

View file

@ -111,6 +111,11 @@ in
})) }))
pkgs.npins pkgs.npins
# SSO testing
pkgs.kanidm
pkgs.freeradius
pkgs.picocom # for serial access
(pkgs.callPackage ./lib/colmena { (pkgs.callPackage ./lib/colmena {
colmena = pkgs.callPackage "${sources.colmena}/package.nix" { }; colmena = pkgs.callPackage "${sources.colmena}/package.nix" { };
}) })

View file

@ -81,6 +81,7 @@ in
{ {
meta = { meta = {
nixpkgs = import nixpkgs.nixos.unstable.path;
nodeNixpkgs = mapSingleFuse nodePkgs nodes; nodeNixpkgs = mapSingleFuse nodePkgs nodes;
specialArgs = { specialArgs = {
@ -93,13 +94,40 @@ in
}; };
registry = { registry = {
zyxel-nwa50ax = {
evalConfig =
args:
(import "${sources.liminix}/lib/eval-config.nix" {
nixpkgs = args.specialArgs.sourcePkgs.path;
})
args;
defaults =
{ name, nodePath, ... }:
{
# Import the default modules
imports = [
# Import the base configuration for each node
./${nodePath}/_configuration.nix
./modules/generic
./modules/${category name}
];
# It's impure, but who cares?
# Can Flakes even do that? :)
nixpkgs.buildPlatform = builtins.currentSystem;
};
};
nixos = { nixos = {
evalConfig = args: import "${args.specialArgs.sourcePkgs.path}/nixos/lib/eval-config.nix" args; evalConfig = args: import "${args.specialArgs.sourcePkgs.path}/nixos/lib/eval-config.nix" args;
defaults = defaults =
{ {
lib,
name, name,
nodes,
nodeMeta, nodeMeta,
nodePath, nodePath,
meta,
sourcePkgs, sourcePkgs,
... ...
}: }:
@ -113,6 +141,10 @@ in
./modules/${category name} ./modules/${category name}
]; ];
_module.args.serverNodes = lib.filterAttrs (
name: _: meta.nodes.${name}.nixpkgs.system == "nixos"
) nodes;
# Include default secrets # Include default secrets
age-secrets.sources = [ ./${nodePath}/secrets ]; age-secrets.sources = [ ./${nodePath}/secrets ];

View file

@ -0,0 +1,40 @@
{
modulesPath,
sourcePkgs,
...
}:
{
imports = [
"${modulesPath}/wlan.nix"
"${modulesPath}/network"
"${modulesPath}/hostapd"
"${modulesPath}/ssh"
"${modulesPath}/ntp"
"${modulesPath}/vlan"
"${modulesPath}/bridge"
"${modulesPath}/jitter-rng"
"${modulesPath}/pki"
"${modulesPath}/ubus"
# System-level configuration
./system.nix
# Configures our own WLAN.
./wlan.nix
# Configures our LAN interfaces, e.g. bridge + VLANs.
./lan.nix
# Configures our IPv4/IPv6 addresses, e.g. DHCPv4 on VLAN 0, SLAAC on VLAN 3001.
./addresses.nix
# Configures a basic local DNS.
./dns.nix
# Configures our management layer, e.g. SSH server + DGNum FAI keys.
./management.nix
# Configures our recovery system, e.g. a levitation script.
./recovery.nix
# Metadata on the system for field recovery.
./metadata.nix
# TODO: god that's so a fucking hack.
(import "${modulesPath}/../devices/zyxel-nwa50ax").module
];
hostname = "ap01-prototype";
nixpkgs.source = sourcePkgs.path;
}

View file

@ -0,0 +1,18 @@
{ config, ... }:
let
svc = config.system.service;
in
{
services.dhcpv4 = svc.network.dhcp.client.build {
interface = config.services.int;
dependencies = [
config.services.bridge.components.lan
];
};
services.defaultroute4 = svc.network.route.build {
via = "$(output ${config.services.dhcpv4} router)";
target = "default";
dependencies = [ config.services.dhcpv4 ];
};
}

View file

@ -0,0 +1,30 @@
{ config, pkgs, ... }:
let
inherit (pkgs.liminix.services) oneshot;
inherit (pkgs.pseudofile) dir symlink;
inherit (pkgs) serviceFns;
in
{
# TODO: support dynamic reconfiguration once we are in the target VLAN?
services.resolvconf = oneshot rec {
name = "resolvconf";
up = ''
. ${serviceFns}
( in_outputs ${name}
for i in $(output ${config.services.dhcpv4} dns); do
echo "nameserver $i" >> resolv.conf
done
)
'';
dependencies = [
config.services.dhcpv4
];
};
filesystem = dir {
etc = dir {
"resolv.conf" = symlink "${config.services.resolvconf}/.outputs/resolv.conf";
};
};
}

View file

@ -0,0 +1,8 @@
{ config, ... }:
let
svc = config.system.service;
in
{
# ubus socket for various needs.
services.ubus = svc.ubus.build { };
}

View file

@ -0,0 +1,39 @@
{ config, ... }:
let
svc = config.system.service;
in
{
services.int = svc.bridge.primary.build {
ifname = "int";
macAddressFromInterface = config.hardware.networkInterfaces.lan;
};
services.bridge = svc.bridge.members.build {
primary = config.services.int;
members = {
lan.member = config.hardware.networkInterfaces.lan;
wlan0 = {
member = config.hardware.networkInterfaces.wlan0;
# Bridge only once hostapd is ready.
dependencies = [ config.services.hostap-1-ready ];
};
wlan1 = {
member = config.hardware.networkInterfaces.wlan1;
# Bridge only once hostapd is ready.
dependencies = [ config.services.hostap-2-ready ];
};
};
};
# Default VLAN
# services.vlan-apro = svc.vlan.build {
# vlanId = 0;
# interface = config.services.int;
# };
# # Administration VLAN
# services.vlan-admin = svc.vlan.build {
# vlan = 3001;
# interface = config.services.int;
# };
}

View file

@ -0,0 +1,12 @@
{ config, ... }:
let
svc = config.system.service;
in
{
# SSH keys are handled by the access control module.
dgn-access-control.enable = true;
users.root = {
passwd = "$6$Z2MiaMXkpUJRPl2/$fxVE3iD/n208CISM2F6OnWj0Qq0QG2tTQqLCjU80PFJJGIwNLLyOp6SeYH3dH20OvJX1loZRETrThZfIPw.rb/";
};
services.sshd = svc.ssh.build { allowRoot = true; };
}

View file

@ -0,0 +1,15 @@
{ pkgs, ... }:
let
inherit (pkgs.pseudofile) dir;
in
{
filesystem = dir {
etc = dir {
"nixpkgs.version" = {
type = "f";
file = "${pkgs.lib.version}";
mode = "0444";
};
};
};
}

View file

@ -0,0 +1,45 @@
{
config,
pkgs,
modulesPath,
...
}:
let
svc = config.system.service;
in
{
defaultProfile.packages = with pkgs; [
# Levitate enable us to mass-reinstall the system on the fly.
(levitate.override {
config = {
imports = [
"${modulesPath}/network"
"${modulesPath}/ssh"
"${modulesPath}/hardware.nix"
"${modulesPath}/kernel"
"${modulesPath}/outputs/tftpboot.nix"
"${modulesPath}/outputs.nix"
];
nixpkgs.buildPlatform = builtins.currentSystem;
services = {
# In this situation, we fallback to the appro VLAN.
# TODO: add support for the admin VLAN.
# Simplest DHCPv4 we can find.
dhcpv4 = svc.network.dhcp.client.build {
interface = config.hardware.networkInterfaces.lan;
};
inherit (config.services) sshd;
defaultroute4 = svc.network.route.build {
via = "$(output ${config.services.dhcpv4} router)";
target = "default";
dependencies = [ config.services.dhcpv4 ];
};
};
defaultProfile.packages = [ mtdutils ];
# Only keep root, which should inherit from DGN access control's root permissions.
users.root = config.users.root;
};
})
];
}

View file

@ -0,0 +1,28 @@
{ pkgs, config, ... }:
let
svc = config.system.service;
in
{
# Get moar random please
services = {
jitter = svc.jitter-rng.build { };
packet_forwarding = svc.network.forward.build { };
ntp = config.system.service.ntp.build {
pools = {
"pool.ntp.org" = [ "iburst" ];
};
dependencies = [ config.services.jitter ];
};
};
boot.tftp = {
serverip = "192.0.2.10";
ipaddr = "192.0.2.12";
};
defaultProfile.packages = with pkgs; [
zyxel-bootconfig
min-collect-garbage
];
}

View file

@ -0,0 +1,93 @@
{ config, pkgs, ... }:
let
svc = config.system.service;
secrets-1 = {
ssid = "DGNum 2G (N)";
};
secrets-2 = {
ssid = "DGNum 5G (AX)";
};
baseParams = {
country_code = "FR";
hw_mode = "g";
channel = 6;
wmm_enabled = 1;
ieee80211n = 1;
ht_capab = "[LDPC][GF][HT40-][HT40+][SHORT-GI-40][MAX-AMSDU-7935][TX-STBC]";
auth_algs = 1;
wpa = 2;
wpa_pairwise = "TKIP CCMP";
rsn_pairwise = "CCMP";
};
radiusKeyMgmt = {
wpa_key_mgmt = "WPA-EAP";
};
modernParams = {
hw_mode = "a";
he_su_beamformer = 1;
he_su_beamformee = 1;
he_mu_beamformer = 1;
preamble = 1;
# Allow radar detection.
ieee80211d = 1;
ieee80211h = 1;
ieee80211ac = 1;
ieee80211ax = 1;
vht_capab = "[MAX-MPDU-7991][SU-BEAMFORMEE][SU-BEAMFORMER][RXLDPC][SHORT-GI-80][MAX-A-MPDU-LEN-EXP3][RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN][TX-STBC-2BY1][RX-STBC-1][MU-BEAMFORMER]";
vht_oper_chwidth = 1;
he_oper_chwidth = 1;
channel = 36;
vht_oper_centr_freq_seg0_idx = 42;
he_oper_centr_freq_seg0_idx = 42;
require_vht = 1;
};
clientRadius = {
ieee8021x = 1;
eapol_version = 2;
use_pae_group_addr = 1;
dynamic_vlan = 0;
vlan_tagged_interface = "lan";
};
externalRadius = {
# TODO: when we have proper IPAM, set the right value here.
own_ip_addr = "127.0.0.1";
nas_identifier = "ap01.dgnum.eu";
# No DNS here, hostapd do not support this mode.
auth_server_addr = "129.199.195.129";
auth_server_port = 1812;
auth_server_shared_secret = "read it online";
};
mkWifiSta =
params: interface: secrets:
svc.hostapd.build {
inherit interface;
package = pkgs.hostapd-radius;
params = params // secrets;
dependencies = [ config.services.jitter ];
};
in
{
services = {
# wlan0 is the 2.4GHz interface.
hostap-1 = mkWifiSta (
baseParams // radiusKeyMgmt
) config.hardware.networkInterfaces.wlan0 secrets-1;
hostap-1-ready = svc.hostapd-ready.build {
interface = config.hardware.networkInterfaces.wlan0;
};
# wlan1 is the 5GHz interface, e.g. AX capable.
hostap-2 = mkWifiSta (
baseParams // clientRadius // externalRadius // radiusKeyMgmt // modernParams
) config.hardware.networkInterfaces.wlan1 secrets-2;
# Oneshot that waits until the hostapd has set the interface in operational state.
hostap-2-ready = svc.hostapd-ready.build {
interface = config.hardware.networkInterfaces.wlan1;
};
};
}

View file

@ -1,7 +1,7 @@
{ {
config, config,
lib, lib,
nodes, serverNodes,
sources, sources,
... ...
}: }:
@ -28,16 +28,18 @@ let
port = 3001; port = 3001;
httpExcludes = [ httpExcludes =
"localhost" [
"ens.cal.dgnum.eu" "localhost"
"luj-current.cal.dgnum.eu" "ens.cal.dgnum.eu"
"s3.dgnum.eu" "luj-current.cal.dgnum.eu"
"cdn.dgnum.eu" "s3.dgnum.eu"
"saml-idp.dgnum.eu" "cdn.dgnum.eu"
"status.dgnum.eu" "saml-idp.dgnum.eu"
"radius.dgnum.eu" "status.dgnum.eu"
] ++ (concatLists (mapAttrsToList (_: { config, ... }: config.dgn-redirections.retired) nodes)); "radius.dgnum.eu"
]
++ (concatLists (mapAttrsToList (_: { config, ... }: config.dgn-redirections.retired) serverNodes));
extraProbes = { extraProbes = {
monitors = { monitors = {
@ -88,7 +90,7 @@ let
}; };
pingProbes = pingProbesFromHive { pingProbes = pingProbesFromHive {
inherit nodes; nodes = serverNodes;
mkHost = _: config: config.networking.fqdn; mkHost = _: config: config.networking.fqdn;
tags = [ { name = "Ping"; } ]; tags = [ { name = "Ping"; } ];
excludes = [ excludes = [
@ -99,7 +101,7 @@ let
}; };
vpnProbes = pingProbesFromHive { vpnProbes = pingProbesFromHive {
inherit nodes; nodes = serverNodes;
prefix = "VPN - "; prefix = "VPN - ";
mkHost = node: _: "${node}.dgnum"; mkHost = node: _: "${node}.dgnum";
tags = [ { name = "VPN"; } ]; tags = [ { name = "VPN"; } ];
@ -110,7 +112,7 @@ let
}; };
httpProbes = fromHive { httpProbes = fromHive {
inherit nodes; nodes = serverNodes;
builder = builder =
_: module: _: module:
httpProbesFromConfig { httpProbesFromConfig {

View file

@ -1,6 +1,6 @@
{ {
config, config,
nodes, serverNodes,
lib, lib,
... ...
}: }:
@ -19,7 +19,7 @@ let
host = node; host = node;
}; };
} }
) nodes ) serverNodes
); );
in in

View file

@ -177,4 +177,18 @@
system = "nixos"; system = "nixos";
}; };
}; };
ap01 = {
site = "unknown";
adminGroups = [ "fai" ];
hashedPassword = "$y$j9T$DMOQEWOYFHjNS0myrXp4x/$MG33VSdXGvib.99eN.AbvyVdNNJw4ERjAwK4.ULJe/A";
stateVersion = null;
nixpkgs = {
system = "zyxel-nwa50ax";
version = "24.05";
};
};
} }

View file

@ -6,6 +6,7 @@ let
mkDefault mkDefault
mkIf mkIf
mkOption mkOption
optionalAttrs
; ;
inherit (lib.types) inherit (lib.types)
@ -133,7 +134,7 @@ in
}; };
stateVersion = mkOption { stateVersion = mkOption {
type = str; type = nullOr str;
description = '' description = ''
State version of the node. State version of the node.
''; '';
@ -203,15 +204,18 @@ in
}; };
config = { config = {
deployment = { deployment =
tags = [ "infra-${config.site}" ]; {
targetHost = mkIf (builtins.hasAttr name args.config.network) ( tags = [ "infra-${config.site}" ];
let }
ip = with args.config.network.${name}.addresses; ipv4 ++ ipv6; // (optionalAttrs (builtins.hasAttr name args.config.network) {
in targetHost =
mkIf (ip != [ ]) (mkDefault (builtins.head ip)) let
); ip = with args.config.network.${name}.addresses; ipv4 ++ ipv6;
}; in
mkIf (ip != [ ]) (mkDefault (builtins.head ip));
});
}; };
} }
) )

View file

@ -0,0 +1,6 @@
{
imports = [
# List of modules to import
./dgn-access-control.nix
];
}

View file

@ -0,0 +1,89 @@
# Copyright :
# - Tom Hubrecht <tom.hubrecht@dgnum.eu> 2023
#
# Ce logiciel est un programme informatique servant à déployer des
# configurations de serveurs via NixOS.
#
# Ce logiciel est régi par la licence CeCILL soumise au droit français et
# respectant les principes de diffusion des logiciels libres. Vous pouvez
# utiliser, modifier et/ou redistribuer ce programme sous les conditions
# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA
# sur le site "http://www.cecill.info".
#
# En contrepartie de l'accessibilité au code source et des droits de copie,
# de modification et de redistribution accordés par cette licence, il n'est
# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons,
# seule une responsabilité restreinte pèse sur l'auteur du programme, le
# titulaire des droits patrimoniaux et les concédants successifs.
#
# A cet égard l'attention de l'utilisateur est attirée sur les risques
# associés au chargement, à l'utilisation, à la modification et/ou au
# développement et à la reproduction du logiciel par l'utilisateur étant
# donné sa spécificité de logiciel libre, qui peut le rendre complexe à
# manipuler et qui le réserve donc à des développeurs et des professionnels
# avertis possédant des connaissances informatiques approfondies. Les
# utilisateurs sont donc invités à charger et tester l'adéquation du
# logiciel à leurs besoins dans des conditions permettant d'assurer la
# sécurité de leurs systèmes et ou de leurs données et, plus généralement,
# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité.
#
# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez
# pris connaissance de la licence CeCILL, et que vous en avez accepté les
# termes.
{
config,
lib,
dgn-keys,
meta,
nodeMeta,
...
}:
let
inherit (lib)
mkDefault
mkEnableOption
mkIf
mkOption
types
;
admins =
meta.organization.groups.root
++ nodeMeta.admins
++ (builtins.concatMap (g: meta.organization.groups.${g}) nodeMeta.adminGroups);
cfg = config.dgn-access-control;
in
{
options.dgn-access-control = {
enable = mkEnableOption "DGNum access control." // {
default = true;
};
users = mkOption {
type = with types; attrsOf (listOf str);
default = { };
description = ''
Attribute set describing which member has access to which user on the node.
Members must be declared in `meta/members.nix`.
'';
example = ''
{
user1 = [ "member1" "member2" ];
}
'';
};
};
config = mkIf cfg.enable {
# Admins have root access to the node
dgn-access-control.users.root = mkDefault admins;
users = builtins.mapAttrs (_: members: {
openssh.authorizedKeys.keys = dgn-keys.getKeys members;
}) cfg.users;
};
}

View file

@ -45,8 +45,8 @@ let
mkDefault mkDefault
mkEnableOption mkEnableOption
mkIf mkIf
mkMerge
mkOption mkOption
optionalAttrs
types types
; ;
@ -80,22 +80,16 @@ in
}; };
}; };
config = mkIf cfg.enable (mkMerge [ config = mkIf cfg.enable {
{ # Admins have root access to the node
# Admins have root access to the node dgn-access-control.users.root = mkDefault admins;
dgn-access-control.users.root = mkDefault admins; users.mutableUsers = false;
users.users = builtins.mapAttrs (
users.users = builtins.mapAttrs (_: members: { username: members:
{
openssh.authorizedKeys.keys = dgn-keys.getKeys members; openssh.authorizedKeys.keys = dgn-keys.getKeys members;
}) cfg.users; }
} // optionalAttrs (username == "root") { inherit (nodeMeta) hashedPassword; }
{ ) cfg.users;
users = { };
mutableUsers = false;
users.root = {
inherit (nodeMeta) hashedPassword;
};
};
}
]);
} }

View file

@ -127,14 +127,13 @@
"liminix": { "liminix": {
"type": "Git", "type": "Git",
"repository": { "repository": {
"type": "GitHub", "type": "Git",
"owner": "RaitoBezarius", "url": "https://git.dgnum.eu/DGNum/liminix"
"repo": "liminix"
}, },
"branch": "nwa50ax", "branch": "main",
"revision": "a4aa10dcc30225a8bb8eb465abfe908629175f2c", "revision": "473d6acc3de70bd6dbbb4a77af54f508f25c3c9c",
"url": "https://github.com/RaitoBezarius/liminix/archive/a4aa10dcc30225a8bb8eb465abfe908629175f2c.tar.gz", "url": null,
"hash": "1m1sc6agg5z65lmyjl48i7sddlwm8d0zgvs8z81iammfy4jpy7qd" "hash": "00slsh0yqd8n8jcx3sbxgcmw1z28bnszy87pfs0ynfkl3bldzs3d"
}, },
"linkal": { "linkal": {
"type": "Git", "type": "Git",

23
scripts/cache-node.sh Normal file → Executable file
View file

@ -3,7 +3,28 @@ set -o nounset
set -o pipefail set -o pipefail
shopt -s lastpipe shopt -s lastpipe
drv=$(colmena eval --instantiate -E "{ nodes, ... }: nodes.${BUILD_NODE}.config.system.build.toplevel") # Remove the `nixpkgs=` default input.
export NIX_PATH="nixpkgs="
system_type="$(colmena eval -E "{ nodes, ... }: nodes.${BUILD_NODE}.config.deployment.systemType" --show-trace)"
# Get rid of surrounding quotes.
system_type="${system_type%\"}"
system_type="${system_type#\"}"
case "$system_type" in
nixos)
toplevel_path="config.system.build.toplevel"
;;
zyxel-nwa50ax)
toplevel_path="config.system.outputs.zyxel-nwa-fit"
;;
*)
echo "Unsupported system type '$system_type' for caching; add an entry in 'scripts/cache-node.sh'"
exit 1
;;
esac
drv=$(colmena eval --instantiate -E "{ nodes, ... }: nodes.${BUILD_NODE}.${toplevel_path}" --show-trace)
# Build the derivation and send it to the great beyond # Build the derivation and send it to the great beyond
nix-store --query --requisites --force-realise --include-outputs "$drv" | grep -v '.*\.drv' >paths.txt nix-store --query --requisites --force-realise --include-outputs "$drv" | grep -v '.*\.drv' >paths.txt

37
scripts/push-to-cache.sh Executable file
View file

@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
shopt -s lastpipe
output_path="$1"
if [ "$STORE_ENDPOINT" == "" ]; then
echo "No endpoint given for the remote cache, uploading cannot take place."
exit 0
fi
if [ "$STORE_USER" == "" ]; then
echo "No user given for the remote cache, uploading cannot take place."
exit 0
fi
if [ "$STORE_PASSWORD" == "" ]; then
echo "No password given for the remote cache, uploading cannot take place."
exit 1
fi
cat <<EOF >.netrc
default
login $STORE_USER
password $STORE_PASSWORD
EOF
nix copy \
--extra-experimental-features nix-command \
--to "$STORE_ENDPOINT?compression=none" \
--netrc-file .netrc \
"$output_path"
rm .netrc