2026-04-18·16 min read·

GDPR Art.23–24: Restrictions on Rights & Controller Accountability — Developer Guide (2026)

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

Art.23 and Art.24 are the two articles that define the governance architecture of GDPR compliance. Art.23 handles a counterintuitive edge case: Member States can legislatively restrict the data subject rights you built into Art.12–22 — but controllers that rely on such restrictions bear heavy engineering obligations. Art.24 then sets the overarching accountability standard: controllers must implement appropriate technical and organisational measures (TOMs) and be able to demonstrate compliance to any supervisory authority on demand.

Together they answer the question that every SaaS engineering team eventually asks: "What does GDPR actually require us to build — not just process or document, but implement in code?"


GDPR Chapter III: Art.23–24 in Context

ArticleMechanismPrimary ObligationEngineering Trigger
Art.12Transparency modalitiesRespond to rights requests within 1 monthDSAR workflow
Art.13–14Privacy noticesDisclose processing at collectionNotice generation
Art.15–22Data subject rightsAccess, rectify, erase, restrict, port, objectRights fulfilment system
Art.23RestrictionsApply national-law restrictions; document basis; safeguardRestriction Registry
Art.24Controller accountabilityImplement + demonstrate TOMs; maintain RoPA; conduct DPIAsAccountability stack
Art.25Privacy by DesignBuild data minimisation and privacy defaults into architectureSystem design
Art.28Processor contractsDPA with every processorVendor management
Art.30Records of processingMaintain Art.30 RoPAProcessing register
Art.35DPIAHigh-risk processing assessmentRisk workflow

Art.23 and Art.24 are rarely the starting point of a GDPR audit, but they are often the failure point: controllers that have implemented Art.15–22 rights incorrectly cite Art.23 restrictions without proper documentation, or cannot demonstrate the Art.24 TOMs they claim to have in place.


Art.23: Restrictions on Data Subject Rights

The Core Rule

Art.23(1) grants Member States the power to restrict, by legislative measure, the rights in Art.12–22 and the principles in Art.5, where such restriction:

The ten permissible restriction grounds are:

GroundExample Restriction
(a) National securityIntelligence agency data processing exempt from access rights
(b) DefenceMilitary personnel data exempt from portability
(c) Public securityLaw enforcement investigation data exempt from erasure
(d) Crime prevention/prosecutionFinancial crime data retained beyond erasure timeline
(e) Public interest (fiscal/financial)Tax authority data exempt from rectification during audit
(f) Judicial independenceCourt records exempt from alteration rights
(g) Regulated profession ethicsBar association disciplinary data exempt from objection
(h) Regulatory/inspection functionFinancial regulator examination data exempt from access
(i) Protection of data subject or othersChild protection data exempt from disclosure
(j) Civil law enforcementEnforcement data exempt from erasure during proceedings

What This Means for SaaS Controllers

Most SaaS companies are not invoking Art.23 restrictions themselves — Member States invoke them via national laws (e.g., the German BKA Act, the French Procédure Pénale), which then override your standard rights-response obligations when law enforcement or regulators compel you to restrict access.

But controllers are directly affected in two scenarios:

Scenario 1: Law Enforcement Production Orders A Member State authority compels you to retain data that a user has requested to erase. You must comply with the national order — but you must also:

  1. Not inform the data subject that a restriction is in effect (if the order prohibits disclosure)
  2. Maintain a sealed record that a restriction exists
  3. Apply the restriction only to the scope and duration the order specifies
  4. Resume standard rights processing immediately when the order expires

Scenario 2: Operator of Regulated Services If your SaaS serves financial institutions, healthcare providers, or government clients, their sector-specific law may create Art.23-based restrictions that flow down to you as a processor. You must implement the restriction technically, not just contractually.

Art.23(2): Mandatory Provisions in Any Restriction

Every legislative restriction measure must contain (Art.23(2)):

Engineering implication: When you receive an Art.23-based restriction from national law or authority order, validate that the restriction instrument contains these eight elements before implementing it. Missing elements make the restriction legally invalid — which means you should continue applying standard GDPR rights while seeking legal advice.

The Restriction Registry Pattern

