Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae3461b085 | ||
|
|
6bcb16b8ab | ||
|
|
77b089bc45 |
+65
-1
@@ -1,7 +1,9 @@
|
|||||||
# Socle commun à toutes les machines de la flotte (repo public).
|
# 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
|
# Toutes les machines de la flotte portent le même nom : pas
|
||||||
# d'identification individuelle pour l'instant (voir README du repo
|
# 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
|
# racine). À revoir plus tard via le numéro de série DMI si un vrai
|
||||||
@@ -9,6 +11,68 @@
|
|||||||
networking.hostName = "Nixos_Fleet";
|
networking.hostName = "Nixos_Fleet";
|
||||||
networking.networkmanager.enable = true;
|
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";
|
time.timeZone = "Europe/Paris";
|
||||||
i18n.defaultLocale = "fr_FR.UTF-8";
|
i18n.defaultLocale = "fr_FR.UTF-8";
|
||||||
i18n.extraLocaleSettings = {
|
i18n.extraLocaleSettings = {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
{
|
{
|
||||||
services.xserver.enable = true;
|
services.xserver.enable = true;
|
||||||
services.xserver.displayManager.lightdm.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.xserver.desktopManager.cinnamon.enable = true;
|
||||||
services.accounts-daemon.enable = true;
|
services.accounts-daemon.enable = true;
|
||||||
|
|
||||||
@@ -14,6 +15,17 @@
|
|||||||
warpinator
|
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
|
# Réglages de confidentialité génériques (pas de SearXNG/DNS personnels
|
||||||
# comme sur le poste perso — cette config est destinée à être publique).
|
# comme sur le poste perso — cette config est destinée à être publique).
|
||||||
programs.firefox = {
|
programs.firefox = {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
+32
-1
@@ -92,6 +92,32 @@ do_install() {
|
|||||||
|
|
||||||
swapoff /mnt/.install-swapfile
|
swapoff /mnt/.install-swapfile
|
||||||
rm -f /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 ==="
|
echo "=== Installeur Nixos_Fleet ==="
|
||||||
@@ -190,6 +216,11 @@ case "$CHOICE" in
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
echo
|
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
|
sleep 10
|
||||||
reboot
|
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