2026-04-19·12 min read·

GDPR Art.29 Processing Under Authority: Sub-Processors, Employees & Instruction Chains — Developer Guide (2026)

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

Article 29 is one sentence long. It is also one of the most operationally consequential provisions in the GDPR for software developers:

"The processor and any person acting under the authority of the controller or of the processor who has access to personal data shall not process those data except on instructions from the controller, unless required to do so by Union or Member State law."

In practice, this sentence governs: every employee who touches production data, every contractor with database access, every sub-processor your SaaS vendor uses, and every automated job running under a service account. If someone or something can touch personal data in your system, Art.29 requires a documented instruction authorising exactly what they may do — and nothing more.


What Art.29 Actually Prohibits

The prohibition is precise: personal data may not be processed except on instructions from the controller.

This creates a closed instruction loop:

Controller
    │
    ├─▶ Processor (bound by Art.28 contract + Art.29 instructions)
    │       │
    │       ├─▶ Sub-processors (same restriction flows down)
    │       │
    │       └─▶ Processor's employees (same restriction applies)
    │
    └─▶ Controller's employees (same restriction — Art.29 says "under authority of the controller" too)

The two exceptions where processing without controller instructions is permitted:

  1. Union law requires it (e.g., a court order from an EU court)
  2. Member State law requires it (e.g., a national tax authority demanding payroll data)

Both exceptions are narrow and require documented legal basis — "our regulator asked" needs to be backed by an actual legal instrument, not a verbal request.


The Four Categories Covered by Art.29

1. Processor Employees

Your SaaS vendor's DBA who runs a maintenance query touching your customer data is acting "under the authority of the processor." Art.29 requires:

Technical implication: production database access for processor employees must be limited to what the processing agreement authorises. Unrestricted root access to a production database containing controller data violates Art.29 on its face.

2. Controller Employees

Art.29 explicitly covers persons "under the authority of the controller" too. This means your internal staff.

Every team member with production access must have a documented role that defines what personal data they may access and for what purpose. "We trust our engineers" is not an Art.29 instruction. "Engineers may access user records for the purpose of debugging production incidents, scoped to the affected user's records, for the duration of the incident" is.

3. Sub-Processors

The instruction chain does not stop at the processor. Art.28(4) requires processors to impose Art.29-equivalent obligations on every sub-processor. In practice, this means:

4. Automated Systems and Service Accounts

Service accounts, background jobs, and API tokens are also "acting under the authority of the controller or processor." A cron job that queries a user table is processing personal data. Its authorisation must trace back to a documented instruction.


The Instruction Document: What It Must Contain

Art.29 does not prescribe the format of processing instructions, but the EDPB and national DPAs have converged on a minimum structure through enforcement decisions and guidelines.

A compliant instruction set (typically embedded in or annexed to the Art.28 processor contract) should specify:

@dataclass
class ProcessingInstruction:
    """Represents a documented Art.29 processing instruction."""
    
    # What data categories may be processed
    data_categories: list[str]          # e.g. ["email", "usage_logs", "payment_status"]
    
    # For what purposes processing is permitted
    permitted_purposes: list[str]       # e.g. ["service_delivery", "billing", "support"]
    
    # Who may process (roles, not individuals — individuals change)
    authorised_roles: list[str]         # e.g. ["backend_service", "support_tier2", "dba_oncall"]
    
    # What operations are permitted
    permitted_operations: list[str]     # e.g. ["read", "write", "delete"] — NOT "*"
    
    # Retention: how long processed data may be held
    retention_period: str               # e.g. "90 days from last access"
    
    # Geographic scope: where processing may occur
    processing_locations: list[str]     # e.g. ["EU", "EEA"] — required for Art.46 purposes
    
    # What to do when the instruction cannot be followed
    conflict_escalation: str            # "Contact DPO at privacy@company.com"

A common implementation failure: instructions that describe what the processor does in general ("we host your database") but not what personal data may be accessed and for which purposes. Art.29 compliance requires purpose-binding, not just service-level description.


Technical Implementation: Enforcing the Instruction Chain

Role-Based Access Control Aligned to Processing Instructions

RBAC is the natural technical mechanism for Art.29, but standard RBAC implementations often fail the compliance requirement because they define permissions in terms of system capabilities rather than processing purposes.

from enum import Enum
from dataclasses import dataclass
from typing import Callable
import functools

class ProcessingPurpose(str, Enum):
    SERVICE_DELIVERY = "service_delivery"
    BILLING = "billing"
    SUPPORT = "support"
    ANALYTICS_AGGREGATE = "analytics_aggregate"
    LEGAL_COMPLIANCE = "legal_compliance"
    SECURITY_INCIDENT = "security_incident"

