2026-04-17·18 min read·

ENISA NIS2 Technical Guidelines: Implementing Article 21 Security Measures — Baseline Controls, Risk Tiers, and Compliance Checklist (2026)

Post #401 in the sota.io EU Cyber Compliance Series

NIS2 Article 21 mandates that Essential and Important Entities implement "appropriate and proportionate technical, operational, and organisational measures" to manage cybersecurity risks across 10 specific domains. But what does "appropriate and proportionate" actually mean in practice?

ENISA — the EU Agency for Cybersecurity — has answered this question with formal Technical Guidelines for the implementation of the minimum cybersecurity measures under NIS2 Article 21. These guidelines, first published in 2023 and updated in 2024, provide the most authoritative interpretation of what supervisory authorities across the EU will look for during NIS2 audits.

This guide translates those ENISA guidelines into a developer-actionable framework: what baseline controls look like per security domain, how to apply the risk-proportionate security level matrix, and how to demonstrate compliance.

What Are the ENISA NIS2 Technical Guidelines?

ENISA's Technical Guidelines for NIS2 Art.21 are a soft-law instrument — they do not have binding legal force in themselves, but they represent the EU-level consensus on minimum security standards. Key characteristics:

Authority and Status:

Scope:

Relationship to NIS2: The guidelines do not replace national transposition law. Each EU member state has transposed NIS2 into national law (deadline October 2024), and some have added sector-specific technical requirements. The ENISA guidelines represent the baseline floor — national law may require more.

The Three-Tier Security Level Matrix

The ENISA guidelines organise requirements around three security levels, applied proportionately based on entity type and risk profile:

Level 1 — Basic

Level 2 — Standard

Level 3 — Advanced

Determining Your Tier

The ENISA guidelines provide a risk-based tier determination methodology:

FactorIndicators for Higher Tier
Sector classificationAnnex I (Essential) → Standard/Advanced
Size250+ employees or €50M+ turnover → Standard minimum
CriticalitySingle point of failure for sector → Advanced
Cross-border dependencyServices in 3+ member states → Standard minimum
Attack surfaceCustomer data >500k records → Standard minimum
Prior incidentsCSIRT-reported incident in last 3 years → +1 tier

Most SaaS providers serving EU businesses will fall into Standard tier minimum.

Domain-by-Domain ENISA Baseline Controls

Domain (a) — Policies on the Analysis and Management of Cybersecurity Risks (Art.21(2)(a))

ENISA Baseline — Standard Level:

Policy Requirements:

Technical Controls:

Evidence for Auditors:


Domain (b) — Incident Handling (Art.21(2)(b))

ENISA Baseline — Standard Level:

Policy Requirements:

Technical Controls:

NIS2 Art.23 Notification Timelines (ENISA clarification):

TimelineTriggerRecipientContent
≤24 hoursIncident awarenessNational CSIRT or competent authorityEarly warning: suspected cause, affected systems, initial impact
≤72 hoursIncident awarenessNational CSIRT or competent authorityIncident notification: updated assessment, preliminary measures taken
≤1 monthIncident resolutionNational CSIRT or competent authorityFinal report: root cause, impact analysis, cross-border implications

Domain (c) — Business Continuity: Backup, Disaster Recovery, and Crisis Management (Art.21(2)(c))

ENISA Baseline — Standard Level:

RTO/RPO Matrix by System Criticality:

System CategoryRTO (Essential)RTO (Important)RPO
Customer-facing production≤4 hours≤24 hours≤1 hour
Internal operational systems≤24 hours≤72 hours≤24 hours
Development/staging≤72 hours≤1 week≤24 hours
Archives≤1 week≤2 weeks≤72 hours

Backup Requirements:

Crisis Management:


Domain (d) — Supply Chain Security (Art.21(2)(d))

The ENISA guidelines treat supply chain security as one of the highest-priority domains given its systemic risk profile.

ENISA Baseline — Standard Level:

Supplier Assessment:

Software Supply Chain (particularly relevant for SaaS/cloud providers):

Third-Party Access Controls:


Domain (e) — Security in Network and Information Systems Acquisition, Development, and Maintenance (Art.21(2)(e))

ENISA Baseline — Standard Level:

Secure Development Lifecycle (SDL):