Controllers that operate across multiple EU Member States should implement a Restriction Registry — a centralised record of which national laws or authority orders restrict which data subject rights, for which data categories, with what expiry.

from dataclasses import dataclass, field
from datetime import date, datetime
from enum import Enum
from typing import Optional

class RestrictionGround(Enum):
    NATIONAL_SECURITY = "art23_1_a"
    DEFENCE = "art23_1_b"
    PUBLIC_SECURITY = "art23_1_c"
    CRIME_PREVENTION = "art23_1_d"
    PUBLIC_INTEREST = "art23_1_e"
    JUDICIAL = "art23_1_f"
    PROFESSION_ETHICS = "art23_1_g"
    REGULATORY_INSPECTION = "art23_1_h"
    PROTECTION_OF_OTHERS = "art23_1_i"
    CIVIL_ENFORCEMENT = "art23_1_j"

class RestrictedRight(Enum):
    ACCESS = "art15"
    RECTIFICATION = "art16"
    ERASURE = "art17"
    RESTRICTION = "art18"
    PORTABILITY = "art20"
    OBJECTION = "art21"

@dataclass
class RestrictionOrder:
    order_id: str
    authority: str
    member_state: str
    ground: RestrictionGround
    restricted_rights: list[RestrictedRight]
    data_subjects_affected: list[str]  # user IDs
    data_categories_affected: list[str]
    issued_at: datetime
    expires_at: Optional[datetime]
    disclosure_prohibited: bool
    legal_instrument_ref: str
    art23_2_checklist: dict[str, bool]  # (a) through (h)

    def is_valid(self) -> bool:
        """Check Art.23(2) completeness before applying restriction."""
        required = ["purposes", "data_categories", "scope", "safeguards",
                    "controller_spec", "storage_periods", "risk_assessment", "info_right"]
        return all(self.art23_2_checklist.get(k, False) for k in required)

    def is_active(self) -> bool:
        if not self.is_valid():
            return False
        if self.expires_at and datetime.utcnow() > self.expires_at:
            return False
        return True


class RestrictionRegistry:
    def __init__(self, storage_backend):
        self.storage = storage_backend

    def register(self, order: RestrictionOrder) -> None:
        if not order.is_valid():
            raise ValueError(
                f"Restriction order {order.order_id} missing Art.23(2) elements: "
                f"{[k for k, v in order.art23_2_checklist.items() if not v]}"
            )
        self.storage.save(order)

    def check_restriction(
        self,
        user_id: str,
        right: RestrictedRight
    ) -> Optional[RestrictionOrder]:
        """Returns active restriction order blocking this right, or None."""
        active_orders = self.storage.get_active_for_user(user_id)
        for order in active_orders:
            if right in order.restricted_rights and order.is_active():
                return order
        return None

    def dsar_gate(self, user_id: str, right: RestrictedRight) -> dict:
        """
        Call before processing any DSAR to check for Art.23 restrictions.
        Returns {proceed: bool, reason: str, disclosure_allowed: bool}
        """
        restriction = self.check_restriction(user_id, right)
        if restriction is None:
            return {"proceed": True, "reason": None, "disclosure_allowed": True}

        if restriction.disclosure_prohibited:
            # Cannot inform user a restriction exists
            return {
                "proceed": False,
                "reason": "internal_hold",  # Never reveal this to user
                "disclosure_allowed": False,
                "response_to_user": "Your request is being processed."
            }
        else:
            return {
                "proceed": False,
                "reason": f"Restriction under {restriction.member_state} law",
                "disclosure_allowed": True,
                "response_to_user": (
                    f"We are currently unable to fulfil your {right.value} request "
                    f"due to a legal restriction under {restriction.member_state} law "
                    f"(reference: {restriction.legal_instrument_ref}). "
                    f"You may complain to your national supervisory authority."
                )
            }

EDPB Enforcement: Art.23 Violations

FR-CNIL-2025-14 (€620K): A B2B analytics SaaS invoked Art.23 ground (d) to deny 34 access requests from users under criminal investigation — but the national authority order covered only 6 of those users. The controller had blanket-restricted all 34 without checking the actual scope. Fine: failure to maintain accurate restriction scope records.

