Compare commits
3
Commits
096e534e91
...
ae3461b085
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae3461b085 | ||
|
|
6bcb16b8ab | ||
|
|
77b089bc45 |
+65
-1
@@ -1,7 +1,9 @@
|
||||
# Socle commun à toutes les machines de la flotte (repo public).
|
||||
{ pkgs, ... }:
|
||||
{ pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
imports = [ ../../modules/anssi ];
|
||||
|
||||
# Toutes les machines de la flotte portent le même nom : pas
|
||||
# d'identification individuelle pour l'instant (voir README du repo
|
||||
# racine). À revoir plus tard via le numéro de série DMI si un vrai
|
||||
@@ -9,6 +11,68 @@
|
||||
networking.hostName = "Nixos_Fleet";
|
||||
networking.networkmanager.enable = true;
|
||||
|
||||
# Sans ça, les firmwares non-libres (wifi/Bluetooth Broadcom, Realtek,
|
||||
# Intel...) ne sont pas inclus dans le système. Sur un parc de PC
|
||||
# hétérogène et inconnu à l'avance, une partie aura besoin de ces
|
||||
# firmwares pour que la carte wifi soit seulement détectée (option
|
||||
# documentée : nixos.wiki/wiki/NixOS_Installation_Guide, section
|
||||
# firmware). mkDefault : un profil peut désactiver au cas par cas.
|
||||
hardware.enableRedistributableFirmware = lib.mkDefault true;
|
||||
|
||||
# Durcissement : reprend le module ANSSI de SécurixOS (modules/anssi/,
|
||||
# vendored ci-dessus), qui remplace/couvre déjà tous les sysctls qui
|
||||
# étaient déclarés à la main sur le profil oreo (dmesg_restrict,
|
||||
# kptr_restrict, yama.ptrace_scope, rp_filter, tcp_syncookies,
|
||||
# protected_hardlinks/symlinks -- mêmes valeurs, vérifié règle par
|
||||
# règle dans modules/anssi/kernel-options.nix : R9, R11, R12, R14).
|
||||
# level "intermediary" : c'est le niveau que couvraient déjà TOUTES
|
||||
# les valeurs qu'on avait mises à la main sur oreo -- rien de plus
|
||||
# fort n'a été inventé ici. category "client" : ce sont des postes de
|
||||
# travail, pas des serveurs (les règles actuellement implémentées sont
|
||||
# toutes "category = base" donc ça ne change rien pour l'instant, mais
|
||||
# c'est le bon réglage si Sécurix ajoute des règles catégorisées).
|
||||
#
|
||||
# ATTENTION : R12 force net.ipv4.ip_forward = 0. Sans impact ici (ce
|
||||
# sont des postes clients), mais à ne SURTOUT PAS importer sur
|
||||
# pxe-server/configuration.nix, qui a besoin l'inverse (voir son
|
||||
# networking.nat) -- pxe-server n'importe pas ce fichier, donc ok.
|
||||
security.anssi = {
|
||||
enable = true;
|
||||
level = "intermediary";
|
||||
category = "client";
|
||||
|
||||
# R8 (mitigations CPU) désactive l'hyperthreading via "mds=full,nosmt"
|
||||
# -- impact perf trop lourd sur le matériel bas de gamme du parc (PC
|
||||
# à 4 Go de RAM). On désactive donc la règle via le mécanisme d'exception
|
||||
# prévu par le module lui-même (modules/anssi/options.nix), PAS en la
|
||||
# contournant en silence.
|
||||
exceptions.R8.rationale = "SMT (hyperthreading) volontairement conservé : impact perf trop lourd sur le parc (matériel bas de gamme, parfois 4 Go de RAM). Les autres mitigations de R8 sont réappliquées ci-dessous, à l'identique du texte source (modules/anssi/kernel-options.nix), sauf 'nosmt' remplacé par la valeur alternative documentée 'mds=full' (mitige MDS sans couper l'hyperthreading).";
|
||||
};
|
||||
|
||||
# Reprise verbatim de modules/anssi/kernel-options.nix (règle R8), sauf
|
||||
# "mds=full,nosmt" -> "mds=full" (voir exception R8 ci-dessus). Toutes
|
||||
# les autres valeurs viennent du texte source, aucune n'est inventée.
|
||||
boot.kernelParams = [
|
||||
"l1tf=full,force"
|
||||
"page_poison=on"
|
||||
"pti=on"
|
||||
"slab_nomerge=yes"
|
||||
"slub_debug=FZP"
|
||||
"spec_store_bypass_disable=seccomp"
|
||||
"spectre_v2=on"
|
||||
"mds=full" # sans ",nosmt" : hyperthreading conservé
|
||||
"mce=0"
|
||||
"page_alloc.shuffle=1"
|
||||
"rng_core.default_quality=500"
|
||||
];
|
||||
|
||||
# Pas (encore) couvert par le module ANSSI ci-dessus (R45 y est
|
||||
# explicitement listé en exception : "AppArmor is partially supported
|
||||
# on NixOS", modules/anssi/base.nix) -- repris tel quel du profil oreo.
|
||||
systemd.coredump.enable = false;
|
||||
security.apparmor.packages = [ pkgs.apparmor-profiles ];
|
||||
security.apparmor.enableCache = true;
|
||||
|
||||
time.timeZone = "Europe/Paris";
|
||||
i18n.defaultLocale = "fr_FR.UTF-8";
|
||||
i18n.extraLocaleSettings = {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
{
|
||||
services.xserver.enable = true;
|
||||
services.xserver.displayManager.lightdm.enable = true;
|
||||
services.xserver.displayManager.lightdm.background = "/etc/nixos/hosts/common/wallpaper/Nix/13977413.png";
|
||||
services.xserver.desktopManager.cinnamon.enable = true;
|
||||
services.accounts-daemon.enable = true;
|
||||
|
||||
@@ -14,6 +15,17 @@
|
||||
warpinator
|
||||
];
|
||||
|
||||
# Fond d'écran par défaut (juste le logo NixOS, pas la galerie perso de
|
||||
# nixos_cinnamon -- repo public, on n'y met que ce qui est nécessaire).
|
||||
# Ce chemin (/etc/nixos) suppose la config installée là -- voir
|
||||
# nixos_fleet/installer/install.sh (do_install copie le clone sur le
|
||||
# disque cible après nixos-install).
|
||||
environment.etc."dconf/db/local.d/00-background".text = ''
|
||||
[org/cinnamon/desktop/background]
|
||||
picture-uri='file:///etc/nixos/hosts/common/wallpaper/Nix/13977413.png'
|
||||
'';
|
||||
system.activationScripts.dconfUpdate.text = "${pkgs.dconf}/bin/dconf update";
|
||||
|
||||
# Réglages de confidentialité génériques (pas de SearXNG/DNS personnels
|
||||
# comme sur le poste perso — cette config est destinée à être publique).
|
||||
programs.firefox = {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
+32
-1
@@ -92,6 +92,32 @@ do_install() {
|
||||
|
||||
swapoff /mnt/.install-swapfile
|
||||
rm -f /mnt/.install-swapfile
|
||||
|
||||
# Sans cette étape, la machine installée n'a AUCUNE config sur son
|
||||
# disque : le clone ci-dessus vit dans /tmp de l'environnement live, en
|
||||
# RAM, effacé au reboot. La convergence auto (system.autoUpgrade, voir
|
||||
# hosts/common/base.nix) continue de fonctionner sans ça (elle pointe
|
||||
# sur l'URL distante), mais un technicien qui arrive sur la machine
|
||||
# après coup n'a alors ni /etc/nixos, ni moyen d'ajouter/enlever un
|
||||
# paquet à la main. On installe donc le même clone (déjà substitué avec
|
||||
# le vrai disque) à l'emplacement standard NixOS, en conservant .git
|
||||
# pour que `git pull`/`git log` marchent dessus normalement ensuite.
|
||||
echo "Installation de la configuration dans /mnt/etc/nixos..."
|
||||
rm -rf /mnt/etc/nixos
|
||||
mkdir -p /mnt/etc
|
||||
cp -a "$repo_dir" /mnt/etc/nixos
|
||||
|
||||
# Le disque cible a été substitué localement (sed plus haut) : ce fichier
|
||||
# diffère donc de ce qui est sur git (placeholder). On le committe en
|
||||
# local (jamais poussé) pour que `git status` reste propre sur la
|
||||
# machine installée -- sinon chaque `git pull` ultérieur affiche un
|
||||
# fichier modifié, source de confusion pour un technicien.
|
||||
git -C /mnt/etc/nixos \
|
||||
-c user.name="Installeur Nixos_Fleet" \
|
||||
-c user.email="installeur@nixos-fleet.local" \
|
||||
commit -aqm "Substitution locale du disque cible ($DEVICE)"
|
||||
|
||||
INSTALLED_ATTR="$attr"
|
||||
}
|
||||
|
||||
echo "=== Installeur Nixos_Fleet ==="
|
||||
@@ -190,6 +216,11 @@ case "$CHOICE" in
|
||||
esac
|
||||
|
||||
echo
|
||||
echo "Installation terminée. Redémarrage dans 10s (Ctrl-C pour annuler et inspecter /mnt)..."
|
||||
echo "Installation terminée."
|
||||
echo "Config installée dans /etc/nixos (attribut : ${INSTALLED_ATTR})."
|
||||
echo "Pour ajouter/enlever un paquet après le premier démarrage :"
|
||||
echo " 1. éditer les fichiers dans /etc/nixos"
|
||||
echo " 2. nixos-rebuild switch --flake /etc/nixos#${INSTALLED_ATTR}"
|
||||
echo "Redémarrage dans 10s (Ctrl-C pour annuler et inspecter /mnt)..."
|
||||
sleep 10
|
||||
reboot
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2026 Ryan Lahfa <ryan.lahfa@numerique.gouv.fr>
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
|
||||
# ANSSI compliance module
|
||||
|
||||
This is a self-contained NixOS implementation of <https://messervices.cyber.gouv.fr/guides/recommandations-de-securite-relatives-un-systeme-gnulinux>.
|
||||
It can be copy-pasted outside of Sécurix for your own needs.
|
||||
|
||||
> Vendored verbatim depuis [SécurixOS](https://github.com/cloud-gouv/securix)
|
||||
> (`modules/anssi/`, alpha) le 2026-09-09, tel que suggéré par ce README
|
||||
> ("can be copy-pasted outside of Sécurix"). Ne pas éditer ces fichiers
|
||||
> directement : en cas de correctif, le reprendre depuis Sécurix.
|
||||
@@ -0,0 +1,15 @@
|
||||
# SPDX-FileCopyrightText: 2026 Ryan Lahfa <ryan.lahfa@numerique.gouv.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
{
|
||||
# Default exceptions for MACs.
|
||||
security.anssi.exceptions = {
|
||||
R37.rationale = "No MAC implementation is available on NixOS at the moment.";
|
||||
R45.rationale = "AppArmor is partially supported on NixOS.";
|
||||
R46.rationale = "SELinux is unsupported on NixOS.";
|
||||
R47.rationale = "SELinux is unsupported on NixOS.";
|
||||
R48.rationale = "SELinux is unsupported on NixOS.";
|
||||
R49.rationale = "SELinux is unsupported on NixOS.";
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# SPDX-FileCopyrightText: 2026 Ryan Lahfa <ryan.lahfa@numerique.gouv.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
{
|
||||
imports = [
|
||||
./options.nix
|
||||
./base.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
# SPDX-FileCopyrightText: 2026 Ryan Lahfa <ryan.lahfa@numerique.gouv.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
{
|
||||
# rulesets: a set of rules, e.g., { R1 = {...}; R2 = {...}; ... }
|
||||
generateOptionHierarchy =
|
||||
rulesets:
|
||||
let
|
||||
levelMapping = {
|
||||
"minimal" = 0;
|
||||
"intermediary" = 1;
|
||||
"reinforced" = 2;
|
||||
"high" = 3;
|
||||
};
|
||||
# helper to generate one rule's option set
|
||||
makeRuleOption =
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
ruleId: rule:
|
||||
{
|
||||
anssiRef = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = rule.anssiRef;
|
||||
readOnly = true;
|
||||
description = "ANSSI reference for rule ${rule.name}";
|
||||
};
|
||||
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default =
|
||||
config.security.anssi.enable
|
||||
&& levelMapping.${config.security.anssi.level} >= levelMapping.${rule.severity}
|
||||
&& (rule.category == "base" || rule.category == config.security.anssi.category)
|
||||
&& lib.all (t: !(lib.elem t config.security.anssi.excludes)) (rule.tags or [ ])
|
||||
&& (
|
||||
((rule.architectures or [ ]) != [ ])
|
||||
-> lib.elem pkgs.stdenv.hostPlatform.linuxArch rule.architectures
|
||||
)
|
||||
&& !config.security.anssi.exceptions ? ${ruleId};
|
||||
|
||||
description = ''
|
||||
Enable rule ${rule.name}.
|
||||
${rule.description}
|
||||
ANSSI reference: ${rule.anssiRef}.
|
||||
'';
|
||||
};
|
||||
|
||||
exceptionRationale = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default =
|
||||
if config.security.anssi.exceptions ? ${ruleId} then
|
||||
config.security.anssi.exceptions.${ruleId}.rationale
|
||||
else
|
||||
null;
|
||||
readOnly = true;
|
||||
description = "Optional rationale for disabling or modifying rule ${rule.name}";
|
||||
};
|
||||
|
||||
checkScript = lib.mkOption {
|
||||
type = lib.types.path; # TODO: or string if we store the script inline
|
||||
default = rule.checkScript pkgs;
|
||||
description = "Check script for rule ${rule.name}";
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (rule ? implementations) {
|
||||
implementation = lib.mkOption {
|
||||
type = lib.types.enum (lib.attrNames rule.implementations);
|
||||
description = "Chosen implementation for this rule.";
|
||||
};
|
||||
};
|
||||
|
||||
# map each rule in rulesets to the generated option hierarchy
|
||||
ruleOptions =
|
||||
moduleArgs: moduleArgs.lib.mapAttrs (ruleId: rule: makeRuleOption moduleArgs ruleId rule) rulesets;
|
||||
ruleEnabled = config: ruleId: config.security.anssi.rules.${ruleId}.enable;
|
||||
enabledRuleConfigs =
|
||||
moduleArgs:
|
||||
moduleArgs.lib.mapAttrsToList (
|
||||
ruleId: rule:
|
||||
let
|
||||
complexConfigTerm =
|
||||
cfg:
|
||||
moduleArgs.lib.mkMerge (
|
||||
moduleArgs.lib.mapAttrsToList (
|
||||
implName: _:
|
||||
moduleArgs.lib.mkIf (moduleArgs.config.security.anssi.rules.${ruleId}.implementation == implName) (
|
||||
rule.implementations.${implName}.config cfg
|
||||
)
|
||||
) rule.implementations
|
||||
);
|
||||
simpleConfigFn = rule.config;
|
||||
configFn = if rule ? implementations then complexConfigTerm else simpleConfigFn;
|
||||
in
|
||||
moduleArgs.lib.mkIf (ruleEnabled moduleArgs.config ruleId) (configFn moduleArgs)
|
||||
) rulesets;
|
||||
in
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}@moduleArgs:
|
||||
{
|
||||
options.security.anssi.rules = ruleOptions moduleArgs;
|
||||
config = lib.mkMerge (enabledRuleConfigs moduleArgs);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
# SPDX-FileCopyrightText: 2026 Ryan Lahfa <ryan.lahfa@numerique.gouv.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
let
|
||||
mkSysctlChecker =
|
||||
lib: attrs:
|
||||
let
|
||||
# Normalize whitespace (tabs to spaces, collapse runs, trim edges)
|
||||
# so that sysctl output like "32768\t65535" matches Nix value "32768 65535".
|
||||
normalizeWhitespace = "tr -s '[:space:]' ' ' | sed 's/^ //;s/ $//' ";
|
||||
mkCheckSingularSysctl = attr: expectedValue: ''
|
||||
# Check for sysctl '${attr}'
|
||||
actual_value=$(sysctl -n "${attr}" | ${normalizeWhitespace}) || {
|
||||
echo "Check failed for ${attr}: unable to read sysctl value"
|
||||
exit 1
|
||||
}
|
||||
expected_value=$(echo "${toString expectedValue}" | ${normalizeWhitespace})
|
||||
if [[ "$actual_value" != "$expected_value" ]]; then
|
||||
echo "Check failed for ${attr}: expected $expected_value, got $actual_value"
|
||||
exit 1
|
||||
else
|
||||
echo "Check passed for ${attr}"
|
||||
fi
|
||||
'';
|
||||
in
|
||||
''
|
||||
${lib.concatStringsSep "\n" (
|
||||
map (name: mkCheckSingularSysctl name attrs.${name}) (lib.attrNames attrs)
|
||||
)}
|
||||
'';
|
||||
in
|
||||
{
|
||||
R8 = {
|
||||
name = "R8_MemoryBootOptions";
|
||||
anssiRef = "R8 – Paramétrer les options de configuration de la mémoire";
|
||||
description = "Set memory options at boot";
|
||||
severity = "intermediary";
|
||||
category = "base";
|
||||
|
||||
config = _: {
|
||||
boot.kernelParams = [
|
||||
"l1tf=full,force" # Enable full mitigation for L1 Terminal Fault (L1TF), including disabling SMT
|
||||
"page_poison=on" # Enable poisoning of freed pages to detect leaks
|
||||
"pti=on" # Force use of Page Table Isolation (PTI) for Meltdown vulnerability
|
||||
"slab_nomerge=yes" # Disable slab cache merging to complicate heap overflow attacks
|
||||
"slub_debug=FZP" # Enable slab cache debugging with specific tests
|
||||
"spec_store_bypass_disable=seccomp" # Use seccomp mitigation for Spectre v4
|
||||
"spectre_v2=on" # Force Spectre v2 mitigation
|
||||
"mds=full,nosmt" # Enable full mitigation for MDS and disable SMT (requires microcode update)
|
||||
"mce=0" # Force kernel panic on uncorrected Machine Check exceptions
|
||||
"page_alloc.shuffle=1" # Enable page allocator randomization
|
||||
"rng_core.default_quality=500" # Set HWRNG quality for TPM-based CSPRNG initialization
|
||||
];
|
||||
};
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R8" ''
|
||||
# The kernel boot parameters we expect to check
|
||||
EXPECTED_PARAMS=(
|
||||
"l1tf=full,force"
|
||||
"page_poison=on"
|
||||
"pti=on"
|
||||
"slab_nomerge=yes"
|
||||
"slub_debug=FZP"
|
||||
"spec_store_bypass_disable=seccomp"
|
||||
"spectre_v2=on"
|
||||
"mds=full,nosmt"
|
||||
"mce=0"
|
||||
"page_alloc.shuffle=1"
|
||||
"rng_core.default_quality=500"
|
||||
)
|
||||
|
||||
# Get the current kernel parameters from /proc/cmdline
|
||||
ACTUAL_PARAMS=$(cat /proc/cmdline)
|
||||
|
||||
# Check if each expected parameter is in the actual kernel parameters
|
||||
for param in "''${EXPECTED_PARAMS[@]}"; do
|
||||
if echo "$ACTUAL_PARAMS" | grep -q "$param"; then
|
||||
echo "Kernel parameter '$param' is correctly set."
|
||||
else
|
||||
echo "WARNING: Kernel parameter '$param' is NOT set."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Additionally, check specific parameters from /proc/cpuinfo (for cpu-related parameters)
|
||||
echo "Checking CPU-specific parameters..."
|
||||
|
||||
# Check if SMT (Hyper-Threading) is disabled, as required by "mds=full,nosmt"
|
||||
if grep -q "nosmt" <<< "$ACTUAL_PARAMS"; then
|
||||
if grep -q "siblings" /proc/cpuinfo && ! grep -q "HT" /proc/cpuinfo; then
|
||||
echo "SMT is disabled (as expected for mds=full,nosmt)."
|
||||
else
|
||||
echo "WARNING: SMT is still enabled, but 'mds=full,nosmt' was passed."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if Page Table Isolation (PTI) is enabled (from /proc/cpuinfo)
|
||||
if grep -q "pti" <<< "$ACTUAL_PARAMS"; then
|
||||
if grep -q "pti" /proc/cpuinfo; then
|
||||
echo "PTI is enabled (as expected for pti=on)."
|
||||
else
|
||||
echo "WARNING: PTI is not enabled, despite pti=on being passed."
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
'';
|
||||
};
|
||||
|
||||
R9 =
|
||||
let
|
||||
sysctls = {
|
||||
# Restrict access to dmesg buffer
|
||||
"kernel.dmesg_restrict" = "1";
|
||||
|
||||
# Hide kernel addresses in /proc and other interfaces, even for privileged users
|
||||
"kernel.kptr_restrict" = "2";
|
||||
|
||||
# Set process ID maximum
|
||||
"kernel.pid_max" = "1048576"; # 2^20
|
||||
|
||||
# Restrict perf subsystem usage
|
||||
"kernel.perf_cpu_time_max_percent" = "1";
|
||||
"kernel.perf_event_max_sample_rate" = "1";
|
||||
|
||||
# Restrict access to perf_event_open system call
|
||||
"kernel.perf_event_paranoid" = "2";
|
||||
|
||||
# Enable Address Space Layout Randomization (ASLR)
|
||||
"kernel.randomize_va_space" = "2";
|
||||
|
||||
# Disable Magic SysRq key (prevents magical system requests)
|
||||
"kernel.sysrq" = "0";
|
||||
|
||||
# Restrict use of BPF (Berkeley Packet Filter) to privileged users
|
||||
"kernel.unprivileged_bpf_disabled" = "1";
|
||||
|
||||
# Kernel panic on unexpected kernel behavior (e.g., oops)
|
||||
"kernel.panic_on_oops" = "1";
|
||||
};
|
||||
in
|
||||
{
|
||||
name = "R9_KernelOptions";
|
||||
anssiRef = "R9 – Paramétrer les options de configuration du noyau";
|
||||
description = "Tune kernel configuration options";
|
||||
severity = "intermediary";
|
||||
category = "base";
|
||||
|
||||
config = _: { boot.kernel.sysctl = sysctls; };
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R9" ''
|
||||
${mkSysctlChecker pkgs.lib sysctls}
|
||||
'';
|
||||
};
|
||||
|
||||
R10 = {
|
||||
name = "R10_DisableKernelModuleLoading";
|
||||
anssiRef = "R10 – Désactiver le chargement des modules noyau";
|
||||
description = "Disable loading of kernel modules";
|
||||
severity = "reinforced";
|
||||
category = "base";
|
||||
tags = [ "disable-kernel-module-loading" ];
|
||||
|
||||
config = _: { boot.kernel.sysctl."kernel.modules_disabled" = "1"; };
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R10" ''
|
||||
${mkSysctlChecker pkgs.lib { "kernel.modules_disabled" = 1; }}
|
||||
'';
|
||||
};
|
||||
|
||||
R11 = {
|
||||
name = "R11_ConfigureYamaLSM";
|
||||
anssiRef = "R11 – Activer et configurer le LSM Yama";
|
||||
description = "Enable and configure the Yama Linux Security Module";
|
||||
severity = "intermediary";
|
||||
category = "base";
|
||||
|
||||
config = _: { boot.kernel.sysctl."kernel.yama.ptrace_scope" = "1"; };
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R11" ''
|
||||
${mkSysctlChecker pkgs.lib { "kernel.yama.ptrace_scope" = 1; }}
|
||||
'';
|
||||
};
|
||||
|
||||
R12 =
|
||||
let
|
||||
ipv4_sysctls = {
|
||||
# Harden the JIT compiler for BPF
|
||||
"net.core.bpf_jit_harden" = "2";
|
||||
|
||||
# Disable IP forwarding (no routing between interfaces)
|
||||
"net.ipv4.ip_forward" = "0";
|
||||
|
||||
# Reject packets with source address 127/8 (loopback network)
|
||||
"net.ipv4.conf.all.accept_local" = "0";
|
||||
|
||||
# Disable ICMP redirects (prevents potential traffic redirection)
|
||||
"net.ipv4.conf.all.accept_redirects" = "0";
|
||||
"net.ipv4.conf.default.accept_redirects" = "0";
|
||||
"net.ipv4.conf.all.secure_redirects" = "0";
|
||||
"net.ipv4.conf.default.secure_redirects" = "0";
|
||||
|
||||
# Disable shared media detection (useful for preventing misconfiguration)
|
||||
"net.ipv4.conf.all.shared_media" = "0";
|
||||
"net.ipv4.conf.default.shared_media" = "0";
|
||||
|
||||
# Disable kernel ARP management across interfaces (security for high-availability setups)
|
||||
"net.ipv4.conf.all.arp_filter" = "1";
|
||||
|
||||
# Respond to ARP requests only if source and destination are on the same network
|
||||
"net.ipv4.conf.all.arp_ignore" = "2";
|
||||
|
||||
# Refuse routing of packets with loopback addresses as source or destination
|
||||
"net.ipv4.conf.all.route_localnet" = "0";
|
||||
|
||||
# Drop Gratuitous ARP (prevents ARP poisoning attacks)
|
||||
"net.ipv4.conf.all.drop_gratuitous_arp" = "1";
|
||||
|
||||
# Perform Reverse Path Filtering (checks if the source is reachable via the interface it came from)
|
||||
"net.ipv4.conf.default.rp_filter" = "1";
|
||||
"net.ipv4.conf.all.rp_filter" = "1";
|
||||
|
||||
# Disable sending ICMP redirects (normal for routers but unnecessary for end hosts)
|
||||
"net.ipv4.conf.default.send_redirects" = "0";
|
||||
"net.ipv4.conf.all.send_redirects" = "0";
|
||||
|
||||
# Ignore bogus ICMP error responses (RFC 1122)
|
||||
"net.ipv4.icmp_ignore_bogus_error_responses" = "1";
|
||||
|
||||
# Increase the range for ephemeral ports (for better security)
|
||||
"net.ipv4.ip_local_port_range" = "32768 65535";
|
||||
|
||||
# Enable RFC 1337 compliance (prevents TCP timestamp attacks)
|
||||
"net.ipv4.tcp_rfc1337" = "1";
|
||||
|
||||
# Enable SYN cookies (prevents SYN flood attacks)
|
||||
"net.ipv4.tcp_syncookies" = "1";
|
||||
};
|
||||
in
|
||||
{
|
||||
name = "R12_IPv4Networking";
|
||||
anssiRef = "R12 – Paramétrer les options de configuration du réseau IPv4";
|
||||
description = "Configure IPv4 network options";
|
||||
severity = "intermediary";
|
||||
category = "base";
|
||||
|
||||
config = _: { boot.kernel.sysctl = ipv4_sysctls; };
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R12" ''
|
||||
${mkSysctlChecker pkgs.lib ipv4_sysctls}
|
||||
'';
|
||||
};
|
||||
|
||||
R13 =
|
||||
let
|
||||
sysctls = {
|
||||
# Disable IPv6 on all interfaces by default
|
||||
"net.ipv6.conf.default.disable_ipv6" = "1";
|
||||
|
||||
# Disable IPv6 on all interfaces
|
||||
"net.ipv6.conf.all.disable_ipv6" = "1";
|
||||
};
|
||||
in
|
||||
{
|
||||
name = "R13_DisableIPv6";
|
||||
anssiRef = "R13 – Désactiver le plan IPv6";
|
||||
description = "Disable IPv6";
|
||||
severity = "intermediary";
|
||||
category = "base";
|
||||
tags = [ "no-ipv6" ];
|
||||
|
||||
config = _: { boot.kernel.sysctl = sysctls; };
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R13" ''
|
||||
${mkSysctlChecker pkgs.lib sysctls}
|
||||
'';
|
||||
};
|
||||
|
||||
R14 =
|
||||
let
|
||||
sysctls = {
|
||||
# Disable coredumps for setuid executables
|
||||
"fs.suid_dumpable" = "0";
|
||||
|
||||
# Prevent opening FIFOs and regular files not owned by the user in sticky directories
|
||||
"fs.protected_fifos" = "2";
|
||||
"fs.protected_regular" = "2";
|
||||
|
||||
# Restrict the creation of symbolic links to files owned by the user
|
||||
"fs.protected_symlinks" = "1";
|
||||
|
||||
# Restrict the creation of hard links to files owned by the user
|
||||
"fs.protected_hardlinks" = "1";
|
||||
};
|
||||
in
|
||||
{
|
||||
name = "R14_FilesystemConfig";
|
||||
anssiRef = "R14 – Paramétrer les options de configuration des systèmes de fichiers";
|
||||
description = "Tune filesystem configuration options";
|
||||
severity = "intermediary";
|
||||
category = "base";
|
||||
|
||||
config = _: { boot.kernel.sysctl = sysctls; };
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R14" ''
|
||||
${mkSysctlChecker pkgs.lib sysctls}
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
# SPDX-FileCopyrightText: 2026 Ryan Lahfa <ryan.lahfa@numerique.gouv.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
{
|
||||
pkgs,
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
inherit (lib) mkEnableOption mkOption types;
|
||||
|
||||
cfg = config.security.anssi;
|
||||
rulesets = import ./ruleset.nix { inherit lib; };
|
||||
levelMapping = {
|
||||
"minimal" = 0;
|
||||
"intermediary" = 1;
|
||||
"reinforced" = 2;
|
||||
"high" = 3;
|
||||
};
|
||||
|
||||
# rules.* must follow the format used earlier:
|
||||
# {
|
||||
# name = "...";
|
||||
# category = "client" / "server" / "base";
|
||||
# severity = "minimal" / "intermediary" / "reinforced" / "high";
|
||||
# tags = [ "kernel" ... ];
|
||||
# config = f: { ... };
|
||||
# checkScript = pkgs: pkgs.writeShellScript ...
|
||||
# }
|
||||
|
||||
# Look at all rules that are not enabled.
|
||||
# Determine why they are not enabled:
|
||||
# - if there's an exception for it, use it.
|
||||
# - if it's excluded via a tag, use it.
|
||||
# - if it's excluded via architecture, use it.
|
||||
# - if it's out of the scope for the category, use it.
|
||||
# - if it's out of the scope for the level, use it.
|
||||
# - otherwise, mark it unknown.
|
||||
# Determine why a rule is not enabled.
|
||||
determineExclusionReason =
|
||||
ruleId: rule:
|
||||
if !cfg.rules.${ruleId}.enable then
|
||||
# Check if it's excluded via a clear mechanism.
|
||||
let
|
||||
tagExclusions = lib.filter (tag: lib.elem tag (rule.tags or [ ])) cfg.excludes;
|
||||
categoryExclusion = rule.category != "base" && rule.category != cfg.category;
|
||||
levelExclusion = levelMapping.${rule.severity} > levelMapping.${cfg.level};
|
||||
architectureExclusion =
|
||||
if (rule.architectures or [ ]) != [ ] then
|
||||
!lib.elem pkgs.stdenv.hostPlatform.linuxArch rule.architectures
|
||||
else
|
||||
false;
|
||||
in
|
||||
if tagExclusions != [ ] then
|
||||
{
|
||||
reason = "excluded by tag ${lib.concatStringsSep ", " tagExclusions}";
|
||||
via = "tag";
|
||||
exclusionTags = tagExclusions;
|
||||
}
|
||||
else if categoryExclusion then
|
||||
{
|
||||
reason = "out of scope for category '${cfg.category}' (${rule.category})";
|
||||
via = "category";
|
||||
requiredCategory = rule.category;
|
||||
}
|
||||
else if levelExclusion then
|
||||
{
|
||||
reason = "out of scope for level ${cfg.level} < ${rule.severity}";
|
||||
via = "level";
|
||||
requiredLevel = rule.severity;
|
||||
}
|
||||
else if architectureExclusion then
|
||||
{
|
||||
reason = "out of scope for architecture '${pkgs.stdenv.hostPlatform.linuxArch}'";
|
||||
via = "architecture";
|
||||
requiredArchitectures = rule.architectures or [ ];
|
||||
}
|
||||
else if lib.hasAttr ruleId cfg.exceptions then
|
||||
{
|
||||
reason = "excluded by explicit exception: ${cfg.exceptions.${ruleId}.rationale}";
|
||||
via = "exception";
|
||||
rationale = cfg.exceptions.${ruleId}.rationale;
|
||||
}
|
||||
else
|
||||
{
|
||||
reason = "could not understand why this rule was excluded, the user must have set the `enable = false;` forcibly via the module system bypassing the governance system. This is a non-compliant.";
|
||||
via = "unknown";
|
||||
}
|
||||
else
|
||||
null;
|
||||
|
||||
generateRuleEntry =
|
||||
ruleId: rule:
|
||||
{
|
||||
enabled = cfg.rules.${ruleId}.enable;
|
||||
anssiRef = cfg.rules.${ruleId}.anssiRef;
|
||||
knownCheckScript = rule.checkScript pkgs;
|
||||
}
|
||||
// lib.optionalAttrs (cfg.exceptions ? ruleId) {
|
||||
declaredExceptionRationale = cfg.exceptions.${ruleId}.rationale;
|
||||
}
|
||||
// lib.optionalAttrs (rule ? implementations) {
|
||||
implementation = cfg.rules.${ruleId}.implementation;
|
||||
};
|
||||
|
||||
complianceReport = {
|
||||
"org.securix.anssi-compliance.v1" = {
|
||||
guideVersion = "2.0";
|
||||
rules = lib.mapAttrsToList (ruleId: rule: generateRuleEntry ruleId rule) rulesets;
|
||||
targetLevel = cfg.level;
|
||||
hostname = config.networking.hostName;
|
||||
domain = config.networking.domain or null;
|
||||
targetSystemCategory = cfg.category;
|
||||
exclusions = lib.mapAttrs determineExclusionReason (
|
||||
lib.filterAttrs (ruleId: _: !cfg.rules.${ruleId}.enable) rulesets
|
||||
);
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
# This will generate `security.anssi.rules.RXX.{anssiRef,enable,checkScript}` options and so on.
|
||||
((import ./generator.nix).generateOptionHierarchy rulesets)
|
||||
];
|
||||
|
||||
options.security.anssi = {
|
||||
enable = mkEnableOption "ANSSI compliance for GNU/Linux systems (v2.0)";
|
||||
|
||||
level = mkOption {
|
||||
type = types.enum [
|
||||
"minimal"
|
||||
"intermediary"
|
||||
"reinforced"
|
||||
"high"
|
||||
];
|
||||
default = "minimal";
|
||||
description = "Select desired ANSSI compliance level.";
|
||||
};
|
||||
|
||||
category = mkOption {
|
||||
type = types.enum [
|
||||
"base"
|
||||
"client"
|
||||
"server"
|
||||
];
|
||||
default = "base";
|
||||
description = "Rule category to activate.";
|
||||
};
|
||||
|
||||
excludes = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "Tags to exclude (e.g. [ \"kernel\" \"no-ipv6\" ])";
|
||||
};
|
||||
|
||||
exceptions = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submodule (_: {
|
||||
options.rationale = mkOption { type = types.lines; };
|
||||
})
|
||||
);
|
||||
default = { };
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
system.build.complianceReport = complianceReport;
|
||||
system.build.complianceReportDocument = pkgs.writers.writeJSON "anssi-compliance.json" complianceReport;
|
||||
system.build.complianceCheckScript =
|
||||
let
|
||||
mkCheckSingularRule = ruleId: rule: ''
|
||||
# Check for rule ${ruleId}
|
||||
if [ -n "${rule.checkScript}" ]; then
|
||||
result=$(bash -c "${rule.checkScript}")
|
||||
results["${ruleId}"]=$result
|
||||
else
|
||||
results["${ruleId}"]="Check script not defined: cannot be checked."
|
||||
fi
|
||||
'';
|
||||
in
|
||||
pkgs.writeShellScriptBin "anssi-nixos-compliance-check" ''
|
||||
declare -A results
|
||||
|
||||
${lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList mkCheckSingularRule (
|
||||
lib.filterAttrs (_: rule: rule.enable) config.security.anssi.rules
|
||||
)
|
||||
)}
|
||||
|
||||
# Output the results in a matrix format
|
||||
echo "ANSSI Compliance Check Results:"
|
||||
for rule in "''${!results[@]}"; do
|
||||
result="''${results[$rule]}"
|
||||
if [[ "$result" =~ .*TODO.* ]]; then
|
||||
printf "\033[33m%-40s : %s\033[0m\n" "$rule" "$result"
|
||||
elif [[ "$result" =~ (fail|WARNING|DIVERGENCE|UNSET|error|not\ completed|incomplete) ]]; then
|
||||
printf "\033[31m%-40s : %s\033[0m\n" "$rule" "$result"
|
||||
else
|
||||
printf "\033[32m%-40s : %s\033[0m\n" "$rule" "$result"
|
||||
fi
|
||||
done
|
||||
'';
|
||||
|
||||
environment.systemPackages = [ config.system.build.complianceCheckScript ];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
# SPDX-FileCopyrightText: 2026 Ryan Lahfa <ryan.lahfa@numerique.gouv.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
{
|
||||
# TODO: these two next rules requires special system integration that depends on the hardware manufacturer.
|
||||
# We implement no specific check or implementation here.
|
||||
R1 = {
|
||||
name = "R1_ChooseHardware";
|
||||
anssiRef = "R1 – Choisir et configurer son matériel";
|
||||
description = "Choosing and configuring hardware";
|
||||
severity = "high";
|
||||
category = "base";
|
||||
|
||||
config = _: {
|
||||
# TODO: hardware-level config (maybe BIOS settings, firmware)
|
||||
};
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R1" ''
|
||||
echo "TODO: check hardware configuration for R1"
|
||||
exit 0
|
||||
'';
|
||||
};
|
||||
|
||||
R2 = {
|
||||
name = "R2_BIOS_UEFI";
|
||||
anssiRef = "R2 – Configurer le BIOS/UEFI";
|
||||
description = "Configure BIOS / UEFI";
|
||||
severity = "intermediary";
|
||||
category = "base";
|
||||
|
||||
config = _: {
|
||||
# TODO: set NixOS options for UEFI / BIOS security
|
||||
};
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R2" ''
|
||||
echo "TODO: check BIOS / UEFI secure configuration"
|
||||
exit 0
|
||||
'';
|
||||
};
|
||||
|
||||
R3 = {
|
||||
name = "R3_UEFI_SecureBoot";
|
||||
anssiRef = "R3 – Activer le démarrage sécurisé UEFI";
|
||||
description = "Enable UEFI Secure Boot";
|
||||
severity = "intermediary";
|
||||
category = "base";
|
||||
|
||||
config = _: {
|
||||
# TODO: NixOS bootloader secure-boot config
|
||||
};
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R3" ''
|
||||
# Check if the system is running in UEFI mode
|
||||
if [ ! -d "/sys/firmware/efi" ]; then
|
||||
echo "Secure Boot cannot be checked. System is not in UEFI mode."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check the Secure Boot status
|
||||
secure_boot_file="/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c"
|
||||
if [ -f "$secure_boot_file" ]; then
|
||||
# Extract the value of SecureBoot
|
||||
secure_boot_status=$(xxd -p -c 4 "$secure_boot_file")
|
||||
|
||||
# Check if Secure Boot is enabled
|
||||
if [[ "$secure_boot_status" =~ 01 ]]; then
|
||||
echo "Secure Boot is enabled."
|
||||
exit 0
|
||||
else
|
||||
echo "fail: Secure Boot is not enabled."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "fail: Secure Boot status file not found."
|
||||
exit 1
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
R4 = {
|
||||
name = "R4_ReplacePreloadedKeys";
|
||||
anssiRef = "R4 – Remplacer les clés préchargées";
|
||||
description = "Replace preloaded UEFI keys";
|
||||
severity = "high";
|
||||
category = "base";
|
||||
|
||||
config = _: { };
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R4" ''
|
||||
echo "TODO: check all loaded certificates"
|
||||
exit 0
|
||||
'';
|
||||
};
|
||||
|
||||
R5 = {
|
||||
name = "R5_BootloaderPassword";
|
||||
anssiRef = "R5 – Configurer un mot de passe pour le chargeur de démarrage";
|
||||
description = "Set a password on the bootloader";
|
||||
severity = "intermediary";
|
||||
category = "base";
|
||||
|
||||
implementations = {
|
||||
secureboot = {
|
||||
checkScript = { };
|
||||
depends = [ "R3" ];
|
||||
config = _: { };
|
||||
};
|
||||
grub = {
|
||||
checkScript = "";
|
||||
config = _: { };
|
||||
};
|
||||
};
|
||||
|
||||
config = _: {
|
||||
# TODO: grub or systemd-boot password
|
||||
};
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R5" ''
|
||||
echo "R5 depends on R3."
|
||||
exit 0
|
||||
'';
|
||||
};
|
||||
|
||||
R6 = {
|
||||
name = "R6_ProtectKernelCmdline";
|
||||
anssiRef = "R6 – Protéger les paramètres de ligne de commande du noyau et l’initramfs";
|
||||
description = "Protect kernel cmdline and initramfs";
|
||||
severity = "high";
|
||||
category = "base";
|
||||
|
||||
# implementations.secureboot = { checkScript = {}; depends = [ "R3" ]; };
|
||||
|
||||
config = _: {
|
||||
# TODO
|
||||
};
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R6" ''
|
||||
echo "R6 depends on R3."
|
||||
'';
|
||||
};
|
||||
|
||||
R7 = {
|
||||
name = "R7_EnableIOMMU";
|
||||
anssiRef = "R7 – Activer l’IOMMU";
|
||||
description = "Enable IOMMU";
|
||||
severity = "reinforced";
|
||||
category = "base";
|
||||
|
||||
config = _: { boot.kernelParams = [ "iommu=force" ]; };
|
||||
|
||||
checkScript =
|
||||
pkgs:
|
||||
pkgs.writeShellScript "check-R7" ''
|
||||
#!/bin/sh
|
||||
|
||||
IOMMU_DIR="/sys/class/iommu"
|
||||
|
||||
if [ -d "$IOMMU_DIR" ] && [ "$(ls -A $IOMMU_DIR)" ]; then
|
||||
echo "IOMMU is enabled. Files found: $(ls $IOMMU_DIR | xargs)"
|
||||
else
|
||||
echo "No IOMMU detected or not enabled."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# SPDX-FileCopyrightText: 2026 Ryan Lahfa <ryan.lahfa@numerique.gouv.fr>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
{ lib, ... }:
|
||||
let
|
||||
loadRules = files: lib.mergeAttrsList (map import files);
|
||||
in
|
||||
loadRules [
|
||||
./preboot.nix
|
||||
./kernel-options.nix
|
||||
# ./kernel.nix
|
||||
# ./vfs.nix
|
||||
# ./users.nix
|
||||
# ./apparmor.nix
|
||||
# ./selinux.nix
|
||||
# ./journaling.nix
|
||||
# ./runtime-minimization.nix
|
||||
# ./hardening.nix
|
||||
# ./nss-hardening.nix
|
||||
# ./integrity.nix
|
||||
# ./mta.nix
|
||||
]
|
||||
Reference in New Issue
Block a user