Vulnerability Management:

Change Management:


Domain (f) — Policies and Procedures to Assess the Effectiveness of Cybersecurity Risk Management (Art.21(2)(f))

ENISA Baseline — Standard Level:

Testing Programme:

Audit and Review:

ENISA Note on Penetration Testing Standards: ENISA recommends that penetration tests follow recognised methodologies (OWASP WSTG for web, PTES or OSSTMM for infrastructure) and produce a written report with remediation guidance. The report must be retained and presented to auditors on request.


Domain (g) — Basic Cyber Hygiene Practices and Cybersecurity Training (Art.21(2)(g))

ENISA Baseline — Standard Level:

Technical Hygiene Controls:

Training Requirements:

Cyber Hygiene Metrics:


Domain (h) — Cybersecurity Policies and Procedures Regarding the Use of Cryptography and Encryption (Art.21(2)(h))

ENISA Baseline — Standard Level:

Encryption Requirements:

Certificate Management:

Advanced Cryptography (Advanced Tier):


Domain (i) — Human Resources Security, Access Control Policies, and Asset Management (Art.21(2)(i))

ENISA Baseline — Standard Level:

Human Resources Security:

Access Control:

Asset Management:


Domain (j) — Use of Multi-Factor Authentication and Secured Communication Solutions (Art.21(2)(j))

ENISA Baseline — Standard Level:

MFA Requirements (NIS2 Art.21(2)(j) is explicit):

Secure Communication:

Python: NIS2TechnicalChecker Implementation

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
import json

class SecurityLevel(Enum):
    BASIC = "basic"
    STANDARD = "standard"
    ADVANCED = "advanced"

class EntityType(Enum):
    ESSENTIAL = "essential"  # Annex I
    IMPORTANT = "important"  # Annex II

class ComplianceStatus(Enum):
    COMPLIANT = "compliant"
    PARTIAL = "partial"
    NON_COMPLIANT = "non_compliant"
    NOT_ASSESSED = "not_assessed"

@dataclass
class DomainControl:
    domain: str
    domain_letter: str  # a-j
    control_name: str
    required_level: SecurityLevel
    status: ComplianceStatus = ComplianceStatus.NOT_ASSESSED
    evidence: Optional[str] = None
    remediation: Optional[str] = None

@dataclass
class NIS2TechnicalAssessment:
    entity_name: str
    entity_type: EntityType
    security_level: SecurityLevel
    controls: list[DomainControl] = field(default_factory=list)
    assessment_date: str = ""

    def compliance_score(self) -> float:
        assessed = [c for c in self.controls if c.status != ComplianceStatus.NOT_ASSESSED]
        if not assessed:
            return 0.0
        compliant = sum(1 for c in assessed if c.status == ComplianceStatus.COMPLIANT)
        partial = sum(0.5 for c in assessed if c.status == ComplianceStatus.PARTIAL)
        return (compliant + partial) / len(assessed) * 100

    def gaps(self) -> list[DomainControl]:
        return [
            c for c in self.controls
            if c.status in (ComplianceStatus.NON_COMPLIANT, ComplianceStatus.PARTIAL)
        ]

    def to_report(self) -> dict:
        return {
            "entity": self.entity_name,
            "entity_type": self.entity_type.value,
            "security_level": self.security_level.value,
            "assessment_date": self.assessment_date,
            "compliance_score": round(self.compliance_score(), 1),
            "total_controls": len(self.controls),
            "compliant": sum(1 for c in self.controls if c.status == ComplianceStatus.COMPLIANT),
            "partial": sum(1 for c in self.controls if c.status == ComplianceStatus.PARTIAL),
            "non_compliant": sum(1 for c in self.controls if c.status == ComplianceStatus.NON_COMPLIANT),
            "gaps": [
                {
                    "domain": g.domain,
                    "control": g.control_name,
                    "status": g.status.value,
                    "remediation": g.remediation,
                }
                for g in self.gaps()
            ],
        }

