2026-04-16·15 min read·

NIS2 Art.21(2)(g): Basic Cyber Hygiene and Security Training — SaaS Developer Guide (2026)

Most data breaches exploit predictable weaknesses: reused passwords, unpatched software, employees clicking phishing links, developers committing secrets. These are not zero-day vulnerabilities — they are hygiene failures. NIS2 Directive Art.21(2)(g) addresses them directly, requiring essential and important entities to maintain basic cyber hygiene practices and regular security training across the organisation.

Art.21(2)(g) is the cultural and operational foundation of NIS2 compliance. The other nine measures — incident handling, business continuity, supply chain security, cryptography — are only effective when the people and systems running them understand basic security principles. Without (g), the technical controls in (b) through (j) operate on a compromised human layer.

NCA auditors in June 2026 will evaluate whether organisations have moved beyond ad-hoc awareness initiatives to structured, measurable, role-specific training programmes with documented coverage and test results.

This guide builds the Art.21(2)(g) framework for SaaS development teams: hygiene baselines, training programme design, phishing simulation cadence, developer-specific secure coding training, and audit-grade evidence collection.


1. Art.21(2)(g) in the Full NIS2 Context

NIS2 Art.21(2) mandates ten cybersecurity risk-management measures. Art.21(2)(g) is the seventh — and the human-layer foundation that all technical controls depend on.

The Ten Mandatory Measures

SubparagraphRequirementPrimary Owner
Art.21(2)(a)Risk analysis and information system security policies (see risk analysis guide)CISO / Management
Art.21(2)(b)Incident handling (see incident handling guide)SOC / DevSecOps
Art.21(2)(c)Business continuity, backup management, disaster recovery (see BCM guide)Ops / SRE
Art.21(2)(d)Supply chain security (see supply chain guide)Procurement / DevSecOps
Art.21(2)(e)Security in acquisition, development and maintenance (see SDL guide)Engineering
Art.21(2)(f)Policies to assess effectiveness of cybersecurity measures (see effectiveness guide)Audit / GRC
Art.21(2)(g)Basic cyber hygiene and trainingHR / Security Awareness
Art.21(2)(h)Cryptography and encryption policies (see cryptography guide)Architecture
Art.21(2)(i)HR security, access control and asset management (see IAM guide)IT / HR / Engineering
Art.21(2)(j)Multi-factor authentication and continuous authentication (see MFA guide)IT / IAM / Engineering

Art.21(2)(g) is the only measure explicitly focused on people — the behaviour, knowledge, and habits of every person who interacts with the organisation's systems.

The Exact Regulatory Text

Art.21(2)(g) requires:

"basic cyber hygiene practices and cybersecurity training"

ENISA's technical guidelines and the NIS2 Implementation Guidance (2023) expand this into four operational requirements:

  1. Defined hygiene baseline — documented minimum security practices for all staff and systems
  2. Role-specific training — content differentiated by role (general staff, developers, privileged users, management)
  3. Measurable coverage — tracking of who has completed training and when
  4. Recurrent cadence — not a one-off event but a sustained programme with annual minimum and triggered refreshes

"We did a training session last year" fails audit. Auditors ask for the training curriculum, the completion records, the phishing simulation results, and the remediation actions taken for non-completers.


2. The Cyber Hygiene Baseline

Three frameworks define the international consensus on what "basic cyber hygiene" means. Each approaches the problem from a different angle; together they form a complete baseline.

NCSC 10 Steps to Cyber Security

The UK National Cyber Security Centre's 10 Steps framework is the clearest mapping of hygiene practices to organisational control areas:

StepControl AreaNIS2 Mapping
1. Risk ManagementGovernanceArt.21(2)(a) risk analysis
2. Engagement and TrainingCultureArt.21(2)(g)
3. Asset ManagementInventoryArt.21(2)(i)
4. Architecture and ConfigurationHardeningArt.21(2)(g) baseline
5. Vulnerability ManagementPatchingArt.21(2)(g) hygiene
6. Identity and Access ManagementIAMArt.21(2)(i)(j)
7. Data SecurityEncryptionArt.21(2)(h)
8. Logging and MonitoringDetectionArt.21(2)(b)(f)
9. Incident ManagementResponseArt.21(2)(b)
10. Supply Chain SecurityThird-partyArt.21(2)(d)

For Art.21(2)(g), Steps 2, 4, and 5 are directly applicable. Steps 3, 6, and 7 are covered by other Art.21(2) measures.

Step 2 — Engagement and Training requires organisations to:

Step 4 — Architecture and Configuration defines the technical hygiene baseline:

Step 5 — Vulnerability Management sets the operational cadence:

CIS Controls IG1 — Basic Cyber Hygiene for Every Organisation

CIS Controls v8 Implementation Group 1 (IG1) defines the minimum security posture for organisations with limited security resources. IG1 comprises 56 safeguards across 18 controls. The core hygiene safeguards relevant to Art.21(2)(g):

Inventory and Control (CIS 1, 2)

Data Protection (CIS 3)

Secure Configuration (CIS 4)

Account Management (CIS 5)

Patch Management (CIS 7)

Email and Web Browser Protections (CIS 9)

Malware Defences (CIS 10)

Security Awareness and Skills Training (CIS 14)

IG1 is the minimum — organisations handling sensitive data or operating critical services should progress to IG2.

BSI IT-Grundschutz Basis-Absicherung

The German Federal Office for Information Security (BSI) IT-Grundschutz standard provides the European regulatory baseline. The Basis-Absicherung (basic safeguarding) level is directly aligned with NIS2 Art.21(2)(g) requirements for organisations beginning their security programme:

ORP.3 — Security Awareness and Training (directly Art.21(2)(g)):

ORP.2 — Personnel (hygiene baseline for HR/onboarding):

SYS.2.1 — General Desktop Systems (device hygiene):

BSI IT-Grundschutz aligns directly with NIS2 because BSI played a central role in shaping the German NIS2 transposition (NISG 2.0). German NCA auditors will use IT-Grundschutz as their reference framework.


3. Security Awareness Training Programme Design

A compliant Art.21(2)(g) training programme has four design dimensions: audience segmentation, content curriculum, delivery cadence, and measurement methodology.

Audience Segmentation

One training module cannot serve all roles. NCA auditors expect evidence that training is differentiated by the security risks associated with each role:

AudienceRisk ProfileTraining Focus
All staffPhishing, credential theft, data handlingSocial engineering, password hygiene, clean desk, incident reporting
DevelopersCode-level vulnerabilities, secrets, supply chainOWASP Top 10, secure coding, secrets management, dependency risk
Privileged users (admins, ops)Lateral movement, privilege escalation, insider threatPrivileged access hygiene, MFA for admin, change management
ManagementBusiness email compromise, strategic riskWhaling/CEO fraud, vendor fraud, risk governance responsibilities
New joinersAll baseline risks, company-specific proceduresComprehensive onboarding module covering all categories

Annual Training Curriculum

The minimum compliant training programme for a SaaS organisation:

Module 1 — Security Fundamentals (All Staff)

Module 2 — Social Engineering and Phishing (All Staff)

Module 3 — Secure Development Practices (Developers)

Module 4 — Privileged Access and Administrative Security (Admins, Ops)

Module 5 — Management and Executive Security (Management)

Triggered Training Events

Beyond the annual cadence, the following events must trigger immediate training interventions:

TriggerResponseTimeline
Failed phishing simulationTargeted remedial module + re-simulationWithin 14 days
Security incident involving human errorRoot-cause-specific refresher for affected teamWithin 7 days of incident closure
New OWASP Top 10 edition publishedDeveloper training updateWithin 60 days of publication
Significant change in threat landscapeAwareness bulletin + optional briefingWithin 5 business days of identification
New joinerComplete onboarding programmeWeek 1
Role change to privileged accessModule 4 completionBefore access is granted

