Durcissement ANSSI (module SécurixOS) sur toutes les machines de la flotte

- vendor modules/anssi/ depuis SécurixOS (DINUM), copy-paste explicitement
  supporté par son propre README, licence MIT
- hosts/common/base.nix: security.anssi.enable, level=intermediary,
  category=client -- remplace le bloc sysctl qui n'existait que sur le
  profil oreo (nixos_users), même valeurs, rien de plus fort
- garde systemd.coredump.enable=false + apparmor (pas couvert par le
  module ANSSI, exception R45 explicite côté Sécurix)
- ip_forward=0 (R12) n'affecte pas pxe-server, qui n'importe pas ce fichier

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 17:00:17 +02:00
co-authored by Claude Sonnet 5
parent 77b089bc45
commit 6bcb16b8ab
9 changed files with 925 additions and 0 deletions
+36
View File
@@ -2,6 +2,8 @@
{ 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
@@ -17,6 +19,40 @@
# 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.
# R8 ajoute des mitigations CPU dont "mds=full,nosmt" (désactive
# l'hyperthreading) : impact perf réel sur du matériel bas de gamme,
# c'est la vraie recommandation ANSSI au niveau "intermediary", pas
# une invention -- à surveiller sur les PC les plus faibles du parc.
security.anssi = {
enable = true;
level = "intermediary";
category = "client";
};
# 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 = {
+15
View File
@@ -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.
+15
View File
@@ -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.";
};
}
+10
View File
@@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2026 Ryan Lahfa <ryan.lahfa@numerique.gouv.fr>
#
# SPDX-License-Identifier: MIT
{
imports = [
./options.nix
./base.nix
];
}
+112
View File
@@ -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);
};
}
+324
View File
@@ -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}
'';
};
}
+209
View File
@@ -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 ];
};
}
+181
View File
@@ -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
'';
};
}
+23
View File
@@ -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
]