def build_standard_controls(entity_type: EntityType) -> list[DomainControl]:
    controls = [
        # Domain (a) — Risk Management
        DomainControl("Risk Management", "a", "Written Information Security Policy", SecurityLevel.BASIC),
        DomainControl("Risk Management", "a", "Risk Register with annual review", SecurityLevel.STANDARD),
        DomainControl("Risk Management", "a", "Risk owner assignments documented", SecurityLevel.STANDARD),
        DomainControl("Risk Management", "a", "Management body risk approval minutes", SecurityLevel.STANDARD),
        # Domain (b) — Incident Handling
        DomainControl("Incident Handling", "b", "Incident Response Plan documented", SecurityLevel.BASIC),
        DomainControl("Incident Handling", "b", "24/72h NIS2 Art.23 notification workflow", SecurityLevel.STANDARD),
        DomainControl("Incident Handling", "b", "Centralised log aggregation ≥12 months", SecurityLevel.STANDARD),
        DomainControl("Incident Handling", "b", "Out-of-band incident communication channel", SecurityLevel.STANDARD),
        # Domain (c) — Business Continuity
        DomainControl("Business Continuity", "c", "3-2-1 backup policy implemented", SecurityLevel.BASIC),
        DomainControl("Business Continuity", "c", "RTO/RPO defined for critical systems", SecurityLevel.STANDARD),
        DomainControl("Business Continuity", "c", "Quarterly backup restoration tests", SecurityLevel.STANDARD),
        DomainControl("Business Continuity", "c", "Immutable backups for critical systems", SecurityLevel.STANDARD),
        # Domain (d) — Supply Chain
        DomainControl("Supply Chain", "d", "Security requirements in supplier contracts", SecurityLevel.STANDARD),
        DomainControl("Supply Chain", "d", "Annual Tier-1 supplier security questionnaire", SecurityLevel.STANDARD),
        DomainControl("Supply Chain", "d", "SBOM maintained for production software", SecurityLevel.STANDARD),
        DomainControl("Supply Chain", "d", "Dependency vulnerability scanning in CI/CD", SecurityLevel.STANDARD),
        # Domain (e) — Secure Development
        DomainControl("Secure Development", "e", "Threat modelling process for new features", SecurityLevel.STANDARD),
        DomainControl("Secure Development", "e", "SAST + DAST in CI/CD pipeline", SecurityLevel.STANDARD),
        DomainControl("Secure Development", "e", "Patch SLA: Critical ≤24h, High ≤7d", SecurityLevel.STANDARD),
        DomainControl("Secure Development", "e", "Vulnerability tracking workflow (not just scanner)", SecurityLevel.STANDARD),
        # Domain (f) — Effectiveness Assessment
        DomainControl("Effectiveness Assessment", "f", "Annual vulnerability assessment", SecurityLevel.STANDARD),
        DomainControl("Effectiveness Assessment", "f", "Annual pen test (biennial for Important)", SecurityLevel.STANDARD if entity_type == EntityType.ESSENTIAL else SecurityLevel.STANDARD),
        DomainControl("Effectiveness Assessment", "f", "MTTD/MTTR metrics tracked", SecurityLevel.STANDARD),
        # Domain (g) — Cyber Hygiene and Training
        DomainControl("Cyber Hygiene", "g", "Annual security awareness training all staff", SecurityLevel.BASIC),
        DomainControl("Cyber Hygiene", "g", "MFA enforced on all remote access", SecurityLevel.STANDARD),
        DomainControl("Cyber Hygiene", "g", "Email: SPF + DKIM + DMARC reject policy", SecurityLevel.STANDARD),
        DomainControl("Cyber Hygiene", "g", "Quarterly phishing simulation programme", SecurityLevel.STANDARD),
        # Domain (h) — Cryptography
        DomainControl("Cryptography", "h", "AES-256 encryption at rest for sensitive data", SecurityLevel.STANDARD),
        DomainControl("Cryptography", "h", "TLS 1.2+ enforced, deprecated protocols disabled", SecurityLevel.STANDARD),
        DomainControl("Cryptography", "h", "Certificate inventory + automated renewal", SecurityLevel.STANDARD),
        # Domain (i) — HR Security and Access Control
        DomainControl("HR / Access Control", "i", "RBAC with least privilege principle", SecurityLevel.STANDARD),
        DomainControl("HR / Access Control", "i", "Off-boarding: access revocation within 24h", SecurityLevel.STANDARD),
        DomainControl("HR / Access Control", "i", "Annual privileged access recertification", SecurityLevel.STANDARD),
        DomainControl("HR / Access Control", "i", "Data classification scheme implemented", SecurityLevel.STANDARD),
        # Domain (j) — MFA and Secure Communication
        DomainControl("MFA / Secure Comms", "j", "MFA mandatory: remote access + admin interfaces", SecurityLevel.STANDARD),
        DomainControl("MFA / Secure Comms", "j", "No SMS-OTP for privileged accounts", SecurityLevel.STANDARD),
        DomainControl("MFA / Secure Comms", "j", "Encrypted crisis communication channel", SecurityLevel.STANDARD),
    ]
    return controls

