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:
- Union law requires it (e.g., a court order from an EU court)
- 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:
- The vendor's employees must be bound by confidentiality obligations (Art.28(3)(b))
- They may only run queries/jobs documented in the processing instructions
- Accessing data outside those instructions — even accidentally — is a potential Art.29 violation
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:
- Your cloud provider (sub-processor) may only process your data as documented in their DPA
- Your analytics vendor may only receive the fields your processing instructions specify
- Your support tooling may only access ticket-relevant user fields, not full account records
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:
| Article | Connection |
|---|---|
| Art.28 | Processor contracts must embed Art.29-compliant instructions in writing |
| Art.32 | Technical security measures must enforce the instruction boundaries |
| Art.30 | RoPA must describe the categories of persons processing (employees, contractors) |
| Art.33 | A 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:
- What system-level data sota.io processes (logs, metrics, storage I/O)
- Which sota.io roles may access that data and for which purposes
- That no third-party US-headquartered sub-processor receives your application data
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.