2026-04-16·15 min read·

GDPR Art.32 Technical and Organisational Security Measures: Developer Implementation Guide (2026)

GDPR Article 32 is the Regulation's core security mandate. It requires every data controller and every data processor to implement appropriate technical and organisational measures to ensure a level of security appropriate to the risk — but it deliberately avoids prescribing a fixed control list. This risk-based design gives engineers flexibility; it also creates compliance ambiguity that organisations routinely exploit to under-invest in security.

This guide cuts through that ambiguity. It maps the four explicit technical measures named in Art.32(1), explains the risk-proportionality test from Art.32(2), covers the state-of-the-art and cost factors, and provides a production-grade implementation pattern. If your organisation also falls under NIS2 Art.21 (critical sector operators), we cover the dual-compliance overlap at the end.


1. What Art.32 Actually Requires

Art.32(1) opens with the principle: "appropriate technical and organisational measures to ensure a level of security appropriate to the risk." It then lists four non-exhaustive examples:

(a) Pseudonymisation and Encryption

"the pseudonymisation and encryption of personal data"

Pseudonymisation (Art.4(5)): replace direct identifiers with a pseudonym — a key, token, or hash — such that re-identification requires separately held information. The pseudonymisation key must be stored separately with strict access control.

Encryption: personal data must be encrypted at rest and in transit. "Encryption" in 2026 means:

Encryption alone does not satisfy Art.32 — it is one measure among several. EDPB Guidelines 4/2019 on pseudonymisation make clear that encryption of a database column is not equivalent to pseudonymisation if the decryption key is co-located with the data.

(b) Confidentiality, Integrity, Availability, and Resilience

"the ability to ensure the ongoing confidentiality, integrity, availability and resilience of processing systems and services"

This maps directly to the classic CIA triad plus resilience:

Confidentiality: only authorised subjects access personal data. Technically: RBAC/ABAC with least-privilege, network segmentation, secrets management (no hardcoded credentials), audit logging of all data access.

Integrity: data is accurate and cannot be altered without authorisation. Technically: database-level integrity constraints, write-ahead logging, cryptographic signatures for audit trails, immutable audit logs (append-only, log integrity checks).

Availability: processing systems must remain available for lawful processing. Technically: redundancy (multi-AZ deployments), auto-scaling, circuit breakers for downstream dependencies, health checks with alerting.

Resilience: systems must continue functioning under adversarial conditions — not just hardware failure but DDoS, ransomware, configuration errors. Resilience goes beyond availability; it requires graceful degradation design (fail-safe defaults, bulkheads, chaos engineering).

(c) Restoration Capability

"the ability to restore the availability and access to personal data in a timely manner in the event of a physical or technical incident"

This is a backup and recovery mandate. "Timely" is not defined — it is proportional to risk. For healthcare or payment data, hours-to-days; for low-risk data, days-to-weeks. Key engineering requirements:

(d) Regular Testing and Evaluation

"a process for regularly testing, assessing and evaluating the effectiveness of technical and organisational measures for ensuring the security of the processing"

This is the control most routinely neglected. "Regularly" is not defined — EDPB guidance suggests at minimum annually; for high-risk processing, quarterly or after material system changes.

Testing must cover:


2. The Risk-Proportionality Test (Art.32(2))

Art.32(2) lists four risk factors that calibrate what is "appropriate":

  1. State of the art: what encryption, authentication, and security controls are standard practice in your industry today. In 2026, passwordless authentication, FIDO2/WebAuthn, and zero-trust network architecture are increasingly "state of the art" for cloud services.

  2. Implementation costs: cost is a legitimate factor — but not a licence to skip controls. Regulators have rejected "too expensive" defences where the cost of the breach (fines + remediation + notification) exceeds the cost of prevention by orders of magnitude.

  3. Nature, scope, context, and purposes of processing: special category data (Art.9) — health, biometric, political opinion, sexual orientation — requires systematically higher security than, say, a newsletter subscriber list.

  4. Risk to rights and freedoms: the likelihood and severity of harm to data subjects (discrimination, identity theft, financial loss, physical harm). High-risk processing requires correspondingly more robust technical controls.

Practical application: before selecting controls, perform a risk assessment that maps each data processing activity to risk level (low/medium/high/very high) and documents control choices with rationale. This documentation is not optional — it demonstrates compliance under Art.5(2) accountability.


3. Art.32 × Art.28: Processor Obligations

Art.32 applies equally to data processors (cloud providers, SaaS vendors, infrastructure operators processing personal data on behalf of a controller). Your Data Processing Agreement (DPA/DPA under GDPR) must:

If you are a cloud provider or SaaS vendor processing customer data: your SOC 2 Type II report or ISO 27001 certificate is not a substitute for Art.32 compliance — but it provides strong evidence. DPAs routinely accept these certifications as evidence of Art.32 conformity; treat them as the floor, not the ceiling.


4. The Art.32 × NIS2 Art.21 Overlap

For operators in NIS2-covered sectors (cloud providers, managed service providers, health operators, banks, energy companies), both Art.32 and NIS2 Art.21 apply. The overlap is substantial but not identical.

MeasureGDPR Art.32NIS2 Art.21
EncryptionNamed examplePart of Art.21(2)(h) "encryption and access control"
Incident handlingImplied by Art.32(1)(b)–(c)Explicit: Art.21(2)(b) "incident handling"
Business continuityArt.32(1)(c) restorationArt.21(2)(c) "business continuity"
Supply chain securityVia Art.28 processor requirementsExplicit: Art.21(2)(d) "supply chain security"
Access controlNot named, but impliedArt.21(2)(i) "access control policies"
MFANot namedArt.21(2)(j) "use of multi-factor authentication"
Regular testingArt.32(1)(d)Art.21(2)(e) "security in network/IS acquisition, development, maintenance"
Vulnerability handlingPart of testingArt.21(2)(e) and ENISA guidance

Key difference: NIS2 Art.21 specifies minimum measures for the organisation's network and information systems — focused on operational continuity. GDPR Art.32 focuses on personal data security and is risk-proportional to data sensitivity, not sector classification.

Dual compliance strategy: implement NIS2 Art.21 as your security baseline (it is more prescriptive), and layer GDPR Art.32 personal-data-specific controls (encryption of personal data columns, pseudonymisation, personal-data-specific access logs) on top. A single integrated security programme covering both is more efficient than parallel compliance silos.

For incident reporting under dual coverage, see the companion guide on NIS2 + GDPR Dual Reporting.


5. Python Implementation: GDPR32ComplianceChecker

"""
GDPR Article 32 Technical Security Measures Compliance Checker
Validates four Art.32(1) measures for a given processing system.
"""

import hashlib
import hmac
import os
import json
import datetime
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional


class RiskLevel(Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    VERY_HIGH = "very_high"


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


@dataclass
class Art32Measure:
    """Represents one of the four Art.32(1) measures."""
    article_ref: str
    measure_name: str
    status: ComplianceStatus
    evidence: list[str] = field(default_factory=list)
    gaps: list[str] = field(default_factory=list)
    last_tested: Optional[str] = None


@dataclass
class ProcessingSystem:
    """Describes a data processing system for Art.32 assessment."""
    system_name: str
    risk_level: RiskLevel
    processes_special_category: bool
    estimated_data_subjects: int
    
    # Art.32(1)(a) - Pseudonymisation and encryption
    encryption_at_rest: bool = False
    encryption_in_transit: bool = False
    pseudonymisation_implemented: bool = False
    key_rotation_days: Optional[int] = None
    
    # Art.32(1)(b) - CIA + Resilience
    rbac_implemented: bool = False
    mfa_enforced: bool = False
    audit_logging: bool = False
    multi_az_deployment: bool = False
    circuit_breakers: bool = False
    
    # Art.32(1)(c) - Restoration
    automated_backups: bool = False
    backup_tested_days_ago: Optional[int] = None
    geo_separated_backups: bool = False
    rto_documented: bool = False
    rpo_documented: bool = False
    
    # Art.32(1)(d) - Regular testing
    last_pentest_days_ago: Optional[int] = None
    vulnerability_scanning_in_cicd: bool = False
    access_review_days_ago: Optional[int] = None
    incident_drills_conducted: bool = False


class GDPR32ComplianceChecker:
    """
    Evaluates Art.32 compliance for a given processing system.
    Applies risk-proportional thresholds per Art.32(2).
    """
    
    # Risk-proportional thresholds (days)
    PENTEST_THRESHOLDS = {
        RiskLevel.LOW: 365,
        RiskLevel.MEDIUM: 180,
        RiskLevel.HIGH: 90,
        RiskLevel.VERY_HIGH: 90,
    }
    
    ACCESS_REVIEW_THRESHOLDS = {
        RiskLevel.LOW: 365,
        RiskLevel.MEDIUM: 180,
        RiskLevel.HIGH: 90,
        RiskLevel.VERY_HIGH: 60,
    }
    
    BACKUP_TEST_THRESHOLDS = {
        RiskLevel.LOW: 365,
        RiskLevel.MEDIUM: 180,
        RiskLevel.HIGH: 90,
        RiskLevel.VERY_HIGH: 30,
    }
    
    def assess(self, system: ProcessingSystem) -> dict:
        measures = []
        
        # Art.32(1)(a): Pseudonymisation and encryption
        enc_measure = self._assess_encryption(system)
        measures.append(enc_measure)
        
        # Art.32(1)(b): CIA + Resilience
        cia_measure = self._assess_cia_resilience(system)
        measures.append(cia_measure)
        
        # Art.32(1)(c): Restoration capability
        restore_measure = self._assess_restoration(system)
        measures.append(restore_measure)
        
        # Art.32(1)(d): Regular testing
        testing_measure = self._assess_testing(system)
        measures.append(testing_measure)
        
        overall = self._calculate_overall(measures, system.risk_level)
        
        return {
            "system": system.system_name,
            "risk_level": system.risk_level.value,
            "assessment_date": datetime.date.today().isoformat(),
            "overall_status": overall.value,
            "measures": [
                {
                    "ref": m.article_ref,
                    "name": m.measure_name,
                    "status": m.status.value,
                    "evidence": m.evidence,
                    "gaps": m.gaps,
                }
                for m in measures
            ],
            "priority_gaps": self._priority_gaps(measures, system.risk_level),
        }
    
    def _assess_encryption(self, s: ProcessingSystem) -> Art32Measure:
        evidence, gaps = [], []
        
        if s.encryption_at_rest:
            evidence.append("Encryption at rest: implemented")
        else:
            gaps.append("CRITICAL: No encryption at rest for personal data")
        
        if s.encryption_in_transit:
            evidence.append("Encryption in transit: TLS enforced")
        else:
            gaps.append("CRITICAL: Personal data transmitted without encryption")
        
        if s.pseudonymisation_implemented:
            evidence.append("Pseudonymisation: implemented")
        elif s.processes_special_category:
            gaps.append("HIGH: Special category data without pseudonymisation")
        else:
            gaps.append("MEDIUM: Pseudonymisation not implemented (recommended for Art.32)")
        
        if s.key_rotation_days is not None:
            if s.key_rotation_days <= 90:
                evidence.append(f"Key rotation: every {s.key_rotation_days} days")
            else:
                gaps.append(f"MEDIUM: Key rotation {s.key_rotation_days}d exceeds 90-day best practice")
        else:
            gaps.append("MEDIUM: No key rotation schedule documented")
        
        if not s.encryption_at_rest or not s.encryption_in_transit:
            status = ComplianceStatus.NON_COMPLIANT
        elif gaps:
            status = ComplianceStatus.PARTIAL
        else:
            status = ComplianceStatus.COMPLIANT
        
        return Art32Measure(
            article_ref="Art.32(1)(a)",
            measure_name="Pseudonymisation and Encryption",
            status=status,
            evidence=evidence,
            gaps=gaps,
        )
    
    def _assess_cia_resilience(self, s: ProcessingSystem) -> Art32Measure:
        evidence, gaps = [], []
        critical_gaps = 0
        
        if s.rbac_implemented:
            evidence.append("RBAC/ABAC: access control implemented")
        else:
            gaps.append("CRITICAL: No role-based access control for personal data")
            critical_gaps += 1
        
        if s.mfa_enforced:
            evidence.append("MFA: enforced for data access")
        elif s.risk_level in (RiskLevel.HIGH, RiskLevel.VERY_HIGH):
            gaps.append("HIGH: MFA not enforced for high-risk processing system")
        else:
            gaps.append("MEDIUM: MFA not enforced")
        
        if s.audit_logging:
            evidence.append("Audit logging: personal data access logged")
        else:
            gaps.append("HIGH: No audit log for personal data access events")
        
        if s.multi_az_deployment:
            evidence.append("Availability: multi-AZ deployment")
        else:
            gaps.append("MEDIUM: Single-AZ deployment — availability risk")
        
        if s.circuit_breakers:
            evidence.append("Resilience: circuit breakers implemented")
        else:
            gaps.append("LOW: No circuit breakers — resilience gap")
        
        if critical_gaps > 0:
            status = ComplianceStatus.NON_COMPLIANT
        elif gaps:
            status = ComplianceStatus.PARTIAL
        else:
            status = ComplianceStatus.COMPLIANT
        
        return Art32Measure(
            article_ref="Art.32(1)(b)",
            measure_name="Confidentiality, Integrity, Availability, Resilience",
            status=status,
            evidence=evidence,
            gaps=gaps,
        )
    
    def _assess_restoration(self, s: ProcessingSystem) -> Art32Measure:
        evidence, gaps = [], []
        threshold = self.BACKUP_TEST_THRESHOLDS[s.risk_level]
        
        if s.automated_backups:
            evidence.append("Backups: automated backup process in place")
        else:
            gaps.append("CRITICAL: No automated backups for personal data")
        
        if s.backup_tested_days_ago is not None:
            if s.backup_tested_days_ago <= threshold:
                evidence.append(f"Backup testing: tested {s.backup_tested_days_ago}d ago (threshold {threshold}d)")
            else:
                gaps.append(f"HIGH: Backup last tested {s.backup_tested_days_ago}d ago (threshold {threshold}d for {s.risk_level.value} risk)")
        else:
            gaps.append("HIGH: No backup restoration test ever conducted")
        
        if s.geo_separated_backups:
            evidence.append("Geo-separation: backups stored in separate region")
        else:
            gaps.append("MEDIUM: Backups not geographically separated — ransomware risk")
        
        if s.rto_documented and s.rpo_documented:
            evidence.append("RTO/RPO: documented and tested")
        else:
            gaps.append("MEDIUM: RTO/RPO not documented (required for Art.32(2) risk assessment)")
        
        critical = not s.automated_backups
        status = (
            ComplianceStatus.NON_COMPLIANT if critical
            else ComplianceStatus.PARTIAL if gaps
            else ComplianceStatus.COMPLIANT
        )
        
        return Art32Measure(
            article_ref="Art.32(1)(c)",
            measure_name="Restoration Capability",
            status=status,
            evidence=evidence,
            gaps=gaps,
        )
    
    def _assess_testing(self, s: ProcessingSystem) -> Art32Measure:
        evidence, gaps = [], []
        pentest_threshold = self.PENTEST_THRESHOLDS[s.risk_level]
        access_threshold = self.ACCESS_REVIEW_THRESHOLDS[s.risk_level]
        
        if s.last_pentest_days_ago is not None:
            if s.last_pentest_days_ago <= pentest_threshold:
                evidence.append(f"Pentest: conducted {s.last_pentest_days_ago}d ago")
            else:
                gaps.append(f"HIGH: Pentest {s.last_pentest_days_ago}d ago (threshold {pentest_threshold}d)")
        else:
            gaps.append("HIGH: No penetration test ever conducted")
        
        if s.vulnerability_scanning_in_cicd:
            evidence.append("Vulnerability scanning: integrated in CI/CD pipeline")
        else:
            gaps.append("HIGH: No automated vulnerability scanning in CI/CD")
        
        if s.access_review_days_ago is not None:
            if s.access_review_days_ago <= access_threshold:
                evidence.append(f"Access review: conducted {s.access_review_days_ago}d ago")
            else:
                gaps.append(f"MEDIUM: Access review {s.access_review_days_ago}d ago (threshold {access_threshold}d)")
        else:
            gaps.append("MEDIUM: No access review conducted")
        
        if s.incident_drills_conducted:
            evidence.append("Incident drills: response exercises conducted")
        else:
            gaps.append("LOW: No incident response drills — Art.32(1)(d) evaluation gap")
        
        status = (
            ComplianceStatus.PARTIAL if gaps
            else ComplianceStatus.COMPLIANT
        )
        
        return Art32Measure(
            article_ref="Art.32(1)(d)",
            measure_name="Regular Testing and Evaluation",
            status=status,
            evidence=evidence,
            gaps=gaps,
        )
    
    def _calculate_overall(
        self, measures: list[Art32Measure], risk: RiskLevel
    ) -> ComplianceStatus:
        statuses = {m.status for m in measures}
        if ComplianceStatus.NON_COMPLIANT in statuses:
            return ComplianceStatus.NON_COMPLIANT
        if ComplianceStatus.PARTIAL in statuses:
            return ComplianceStatus.PARTIAL
        return ComplianceStatus.COMPLIANT
    
    def _priority_gaps(
        self, measures: list[Art32Measure], risk: RiskLevel
    ) -> list[str]:
        all_gaps = []
        for m in measures:
            all_gaps.extend(m.gaps)
        critical = [g for g in all_gaps if g.startswith("CRITICAL")]
        high = [g for g in all_gaps if g.startswith("HIGH")]
        return critical + high[:3]  # Top critical + top 3 high


# Example: SaaS platform processing health data
checker = GDPR32ComplianceChecker()
health_platform = ProcessingSystem(
    system_name="HealthTrack SaaS — EU Production",
    risk_level=RiskLevel.HIGH,
    processes_special_category=True,
    estimated_data_subjects=250_000,
    encryption_at_rest=True,
    encryption_in_transit=True,
    pseudonymisation_implemented=True,
    key_rotation_days=90,
    rbac_implemented=True,
    mfa_enforced=True,
    audit_logging=True,
    multi_az_deployment=True,
    circuit_breakers=False,
    automated_backups=True,
    backup_tested_days_ago=45,
    geo_separated_backups=True,
    rto_documented=True,
    rpo_documented=True,
    last_pentest_days_ago=75,
    vulnerability_scanning_in_cicd=True,
    access_review_days_ago=60,
    incident_drills_conducted=True,
)

result = checker.assess(health_platform)
print(json.dumps(result, indent=2))

Sample output for the health platform:

{
  "system": "HealthTrack SaaS — EU Production",
  "risk_level": "high",
  "assessment_date": "2026-04-16",
  "overall_status": "partial",
  "measures": [
    {
      "ref": "Art.32(1)(a)",
      "name": "Pseudonymisation and Encryption",
      "status": "compliant",
      "evidence": ["Encryption at rest: implemented", "Encryption in transit: TLS enforced",
                   "Pseudonymisation: implemented", "Key rotation: every 90 days"],
      "gaps": []
    },
    {
      "ref": "Art.32(1)(b)",
      "name": "Confidentiality, Integrity, Availability, Resilience",
      "status": "partial",
      "evidence": ["RBAC/ABAC: access control implemented", "MFA: enforced", "Audit logging: implemented", "Availability: multi-AZ"],
      "gaps": ["LOW: No circuit breakers — resilience gap"]
    },
    ...
  ],
  "priority_gaps": []
}

6. Art.32 in the Context of a DPIA

When a processing activity requires a Data Protection Impact Assessment (DPIA) under Art.35, the Art.32 security analysis feeds directly into the DPIA's risk treatment section. The DPIA must:

  1. Describe the processing operation and its purposes (Art.35(7)(a))
  2. Assess the necessity and proportionality (Art.35(7)(b))
  3. Assess the risks to data subjects (Art.35(7)(c))
  4. Describe the measures envisaged to address those risks, including Art.32 technical measures (Art.35(7)(d))

Use the GDPR32ComplianceChecker output as the technical annex to your DPIA. Each measure's status (compliant/partial/non-compliant) maps to residual risk level, which must be acceptable before high-risk processing begins.

For combined DPIA + FRIA documentation, see the DPIA and FRIA Combined Developer Guide.


7. Supervisory Authority Guidance (2026)

Key DPA guidance interpreting Art.32 as of 2026:

EDPB Guidelines 4/2019 (pseudonymisation): distinguishes pseudonymisation from anonymisation; confirms that pseudonymised data remains personal data. Encryption of the pseudonymisation key is required to prevent re-identification.

ICO (UK): "security of personal data" guidance applies post-Brexit UK GDPR with nearly identical Art.32 text. UK ICO enforcement has consistently found that failure to encrypt personal data laptops, databases, or email attachments is an Art.32 violation.

CNIL (France): publishes minimum security requirements (recommendations 2024) including: TLS 1.2+ mandatory, database-at-rest encryption mandatory for special category data, annual penetration testing recommended.

BSI (Germany): C5 cloud security catalogue aligns with NIS2 Art.21 and serves as evidence of Art.32 compliance for cloud processors in Germany.

ENISA Guidelines on Security Measures: ENISA periodically publishes sector-specific guidelines (telecom, smart grids, health) that translate Art.32 into sector-appropriate controls. These are non-binding but demonstrate "state of the art."


8. Art.32 Violation Consequences

Art.32 violations are subject to Tier 2 fines under Art.83(4): up to €10 million or 2% of global annual turnover, whichever is higher. In practice, supervisory authorities have issued significant fines specifically for Art.32 failures:

Common Art.32 enforcement triggers:

  1. Personal data breach that reveals inadequate encryption (automatically triggers DPA investigation)
  2. Employee accessing data beyond their role (RBAC/access control failure)
  3. No data backup → inability to restore after ransomware (Art.32(1)(c))
  4. Failed penetration test report that was not acted upon

9. Art.32 Developer Checklist (25 Items)

Art.32(1)(a) — Pseudonymisation and Encryption

Art.32(1)(b) — CIA + Resilience

Art.32(1)(c) — Restoration

Art.32(1)(d) — Regular Testing


See Also