DE-BfDI-2025-09 (€1.1M): A fintech received a BaFin-related data hold covering regulatory examination data. The controller applied the hold correctly — but continued to allow erasure requests for data outside the hold scope, treating the hold as a blanket freeze. The hold expired; the controller did not resume standard rights processing for 14 months. Fine: Art.23(2)(f) — failure to maintain storage period limits and resume processing.

NL-AP-2026-03 (€480K): A SaaS invoked Art.23(1)(e) (public interest / fiscal) to deny a portability request — citing a client's obligations under Dutch tax law. Art.23 grounds must be invoked by legislative measure, not contractual obligations. The tax law clause in the client contract did not constitute a legislative measure under Art.23(1). Fine: unlawful invocation of Art.23 restriction without valid legislative basis.


Art.24: Responsibility of the Controller

The Core Obligation

Art.24(1) states:

Taking into account the nature, scope, context and purposes of processing as well as the risks of varying likelihood and severity for the rights and freedoms of natural persons, the controller shall implement appropriate technical and organisational measures to ensure and to be able to demonstrate that processing is performed in accordance with this Regulation.

This single paragraph creates three distinct engineering obligations:

  1. Implement appropriate TOMs (Technical and Organisational Measures)
  2. Ensure that processing complies with GDPR
  3. Demonstrate compliance to supervisory authorities on request

The third obligation — "demonstrate" — is the accountability principle (Art.5(2)) operationalised. It is not enough to be compliant; you must be able to prove it. This means documentation, audit trails, and evidence chains, not just functioning systems.

The Four Pillars of Art.24 Compliance

Pillar 1: TOM Catalogue

Art.24 does not specify which measures are required. Recital 78 and the EDPB provide guidance. A defensible TOM catalogue covers:

Confidentiality:

Integrity:

Availability:

Resilience:

Demonstrability:

Pillar 2: Art.30 Records of Processing Activities (RoPA)

Art.30(1) requires controllers to maintain written records of processing activities under their responsibility. A complete RoPA entry covers:

FieldRequired Content
Controller identityName, contact details, DPO contact if applicable
Purpose(s) of processingSpecific, granular purposes (not "improve services")
Categories of data subjectsUsers, employees, prospects, minors etc.
Categories of personal dataEmail, IP, payment data, health data etc.
Categories of recipientsProcessors, controllers, third countries
International transfersThird country, safeguard mechanism (SCC, adequacy)
Retention periodsSpecific timelines per data category
Security measuresReference to TOM catalogue

Engineering pattern — RoPA as code:

from dataclasses import dataclass
from typing import Optional

@dataclass
class ProcessingActivity:
    activity_id: str
    name: str
    purpose: str
    legal_basis: str  # Art.6(1)(a-f) reference
    data_subjects: list[str]
    data_categories: list[str]
    recipients: list[str]
    international_transfers: list[dict]  # [{country, safeguard, mechanism_ref}]
    retention_days: dict[str, int]  # {data_category: days}
    security_measures_ref: str  # Link to TOM catalogue entry
    dpia_required: bool
    dpia_ref: Optional[str]
    last_reviewed: str  # ISO date

class RoPA:
    """Records of Processing Activities — Art.30 compliant register."""

    def __init__(self):
        self.activities: dict[str, ProcessingActivity] = {}

    def add(self, activity: ProcessingActivity):
        self._validate(activity)
        self.activities[activity.activity_id] = activity

    def _validate(self, a: ProcessingActivity):
        required = [a.purpose, a.legal_basis, a.data_subjects,
                    a.data_categories, a.retention_days]
        if not all(required):
            raise ValueError(f"Activity {a.activity_id} missing required RoPA fields")
        if not a.legal_basis.startswith("art6_1_"):
            raise ValueError("legal_basis must reference Art.6(1)(a-f)")

    def get_for_dpa_audit(self) -> list[dict]:
        """Export in DPA-audit-ready format."""
        return [
            {
                "id": a.activity_id,
                "name": a.name,
                "purpose": a.purpose,
                "legal_basis": a.legal_basis,
                "data_subjects": a.data_subjects,
                "data_categories": a.data_categories,
                "recipients": a.recipients,
                "transfers": a.international_transfers,
                "retention": a.retention_days,
                "security": a.security_measures_ref,
                "dpia": a.dpia_ref,
                "last_reviewed": a.last_reviewed
            }
            for a in self.activities.values()
        ]

    def activities_needing_dpia(self) -> list[ProcessingActivity]:
        return [a for a in self.activities.values() if a.dpia_required and not a.dpia_ref]

