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:
@@ -2,6 +2,8 @@
|
|||||||
{ pkgs, lib, ... }:
|
{ 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
|
||||||
@@ -17,6 +19,40 @@
|
|||||||
# firmware). mkDefault : un profil peut désactiver au cas par cas.
|
# firmware). mkDefault : un profil peut désactiver au cas par cas.
|
||||||
hardware.enableRedistributableFirmware = lib.mkDefault true;
|
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";
|
time.timeZone = "Europe/Paris";
|
||||||
i18n.defaultLocale = "fr_FR.UTF-8";
|
i18n.defaultLocale = "fr_FR.UTF-8";
|
||||||
i18n.extraLocaleSettings = {
|
i18n.extraLocaleSettings = {
|
||||||
|
|||||||
@@ -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