# Usage example
assessment = NIS2TechnicalAssessment(
    entity_name="Acme SaaS GmbH",
    entity_type=EntityType.ESSENTIAL,
    security_level=SecurityLevel.STANDARD,
    controls=build_standard_controls(EntityType.ESSENTIAL),
    assessment_date="2026-04-17",
)

# Mark a few controls as assessed
assessment.controls[0].status = ComplianceStatus.COMPLIANT
assessment.controls[0].evidence = "ISP v2.3 approved 2026-01-15"
assessment.controls[4].status = ComplianceStatus.PARTIAL
assessment.controls[4].remediation = "IRP exists but NIS2 Art.23 notification workflow not yet documented — assign owner by Q2 2026"
assessment.controls[24].status = ComplianceStatus.NON_COMPLIANT
assessment.controls[24].remediation = "MFA not enforced on VPN — implement FIDO2 by 2026-06-30"

report = assessment.to_report()
print(json.dumps(report, indent=2))

The ENISA Gap Analysis Methodology

The ENISA guidelines recommend a structured gap analysis process before building a remediation roadmap:

Step 1: Scope Determination Map your systems to NIS2 scope: which services are in scope, which are supporting? Scope defines what the controls need to protect.

Step 2: Tier Assignment Apply the risk factor matrix above to determine Basic/Standard/Advanced for each domain. Note: different domains may warrant different levels (e.g., Standard for most domains but Advanced for incident handling if you process payment data).

Step 3: Current State Assessment For each control in the ENISA framework, rate current status: Compliant / Partial / Non-Compliant. Gather evidence for each rating.

Step 4: Gap Prioritisation ENISA recommends prioritising gaps by:

  1. Regulatory risk first: Gaps in domains with explicit Art.23 notification obligations (domains b, d) carry highest regulatory exposure
  2. Operational risk second: Gaps that increase breach likelihood or impact
  3. Audit visibility third: Controls that auditors will specifically check

Step 5: Remediation Roadmap Map each gap to a remediation action with:

NIS2 Art.21 × ENISA Guidelines: Supervisory Authority Expectations

Based on how national competent authorities across the EU have applied NIS2 in their first wave of audits (2024–2025), the following patterns have emerged:

What auditors consistently check first:

  1. Does a written ISP exist and has it been approved by management?
  2. Was the last risk assessment conducted within 12 months?
  3. Does an Incident Response Plan exist with clear Art.23 timelines?
  4. Are MFA controls documented and enforced (not just policy, but technically enforced)?
  5. Are supplier security requirements in contracts?

Common audit findings (ENISA published in 2024 audit experience report):

What earns immediate enforcement action:

35-Item NIS2 Technical Compliance Checklist

Domain (a) — Risk Management

Domain (b) — Incident Handling

Domain (c) — Business Continuity

Domain (d) — Supply Chain Security

Domain (e) — Secure Development

Domain (f) — Effectiveness Assessment

Domain (g) — Cyber Hygiene + Training

Domain (h) — Cryptography

Domain (i) — HR / Access Control

Domain (j) — MFA and Secure Communication

Implementation Timeline Recommendation

QuarterFocusOutcome
Q2 2026Domains (a)(b)(j): Policy + IRP + MFAHighest audit exposure covered
Q3 2026Domains (g)(h)(i): Hygiene + Crypto + Access ControlTechnical baseline complete
Q4 2026Domains (c)(e): BCP + Secure DevOperational resilience
Q1 2027Domains (d)(f): Supply Chain + EffectivenessProgramme maturity
Q2 2027Internal audit + gap remediationAudit-ready state

See Also