@dataclass
class DataAccessRule:
    """Maps a role to what it may access and why."""
    role: str
    permitted_purposes: list[ProcessingPurpose]
    permitted_fields: list[str]        # explicit field allowlist, not wildcard
    requires_incident_ticket: bool = False
    audit_every_access: bool = True

# Instructions as code — traceable back to the DPA annex
PROCESSING_INSTRUCTIONS: dict[str, DataAccessRule] = {
    "backend_api": DataAccessRule(
        role="backend_api",
        permitted_purposes=[ProcessingPurpose.SERVICE_DELIVERY],
        permitted_fields=["user_id", "email", "plan", "created_at"],
        audit_every_access=False,      # high-volume, sampled audit
    ),
    "support_agent": DataAccessRule(
        role="support_agent",
        permitted_purposes=[ProcessingPurpose.SUPPORT],
        permitted_fields=["user_id", "email", "plan", "ticket_history"],
        requires_incident_ticket=True,
        audit_every_access=True,
    ),
    "dba_oncall": DataAccessRule(
        role="dba_oncall",
        permitted_purposes=[ProcessingPurpose.SECURITY_INCIDENT],
        permitted_fields=["*"],        # full access — but every query is logged
        requires_incident_ticket=True,
        audit_every_access=True,
    ),
    "analytics_service": DataAccessRule(
        role="analytics_service",
        permitted_purposes=[ProcessingPurpose.ANALYTICS_AGGREGATE],
        permitted_fields=["plan", "created_at", "country_code"],  # no PII
        audit_every_access=False,
    ),
}

def enforce_processing_instruction(role: str, purpose: ProcessingPurpose):
    """Decorator: enforce Art.29 instruction before data access."""
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            rule = PROCESSING_INSTRUCTIONS.get(role)
            if rule is None:
                raise PermissionError(
                    f"Art.29 violation: role '{role}' has no processing instruction"
                )
            if purpose not in rule.permitted_purposes:
                raise PermissionError(
                    f"Art.29 violation: purpose '{purpose}' not authorised for role '{role}'"
                )
            if rule.requires_incident_ticket:
                ticket = kwargs.get("incident_ticket")
                if not ticket:
                    raise ValueError(
                        f"Art.29: role '{role}' requires incident_ticket for access"
                    )
            result = func(*args, **kwargs)
            if rule.audit_every_access:
                _log_data_access(role, purpose, func.__name__, kwargs.get("incident_ticket"))
            return result
        return wrapper
    return decorator

def _log_data_access(role: str, purpose: ProcessingPurpose, operation: str, ticket: str | None):
    """Write Art.29 audit trail — keep for minimum 12 months per EDPB guidance."""
    import logging, json, datetime
    audit_logger = logging.getLogger("art29.audit")
    audit_logger.info(json.dumps({
        "ts": datetime.datetime.utcnow().isoformat(),
        "role": role,
        "purpose": purpose.value,
        "operation": operation,
        "incident_ticket": ticket,
        "gdpr_basis": "Art.29 processing instruction"
    }))

Database-Level Instruction Enforcement

RBAC in application code alone is insufficient — it can be bypassed by direct database access. Art.29 compliance requires enforcement at the data layer too:

-- PostgreSQL Row-Level Security aligned to Art.29 instructions
-- Each role can only access what the processing instruction permits

-- Support agents: access only records with open support tickets
CREATE POLICY support_agent_policy ON users
  FOR SELECT
  TO support_agent_role
  USING (
    EXISTS (
      SELECT 1 FROM support_tickets st
      WHERE st.user_id = users.id
      AND st.status = 'open'
    )
  );

-- Analytics: no PII — only aggregate-safe fields via view
CREATE VIEW analytics_safe_users AS
  SELECT plan, created_at, country_code  -- no email, no user_id
  FROM users;

GRANT SELECT ON analytics_safe_users TO analytics_service_role;
-- analytics_service_role has NO direct access to users table

-- DBA oncall: access permitted but every query logged via audit extension
-- (pg_audit or equivalent — required for Art.29 audit trail)
ALTER ROLE dba_oncall_role SET log_statement = 'all';

The "Unless Required by Law" Exception

Art.29 allows processing without controller instructions when Union or Member State law requires it. This exception matters operationally because processors sometimes receive legal demands directly — a national tax authority, a court order, a law enforcement request.

The compliance obligations here:

@dataclass
class LegalProcessingRequest:
    """Documents when a processor processes without controller instructions per Art.29."""
    
    requesting_authority: str
    legal_basis: str               # cite the specific law: "§93 AO (German Tax Code)"
    data_requested: list[str]
    date_received: str
    date_response_required: str
    response_provided: str
    
    # Critical: notify the controller unless legally prohibited
    controller_notified: bool
    notification_prohibited: bool   # e.g., secrecy order from court
    notification_date: str | None

def handle_legal_demand(request: LegalProcessingRequest) -> None:
    """
    Process a legal demand that overrides Art.29 instruction requirement.
    Always document. Notify controller unless legally prohibited.
    """
    _log_legal_demand(request)
    
    if not request.notification_prohibited:
        notify_controller(
            subject=f"Legal demand received: {request.requesting_authority}",
            body=f"Legal basis: {request.legal_basis}. "
                 f"Data scope: {request.data_requested}. "
                 f"Responding by: {request.date_response_required}."
        )
    # Do NOT comply with demands that lack a valid legal basis
    # "National security" requests from non-EU authorities without an
    # MLAT or equivalent treaty instrument should be escalated to DPO
    assert request.legal_basis, "Art.29: legal demand must cite specific legal basis"

The CLOUD Act problem: US law enforcement can issue demands under the CLOUD Act to US-headquartered companies for data stored in the EU. These demands do NOT constitute "Union or Member State law" under Art.29. A processor receiving a CLOUD Act demand for EU personal data faces a conflict between US and EU law — this is precisely why EU data sovereignty (no US-parent processor) matters for Art.29 compliance.


Multi-Tenant SaaS: Each Tenant Is a Separate Controller

In a multi-tenant SaaS application, each customer (tenant) is typically a separate controller. Art.29 applies per-controller:

class MultiTenantProcessingGateway:
    """
    Enforces Art.29 instruction boundaries in multi-tenant SaaS.
    Critical: tenant A's instructions cannot authorise access to tenant B's data.
    """
    
    def __init__(self, tenant_id: str, role: str):
        self.tenant_id = tenant_id
        self.role = role
        self._instructions = self._load_instructions(tenant_id, role)
    
    def _load_instructions(self, tenant_id: str, role: str) -> DataAccessRule:
        """Load the specific controller's (tenant's) processing instructions."""
        tenant_dpa = load_tenant_dpa(tenant_id)
        instruction = tenant_dpa.get_instructions_for_role(role)
        if instruction is None:
            raise PermissionError(
                f"No Art.29 instruction for role '{role}' "
                f"in tenant '{tenant_id}' DPA"
            )
        return instruction
    
    def query_user_data(self, query_purpose: ProcessingPurpose, fields: list[str]):
        """All data access is scoped to the tenant's instructions."""
        # Verify purpose
        if query_purpose not in self._instructions.permitted_purposes:
            raise PermissionError(f"Purpose not authorised by {self.tenant_id} instructions")
        
        # Verify field scope
        unauthorised_fields = set(fields) - set(self._instructions.permitted_fields)
        if unauthorised_fields and "*" not in self._instructions.permitted_fields:
            raise PermissionError(
                f"Fields {unauthorised_fields} not in {self.tenant_id} processing instructions"
            )
        
        # Execute with tenant isolation
        return execute_tenant_scoped_query(self.tenant_id, fields, query_purpose)

The key principle: each tenant's DPA defines the instruction set. A generic "we process all tenant data for service delivery" instruction is insufficient if tenant A's DPA restricts certain fields or purposes.


Art.29 and Employee Training

The obligation is not purely technical. Art.29 requires that persons with personal data access understand they are bound by processing instructions. This has HR/contractual implications:

@dataclass
class EmployeeDataAccessOnboarding:
    """Documents that an employee has been instructed per Art.29."""
    
    employee_id: str
    role: str
    instruction_document_version: str    # version of the processing instructions they read
    training_completed_date: str
    confidentiality_agreement_signed: bool
    
    # Fields they confirmed they understood
    permitted_data_categories: list[str]
    permitted_purposes: list[str]
    prohibited_actions: list[str]         # explicit prohibitions — e.g., "no local copies"
    
    # What happens if they receive an instruction they believe is unlawful
    escalation_path: str                  # "Contact DPO — do not comply without DPO sign-off"

German DPA (BfDI) guidance explicitly states that employees must receive documented instructions — verbal briefings do not satisfy Art.29. The UK ICO similarly requires "clear documented instructions."


Common Art.29 Violations in SaaS