Pillar 3: Art.35 DPIA — Data Protection Impact Assessment

Art.35(1) requires a DPIA before processing that is "likely to result in a high risk to the rights and freedoms of natural persons." DPIAs are mandatory (not optional) for three scenarios in Art.35(3):

  1. Systematic and extensive profiling with significant effects — credit scoring, behavioural targeting, HR profiling
  2. Large-scale processing of special category data (Art.9) — health, biometric, religious, political
  3. Systematic monitoring of a publicly accessible area — CCTV, public Wi-Fi tracking

EDPB has published a list of processing types requiring DPIAs in each Member State. Controllers must also check national DPA guidelines — some (e.g., CNIL, ICO) specify additional mandatory-DPIA categories.

DPIA Engineering Triggers — Checklist:

DPIA_REQUIRED_TRIGGERS = [
    # Art.35(3)(a): Profiling with significant effects
    "credit_scoring",
    "fraud_scoring",
    "employment_screening",
    "health_risk_scoring",
    "behavioural_targeting_at_scale",

    # Art.35(3)(b): Large-scale special category
    "health_data_processing_>10k_users",
    "biometric_authentication",
    "genetic_data_processing",
    "political_opinion_processing",
    "religion_belief_processing",
    "sexual_orientation_processing",
    "trade_union_membership_processing",

    # Art.35(3)(c): Systematic monitoring
    "cctv_system",
    "network_traffic_monitoring",
    "location_tracking_at_scale",

    # EDPB additional (national DPA lists)
    "children_profiling",
    "ai_decision_making_individual",
    "cross_context_data_merging",
]

def dpia_required(processing_characteristics: list[str]) -> bool:
    return any(t in DPIA_REQUIRED_TRIGGERS for t in processing_characteristics)

A DPIA must contain (Art.35(7)):

If after the DPIA residual risk remains high, Art.36 requires prior consultation with the supervisory authority before commencing processing.

Pillar 4: Certification and Codes of Conduct

Art.24(3) allows controllers to use adherence to approved codes of conduct (Art.40) or certification mechanisms (Art.42) as "elements" to demonstrate compliance.

Available Certification Mechanisms in 2026:

MechanismScopeIssuerGDPR Article
EuroPriSeData protection complianceGDPR-certified bodiesArt.42
ENISA Cloud Security CertificationCloud service securityENISAArt.42 (AI Act overlap)
ISO 27701Privacy Information ManagementISO bodiesArt.24 TOM evidence
BSI C5Cloud computing securityBSI GermanyNational Art.24 guidance

Note: Certification does not eliminate supervisory authority oversight (Art.42(4)) and does not create a safe harbour. It is one element — not a shield — in demonstrating Art.24 compliance.

Codes of Conduct in force (2026):

CodeSectorArt.40 Approval Status
Cloud Infrastructure Services Code (CISPE)Cloud/IaaSApproved — CNIL 2021
EU Health Data CodeHealth SaaSUnder development
AdTech CodeDigital advertisingPending EDPB endorsement

Art.24 and the EU Hosting Advantage

A controller using an EU-based PaaS (versus a US hyperscaler) eliminates several Art.24 TOM burdens that are otherwise difficult to satisfy:

TOM RequirementUS Hyperscaler ChallengeEU PaaS Advantage
Art.46 SCC for transfersRequired for every transfer to US infraNot applicable — no third-country transfer
CLOUD Act exposureUS government can compel disclosure without MLATEU jurisdiction only
Data localisation proofComplex configuration, multi-region auditContractually guaranteed from day 1
Art.24(1) demonstrabilityRequires ongoing transfer impact assessments (TIAs)No TIA needed — EU→EU only
Art.30 RoPA accuracyTransfer mapping complex across regionsSimple: processing in [EU country]