4. Phishing Simulation Programme

Phishing simulations are the most effective way to measure the real-world effectiveness of security awareness training. Art.21(2)(g) compliance requires not just running simulations but demonstrating improvement over time.

Simulation Cadence

Programme MaturityFrequencyComplexity
Year 1 (establish baseline)QuarterlyGeneric credential harvest, simple lure
Year 2 (measure improvement)MonthlySpear phishing, pretexting, phone follow-up
Year 3+ (maintain and target)Monthly baseline + targeted for high-risk rolesBEC simulation, supply chain spoofing, multi-step attacks

Simulation Metrics for Audit Evidence

Track and document for NCA audit:

MetricDefinitionTarget
Click rate% of recipients who clicked the phishing link<5% mature programme
Credential submission rate% who submitted credentials after clicking<2% mature programme
Report rate% who reported the simulation via security channel>30% target
Time to reportMedian time from delivery to first report<15 minutes
Repeat offender rate% who fail 3+ consecutive simulations<3%

Simulation Design Principles

Documentation for Art.21(2)(g) Audit

Maintain the following as audit evidence:

phishing_programme_record:
  period: "2025-Q4"
  simulations_run: 4
  total_recipients: 87
  average_click_rate: 6.2%
  average_report_rate: 24%
  trend: "improving"  # from 11.4% click rate Q1 2025
  remedial_training_triggered: 12
  remedial_training_completed: 12
  methodology: "GoPhish / Knowbe4 / Cofense"
  approved_by: "CISO"
  next_review: "2026-Q1"

5. Developer Security Training: OWASP Focus

Developers are the highest-risk role for Art.21(2)(g) purposes because they create the attack surface the other controls must defend. Developer-specific training requires depth that generic awareness programmes cannot provide.

OWASP Top 10 (2021 Edition) — Developer Training Curriculum

Each OWASP category maps to a concrete training module:

A01: Broken Access Control (moved to #1 in 2021)

A02: Cryptographic Failures

A03: Injection (SQL, LDAP, OS, NoSQL)

A04: Insecure Design

A05: Security Misconfiguration

A06: Vulnerable and Outdated Components

A07: Identification and Authentication Failures

A08: Software and Data Integrity Failures

A09: Security Logging and Monitoring Failures

A10: Server-Side Request Forgery (SSRF)

SANS/CWE Top 25 Supplement

Beyond OWASP, the SANS/CWE Top 25 Most Dangerous Software Weaknesses provides additional developer training focus areas:

CWEWeaknessDeveloper Training Priority
CWE-787Out-of-bounds WriteHigh (C/C++ codebases)
CWE-79Cross-site Scripting (XSS)High (all web applications)
CWE-89SQL InjectionHigh (all database access)
CWE-416Use After FreeHigh (memory-managed languages)
CWE-78OS Command InjectionCritical (any shell execution)
CWE-20Improper Input ValidationHigh (all API endpoints)
CWE-125Out-of-bounds ReadMedium
CWE-22Path TraversalHigh (file access operations)
CWE-352CSRFHigh (state-changing web operations)
CWE-434Unrestricted File UploadHigh (file handling features)

6. Password Policy Framework (NIST SP 800-63B Aligned)

Password policy is explicitly named in Art.21(2)(g) requirements. Modern password policy guidance (NIST SP 800-63B, 2024 revision) has shifted significantly from earlier complexity requirements.

What NIST SP 800-63B Requires

Do:

Don't:

Password Policy Template

# Password Policy — [Organisation Name]
Version: 1.0 | Review: Annual | Owner: Security Team

## Scope
All user accounts accessing [Organisation] systems, services, and applications.

## Requirements

### Length and Complexity
- Minimum: 15 characters
- Maximum: 128 characters (longer is permitted)
- Complexity: No mandatory character class rules
- Passphrase encouraged: four random words ≥ 20 characters

### Forbidden Passwords
Passwords are checked against:
- HIBP Have I Been Pwned database (k-anonymity API)
- Organisation-specific dictionary (company name, product names, sequential years)
- Common keyboard patterns (qwerty, 123456789)

### Rotation Policy
- No mandatory periodic rotation
- Immediate rotation required: suspected compromise, shared credential discovered, colleague departure
- Privileged accounts: rotation on every use (PAM-managed where possible)

### Account Lockout
- Trigger: 5 consecutive failures
- Lockout: 15-minute progressive delay (not hard lockout to avoid DoS)
- Admin override: Security team can unlock with identity verification
- Monitoring: Failed login patterns alerted to SIEM

### Multi-Factor Authentication
- Required for: all accounts (see MFA Policy)
- Permitted methods: TOTP, hardware key (FIDO2), passkey
- Prohibited: SMS OTP for privileged accounts

### Password Storage
- Algorithm: Argon2id (memory=19456, iterations=2, parallelism=1) or bcrypt (cost=12)
- Salt: Per-user, cryptographically random, minimum 128 bits
- Never: MD5, SHA-1, SHA-256 unsalted, plain text

## Password Manager
Organisation-provided password manager: [Bitwarden/1Password/etc.]
All staff required to use for work credentials. Personal use permitted.

7. Python NIS2HygieneAssessor

The following tool evaluates an organisation's Art.21(2)(g) compliance posture across five dimensions:

from dataclasses import dataclass, field
from typing import Literal
from datetime import date, timedelta
import json

ComplianceStatus = Literal["COMPLIANT", "PARTIAL", "NON_COMPLIANT", "NOT_ASSESSED"]

@dataclass
class HygieneBaseline:
    """CIS IG1 / NCSC 10 Steps technical hygiene baseline."""
    asset_inventory_maintained: bool = False
    patch_sla_defined: bool = False
    patch_sla_met_percentage: float = 0.0  # % of critical CVEs patched within SLA
    secure_config_baseline_documented: bool = False
    eol_software_present: bool = True
    anti_malware_deployed: bool = False
    email_filtering_enabled: bool = False
    dns_filtering_enabled: bool = False
    mfa_enforced_all_staff: bool = False

    def score(self) -> float:
        checks = [
            self.asset_inventory_maintained,
            self.patch_sla_defined,
            self.patch_sla_met_percentage >= 90,
            self.secure_config_baseline_documented,
            not self.eol_software_present,
            self.anti_malware_deployed,
            self.email_filtering_enabled,
            self.dns_filtering_enabled,
            self.mfa_enforced_all_staff,
        ]
        return sum(checks) / len(checks)


@dataclass
class TrainingProgramme:
    """Security awareness and training programme status."""
    all_staff_training_curriculum_exists: bool = False
    developer_training_curriculum_exists: bool = False
    privileged_user_training_exists: bool = False
    management_training_exists: bool = False
    last_training_completion_date: date | None = None
    completion_rate_percentage: float = 0.0  # % of staff completed in last 12 months
    training_pass_mark_enforced: bool = False
    new_joiner_training_exists: bool = False

    def score(self) -> float:
        days_since_training = (
            (date.today() - self.last_training_completion_date).days
            if self.last_training_completion_date else 999
        )
        checks = [
            self.all_staff_training_curriculum_exists,
            self.developer_training_curriculum_exists,
            self.privileged_user_training_exists,
            self.management_training_exists,
            days_since_training <= 365,
            self.completion_rate_percentage >= 90,
            self.training_pass_mark_enforced,
            self.new_joiner_training_exists,
        ]
        return sum(checks) / len(checks)


@dataclass
class PhishingProgramme:
    """Phishing simulation programme status."""
    programme_active: bool = False
    simulations_per_year: int = 0
    last_simulation_date: date | None = None
    last_click_rate_percentage: float = 100.0
    click_rate_trend: Literal["improving", "stable", "worsening", "not_measured"] = "not_measured"
    report_rate_percentage: float = 0.0
    remedial_training_for_failures: bool = False

    def score(self) -> float:
        days_since_sim = (
            (date.today() - self.last_simulation_date).days
            if self.last_simulation_date else 999
        )
        checks = [
            self.programme_active,
            self.simulations_per_year >= 4,
            days_since_sim <= 90,
            self.last_click_rate_percentage <= 10,
            self.click_rate_trend in ("improving", "stable"),
            self.report_rate_percentage >= 20,
            self.remedial_training_for_failures,
        ]
        return sum(checks) / len(checks)


@dataclass
class PasswordPolicy:
    """Password policy compliance (NIST SP 800-63B aligned)."""
    minimum_length: int = 8
    breached_password_check_enabled: bool = False
    mandatory_rotation_disabled: bool = False
    complexity_rules_disabled: bool = False
    password_manager_provided: bool = False
    account_lockout_configured: bool = False
    argon2_or_bcrypt_hashing: bool = False

    def score(self) -> float:
        checks = [
            self.minimum_length >= 15,
            self.breached_password_check_enabled,
            self.mandatory_rotation_disabled,  # True = compliant (rotation not mandatory)
            self.complexity_rules_disabled,    # True = compliant (no complexity theatre)
            self.password_manager_provided,
            self.account_lockout_configured,
            self.argon2_or_bcrypt_hashing,
        ]
        return sum(checks) / len(checks)


@dataclass
class NIS2HygieneAssessor:
    hygiene: HygieneBaseline = field(default_factory=HygieneBaseline)
    training: TrainingProgramme = field(default_factory=TrainingProgramme)
    phishing: PhishingProgramme = field(default_factory=PhishingProgramme)
    password: PasswordPolicy = field(default_factory=PasswordPolicy)

    WEIGHTS = {
        "hygiene": 0.35,
        "training": 0.30,
        "phishing": 0.20,
        "password": 0.15,
    }

    def overall_score(self) -> float:
        return (
            self.hygiene.score() * self.WEIGHTS["hygiene"]
            + self.training.score() * self.WEIGHTS["training"]
            + self.phishing.score() * self.WEIGHTS["phishing"]
            + self.password.score() * self.WEIGHTS["password"]
        )

    def compliance_status(self) -> ComplianceStatus:
        score = self.overall_score()
        if score >= 0.85:
            return "COMPLIANT"
        elif score >= 0.60:
            return "PARTIAL"
        else:
            return "NON_COMPLIANT"

    def gaps(self) -> list[dict]:
        findings = []

        if not self.hygiene.asset_inventory_maintained:
            findings.append({"severity": "HIGH", "area": "Hygiene", "gap": "No asset inventory maintained (CIS 1.1)", "remediation": "Implement automated asset discovery and maintain CMDB"})
        if self.hygiene.eol_software_present:
            findings.append({"severity": "CRITICAL", "area": "Hygiene", "gap": "EOL software present in environment", "remediation": "Identify all EOL components, plan upgrade or compensating controls"})
        if self.hygiene.patch_sla_met_percentage < 90:
            findings.append({"severity": "HIGH", "area": "Hygiene", "gap": f"Patch SLA compliance {self.hygiene.patch_sla_met_percentage:.0f}% (target ≥90%)", "remediation": "Review patch management process, identify blockers"})
        if not self.hygiene.mfa_enforced_all_staff:
            findings.append({"severity": "CRITICAL", "area": "Hygiene", "gap": "MFA not enforced for all staff", "remediation": "Enforce MFA via IdP policy — no exceptions for standard accounts"})
        if self.training.completion_rate_percentage < 90:
            findings.append({"severity": "HIGH", "area": "Training", "gap": f"Training completion {self.training.completion_rate_percentage:.0f}% (target ≥90%)", "remediation": "Escalate non-completers to line managers, consider mandatory completion policy"})
        if not self.training.developer_training_curriculum_exists:
            findings.append({"severity": "HIGH", "area": "Training", "gap": "No developer-specific security training curriculum", "remediation": "Develop OWASP Top 10 training module for engineering teams"})
        if not self.phishing.programme_active:
            findings.append({"severity": "HIGH", "area": "Phishing", "gap": "No phishing simulation programme active", "remediation": "Implement quarterly phishing simulations (GoPhish, KnowBe4, or Cofense)"})
        if self.phishing.last_click_rate_percentage > 10:
            findings.append({"severity": "MEDIUM", "area": "Phishing", "gap": f"Click rate {self.phishing.last_click_rate_percentage:.1f}% exceeds 10% target", "remediation": "Increase simulation frequency, targeted training for repeat clickers"})
        if self.password.minimum_length < 15:
            findings.append({"severity": "MEDIUM", "area": "Password", "gap": f"Minimum password length {self.password.minimum_length} chars (NIST SP 800-63B: ≥15)", "remediation": "Update password policy to require minimum 15 characters"})
        if not self.password.breached_password_check_enabled:
            findings.append({"severity": "MEDIUM", "area": "Password", "gap": "No breached password check on account creation/change", "remediation": "Integrate HIBP k-anonymity API into authentication flow"})
        if not self.password.argon2_or_bcrypt_hashing:
            findings.append({"severity": "CRITICAL", "area": "Password", "gap": "Passwords not hashed with Argon2id or bcrypt", "remediation": "Migrate to Argon2id (memory=19456, iterations=2) or bcrypt (cost≥12)"})

        return sorted(findings, key=lambda f: {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}[f["severity"]])

    def nca_audit_report(self) -> str:
        status = self.compliance_status()
        score = self.overall_score()
        gaps = self.gaps()
        critical_gaps = [g for g in gaps if g["severity"] == "CRITICAL"]

        report = f"""
=== NIS2 Art.21(2)(g) Compliance Assessment ===
Date: {date.today()}
Overall Score: {score:.0%} → {status}

Dimension Scores:
  Hygiene Baseline (35%): {self.hygiene.score():.0%}
  Training Programme (30%): {self.training.score():.0%}
  Phishing Programme (20%): {self.phishing.score():.0%}
  Password Policy (15%): {self.password.score():.0%}

Critical Gaps ({len(critical_gaps)} items):
"""
        for gap in critical_gaps:
            report += f"  ❌ [{gap['area']}] {gap['gap']}\n"
            report += f"     → {gap['remediation']}\n"

        if not critical_gaps:
            report += "  ✅ No critical gaps identified\n"

        report += f"""
All Gaps ({len(gaps)} items): see full gap list in JSON export
NCA Audit Readiness: {"READY" if status == "COMPLIANT" and not critical_gaps else "ACTION REQUIRED"}
"""
        return report


# Example assessment — typical SaaS organisation starting NIS2 compliance
if __name__ == "__main__":
    assessor = NIS2HygieneAssessor(
        hygiene=HygieneBaseline(
            asset_inventory_maintained=True,
            patch_sla_defined=True,
            patch_sla_met_percentage=78.0,
            secure_config_baseline_documented=False,
            eol_software_present=False,
            anti_malware_deployed=True,
            email_filtering_enabled=True,
            dns_filtering_enabled=False,
            mfa_enforced_all_staff=True,
        ),
        training=TrainingProgramme(
            all_staff_training_curriculum_exists=True,
            developer_training_curriculum_exists=True,
            privileged_user_training_exists=False,
            management_training_exists=False,
            last_training_completion_date=date(2025, 11, 15),
            completion_rate_percentage=84.0,
            training_pass_mark_enforced=True,
            new_joiner_training_exists=True,
        ),
        phishing=PhishingProgramme(
            programme_active=True,
            simulations_per_year=4,
            last_simulation_date=date(2026, 3, 1),
            last_click_rate_percentage=7.2,
            click_rate_trend="improving",
            report_rate_percentage=22.0,
            remedial_training_for_failures=True,
        ),
        password=PasswordPolicy(
            minimum_length=12,
            breached_password_check_enabled=False,
            mandatory_rotation_disabled=True,
            complexity_rules_disabled=True,
            password_manager_provided=True,
            account_lockout_configured=True,
            argon2_or_bcrypt_hashing=True,
        ),
    )

    print(assessor.nca_audit_report())
    print(json.dumps(assessor.gaps(), indent=2))

8. 25-Item Art.21(2)(g) Compliance Checklist

Hygiene Baseline (8 items)

Training Programme (8 items)

Phishing Simulation (4 items)

Password Policy (5 items)


9. Building Art.21(2)(g) in 12 Weeks

WeekMilestoneOwner
1–2Hygiene baseline audit: asset inventory, patch SLA compliance, EOL software scanIT / Ops
3–4Training curriculum design: all-staff and developer modulesHR / Security Team
5–6LMS setup and first training cohort (all staff)HR
7First phishing simulation — establish baseline click rateSecurity Team
8Developer training cohort — OWASP Top 10 moduleEngineering Lead
9Password policy update — migrate to NIST SP 800-63B alignmentIT / Engineering
10Privileged user and management training cohortsHR / CISO
11Second phishing simulation — measure improvement vs Week 7 baselineSecurity Team
12Evidence compilation for NCA audit: completion records, simulation reports, policy documentsCISO / GRC

10. Common Art.21(2)(g) Audit Failures

NCA auditors consistently find these gaps when assessing Art.21(2)(g) compliance:

1. Training exists but coverage is incomplete "We did training" is not the same as "90% of staff completed training." Auditors request completion records by department, role, and date. Organisations without an LMS often cannot demonstrate coverage.

2. Developer training is generic awareness content Developers are treated as "just another staff group" in many awareness programmes. Art.21(2)(g) requires training that addresses role-specific risks. An OWASP Top 10 module is not optional for engineering teams.

3. Phishing simulations lack trend data Running a single simulation in the week before the audit demonstrates awareness of the requirement, not a functioning programme. Auditors look for multi-year data showing measurement and improvement.

4. Password policy contradicts NIST guidance Organisations that mandate 90-day rotation and complexity rules are implementing 2010-era guidance that NIST explicitly discourages. Auditors will not reject this, but they will note the gap between policy and current best practice.

5. Training records are not retained Staff who completed training two years ago may have left; their records may have been deleted. NCA expects a 3-year minimum retention period for training evidence as part of the Art.21(2)(a) documentation requirements.


Completing the NIS2 Art.21(2) Series

With Art.21(2)(g), all ten mandatory NIS2 cybersecurity risk-management measures are now covered:

SubparagraphRequirementGuide
Art.21(2)(a)Risk analysis and information security policiesRisk Analysis Guide
Art.21(2)(b)Incident handlingIncident Handling Guide
Art.21(2)(c)Business continuity, backup, disaster recoveryBCM Guide
Art.21(2)(d)Supply chain securitySupply Chain Guide
Art.21(2)(e)Secure development lifecycleSDL Guide
Art.21(2)(f)Effectiveness assessmentEffectiveness Guide
Art.21(2)(g)Basic cyber hygiene and trainingThis guide
Art.21(2)(h)Cryptography and encryptionCryptography Guide
Art.21(2)(i)HR security, access control, asset managementIAM Guide
Art.21(2)(j)Multi-factor authenticationMFA Guide

NIS2 Art.21(2) compliance requires all ten measures. This series provides the implementation guidance for each. Start with the Risk Analysis guide as the foundation — it creates the risk register that all other measures must address.

For SaaS organisations seeking a European hosting platform that reduces their CLOUD Act exposure under Art.21(2)(a) risk analysis requirements, sota.io provides EU-sovereign infrastructure with no US jurisdiction.