Violation 1: Undocumented Production Access

Pattern: Engineering team has shared production credentials. Anyone can query the user database.

Art.29 problem: No processing instruction authorises "engineering team" as a role. Individual engineers are acting under the controller's authority without documented instructions.

Fix: Individual credentials + RBAC + audit logging. Service accounts for automated jobs.

Violation 2: Sub-Processor Scope Creep

Pattern: Analytics vendor instrumented to receive full user objects, including PII fields not needed for analytics.

Art.29 problem: Sub-processor (analytics vendor) receives and processes data outside what your processing instructions with them permit.

Fix: Field-level filtering before sending data to sub-processors. Never send user objects wholesale — send only the fields the sub-processor's DPA covers.

Violation 3: Log Files with Personal Data

Pattern: Application logs include email addresses, user IDs, IP addresses for debugging. Logs forwarded to third-party log aggregation service.

Art.29 problem: The log aggregation vendor is processing personal data. If this isn't in your processor contract with them, and your own employees can access logs with no documented instruction, you have two Art.29 issues.

Fix: Log scrubbing before export. Correlation IDs instead of email in logs. Explicit processing instruction for log retention and access.

Violation 4: "Our Engineer Fixed It" Without Documentation

Pattern: Customer support escalation → engineer accesses production DB → fixes the issue. No ticket, no documented authorisation.

Art.29 problem: The engineer processed personal data (read user record, modified data) without a traceable instruction from the controller (their employer, for controller-side; the customer's DPA, for processor-side).

Fix: All production access requests must link to a ticket. The ticket is the documented instruction. This is a process requirement, not just a technical one.


Art.29 and the Processor-Controller Distinction

One source of Art.29 complexity: in some SaaS architectures, the same party can be controller for some data and processor for others.

Example: SaaS HR Platform

For billing data (own customers' payment info):
  → Platform is CONTROLLER → Art.29 applies to platform's own employees

For HR records managed on behalf of customers:
  → Platform is PROCESSOR → Art.29 applies to platform employees re: customer HR data
                          → AND applies to customers' own HR teams re: the platform

Art.29 instructions needed:
  (A) Platform's DPA with its cloud provider (sub-processor) 
  (B) Platform's employment contracts + access policies (controller-side, billing data)
  (C) Customer DPAs with the platform (processor contract)
  (D) Customer's internal policies for their HR admin team using the platform

The instruction chain must be complete at every layer. A gap at any point — even if all other layers are documented — creates an Art.29 exposure.


Relationship to Other GDPR Articles

Art.29 does not stand alone. It connects to:

ArticleConnection
Art.28Processor contracts must embed Art.29-compliant instructions in writing
Art.32Technical security measures must enforce the instruction boundaries
Art.30RoPA must describe the categories of persons processing (employees, contractors)
Art.33A breach of Art.29 instructions (unauthorised access) likely triggers Art.33 breach notification
Art.83(4)Art.29 violations attract fines up to €10M or 2% of global turnover

Practical Checklist: Art.29 Audit

□ Every role with personal data access has a written processing instruction
□ Instructions specify: data categories, purposes, permitted operations, retention
□ Employees have signed confidentiality agreements referencing the instructions
□ Service accounts / automated jobs have documented instructions (not just system permissions)
□ Sub-processors have DPAs that include Art.29-equivalent obligations (Art.28(4))
□ Sub-processors receive only the data fields their DPA covers
□ Audit logging is in place for privileged access (DBAs, support with production access)
□ A procedure exists for legal demands that override controller instructions
□ Multi-tenant isolation prevents cross-tenant data access at the DB layer
□ Processing instruction versions are tracked — employees re-acknowledge after changes

sota.io and Art.29 Compliance Architecture

Running on sota.io as your EU PaaS means your infrastructure processing relationship is explicitly scoped: sota.io processes only what is needed to run your containers. Our DPA precisely documents:

For your application's Art.29 compliance (your employees, your sub-processors), the pattern above gives you the starting point. The instruction chain starts with your controller role and needs to flow through every layer — including your choice of cloud infrastructure.


Summary

Art.29 establishes the instruction principle for GDPR processing relationships: personal data may only be processed when an instruction from the controller permits it. This applies to processor employees, controller employees, sub-processors, automated jobs, and service accounts. Technical enforcement through RBAC, RLS, field-level filtering, and audit logging is necessary but not sufficient — the instructions must be documented and personnel must acknowledge them. The exception for legal demands is narrow, requires documentation, and does not cover non-EU legal instruments like the US CLOUD Act.