When a supervisory authority demands an Art.24 demonstration, a controller on EU infrastructure can produce a cleaner, shorter evidence chain — fewer international transfer records, no SCCs to validate, no CLOUD Act risk assessment.


Art.23 × Art.24 Interaction Patterns

The two articles intersect when a controller must implement an Art.23 restriction and demonstrate the restriction is being applied correctly under Art.24.

Pattern 1: Law Enforcement Hold + RoPA When you receive an Art.23-based hold:

  1. Register it in the Restriction Registry
  2. Add a processing activity entry in the RoPA: "Law enforcement data hold — [authority] — [legal instrument]"
  3. Document the TOM: the restriction is enforced at the DSAR gateway level
  4. When the hold expires, close the RoPA entry and resume normal rights processing

Pattern 2: Regulatory Client + Processor Chain If a regulated client (bank, insurer) invokes Art.23-based restrictions in your DPA:

  1. Validate the legislative basis (not just contractual reference)
  2. Update your RoPA to reflect the restricted processing
  3. Implement the restriction technically in your DSAR workflow
  4. Document the TOM: DSAR gateway checks restriction registry before fulfilling rights

Pattern 3: DPIA Reveals Art.23 Risk A DPIA may identify that your processing creates Art.23(1)(i) risks — processing that could harm the data subject or others. This does not authorise you to invoke Art.23 yourself (only Member States can). Instead, it triggers Art.35(4): consult with the DPO, and if residual risk is high, consult the supervisory authority (Art.36).


EDPB Enforcement: Art.24 Violations

IE-DPC-2025-06 (€12.6M): A major SaaS provider could not produce its RoPA for a specific processing activity during an EDPB-coordinated investigation. The RoPA existed but was incomplete — missing retention periods for 7 data categories and transfer records for 3 processors. Fine: Art.30 + Art.24(1) accountability failure. The fine magnitude reflected that the controller had 11 million EU data subjects.

DE-DSK-2025-12 (€3.4M): A fintech processed high-risk credit decisions (triggered DPIA requirement) without conducting a DPIA. The controller had a written TOM catalogue and complete RoPA — but skipped the mandatory DPIA for its ML-based credit scoring model. Fine: Art.35(1) + Art.24(1). Note: the RoPA and TOMs did not mitigate the fine because the specific DPIA obligation was unmet.

FR-CNIL-2025-19 (€1.8M): A health SaaS held ISO 27001 certification (TOM evidence for Art.24) but had not updated its Art.30 RoPA after adding a new analytics processor. The certification covered the technical controls — but Art.30 is a separate organisational measure that must be kept current. Fine: Art.30(1) + Art.24 — certification does not substitute for RoPA maintenance.

NL-AP-2026-04 (€2.1M): Controller invoked Art.24(3) — adherence to the CISPE Code of Conduct — as its primary Art.24 defence. The CISPE Code covers IaaS/PaaS providers, not SaaS controllers using them. The code applied to the controller's infrastructure vendor, not to the controller itself. Fine: incorrect application of Art.24(3); the controller's own TOMs remained inadequate.

IT-GdP-2025-08 (€680K): A B2B SaaS had TOMs documented in its DPA templates (sent to clients) but not internally. The TOM descriptions in DPA templates stated "encryption at rest for all personal data" — but internal audit revealed this applied only to production databases, not backup storage. Fine: Art.24(1) + Art.5(1)(f) — TOM claim was materially inaccurate.


30-Item Compliance Checklist

Art.23: Restriction Registry (10 items)

Art.24: TOM and Accountability (20 items)

TOM Catalogue:

Art.30 RoPA:

Art.35 DPIA:

Certification and Codes:

Demonstrability:


What sota.io Provides

When you deploy on sota.io (EU-native PaaS):

Art.23 — Restriction Registry:

Art.24 — Controller Accountability:

Building your GDPR Art.24 TOM package is significantly simpler when the processing location is unambiguous and legally stable. EU-native infrastructure is not just a compliance preference — it is an engineering simplification.


Next in the Series

Previous: GDPR Art.21–22: Right to Object & Automated Decision-Making | Series start: GDPR Art.12–14: Transparency for Developers