Policy Data Handling
This document records the operator-facing privacy contracts that policy and compliance packs rely on when they reference release evidence.
It is the binding reference for:
- the PII boundary every capsule producer must respect (Sprint 20260518_051);
- the pseudonymisation pattern that lets Stella Ops retain immutable evidence while honouring GDPR Art. 17 erasure;
- the Subject Access Request endpoint shape;
- the offline
stella verify sarintegrity check.
Definition of PII (binding)
The following data classes are forbidden inside immutable capsule manifests, DSSE payloads, append-only audit rows, and any field whose bytes feed a canonicaliser used for signing:
- Display names / personal names —
firstName,lastName,displayName,fullName, anything intended to identify a natural person to a human reader. - Email addresses — any string matching
^[^@\s]+@[^@\s]+\.[^@\s]+$. - IP addresses captured during admin actions — IPv4 and IPv6, structured (
inet) or string forms. - User-Agent strings — full browser/client UA strings (they fingerprint individuals when combined with other signals).
- Free-form usernames that contain
@— covered by the existingValidateOpaqueReferenceprecedent on regulatory audit events. - Telephone numbers, postal addresses, employee IDs from external HR systems — anything correlatable to a natural person via an external directory.
- Tenant display names when the tenant maps to a single natural person (sole-trader case). Tenant UUIDs are not PII; tenant slugs are PII-by-association only when the tenant is a single natural person.
Allowed inside capsules:
- Opaque
actor_ref(UUIDv4 or KSUID, no embedded semantics). - SHA-256 hashes of any value (one-way; not reversible at GDPR-meaningful effort).
- Role identifiers (
role:operator,role:admin). - Tenant UUIDs.
- Service identifiers (
service:scanner-web). - Approver role references like
dedicated-product-security-primary.
The write-time enforcer is CapsulePiiGuard (sprint 20260518_051 / TASK-051-03); it runs on every canonicalised manifest before the signer touches the bytes, on regulatory audit events, on evidence-hold reasons, and on decision_capsules.assembly_inputs. Violations raise HTTP 400 and emit an evidence.capsule.pii_rejected audit event containing the path of the offending field but not the offending value.
EDPB-aligned Pseudonymisation Pattern
A SHA-256 of an email address is pseudonymous data, not anonymous data. If the side-table mapping actor_ref -> email is purged but the hash remains in a sealed capsule, the hash is still personal data until the salt is destroyed (EDPB Guidelines 04/2019 on Pseudonymisation, §28).
Stella Ops therefore:
- never binds
sha256(email)(or any other PII hash) into a capsule’s signed payload; - uses an opaque
actor_ref(UUIDv4 or KSUID) as the only identifier inside immutable artefacts; - stores the cleartext, mutable, and erasable mapping in
shared.actor_identity, which is the side-table holding “additional information [that] is kept separately and subject to technical and organisational measures” (EDPB Guidelines 04/2019, definition of pseudonymisation).
Erasure therefore deletes the cleartext mapping from shared.actor_identity; the capsules referencing actor_ref continue to verify because the canonical bytes they signed never contained any PII.
Side-Table Contract
Table: shared.actor_identity (tenant_id, actor_ref) primary key.
Columns: display_name, email, ip_first_seen, ip_last_seen, user_agent_last, external_subject, region_tag, created_at, updated_at, erased_at, erased_by, erasure_audit_id.
Operations are exposed through Platform.ActorIdentity.Client:
GetAsync(tenantId, actorRef, ct)— returns an active row, an erased row (PII nulled +erased_atpopulated), ornullwhen no row exists.UpsertAsync(tenantId, actorRef, identity, ct)— idempotent; never rehydrates an erased row.EraseAsync(tenantId, actorRef, requestedBy, ct)— nulls PII columns, stampserased_at+erased_by+erasure_audit_id, and emits theactor.identity.erasedunified audit event.
Read-path Redaction Shape
When a capsule read, audit projection, or SAR endpoint surfaces an actor reference, it must call Platform.ActorIdentity.Client.GetAsync and render one of these two shapes through ActorIdentityRedactor.Project:
// Active actor
{
"actorRef": "01HXYZ...",
"displayName": "Operator",
"email": "operator@example.test",
"externalSubject": "idp|subject",
"regionTag": "eu-west-1",
"erased": null
}
// Erased actor — `erased` discriminator is required (never absent)
{
"actorRef": "01HXYZ...",
"displayName": null,
"email": null,
"externalSubject": null,
"regionTag": "eu-west-1",
"erased": { "at": "2026-05-18T12:34:56Z", "by": "01HXYZ-OPERATOR" }
}
The actorRef is always preserved (it is opaque and so not PII), which keeps audit graphs navigable: an operator can still see that an action happened after the natural-person mapping was purged.
Subject Access Request Bundle
Platform exposes the subject access foundation at:
GET /api/v1/operator/sar/{actorRef}
The endpoint is tenant scoped and requires the platform:sar:read scope. The request tenant is resolved through the existing Platform request-context resolver, and both the tenant id and actorRef must be UUID values.
The current bundle shape is:
{
"sarVersion": "1.0.0",
"tenantId": "00000000-0000-0000-0000-000000000000",
"actorRef": "00000000-0000-0000-0000-000000000000",
"generatedAt": "2026-05-18T12:34:56Z",
"generator": "stellaops-platform",
"regionFilter": "eu-only",
"actorIdentity": {
"actorRef": "00000000-0000-0000-0000-000000000000",
"externalSubject": "idp|subject",
"displayName": "Operator",
"email": "operator@example.test",
"regionTag": "eu-only",
"erasedAt": null
},
"services": {
"timeline": {
"service": "timeline",
"status": "deferred",
"count": 0,
"reason": "timeline_join_pending"
},
"authority": {
"service": "authority",
"status": "available",
"count": 2,
"authority": {
"user": {
"userId": "00000000-0000-0000-0000-000000000000",
"externalSubject": "idp|subject",
"username": "operator",
"status": "active"
},
"roleAssignments": [
{ "role": "release-approver", "assignedAt": "2026-05-18T11:15:00Z" }
]
}
},
"findings": {
"service": "findings",
"status": "available",
"count": 2,
"findings": {
"events": [
{
"eventId": "11111111-1111-1111-1111-111111111111",
"findingId": "finding-1",
"eventType": "finding.created",
"actorType": "operator",
"occurredAt": "2026-05-19T07:00:00Z"
},
{
"eventId": "22222222-2222-2222-2222-222222222222",
"findingId": "finding-1",
"eventType": "finding.status_changed",
"actorType": "operator",
"occurredAt": "2026-05-19T07:05:00Z"
}
]
}
},
"evidenceLocker": {
"service": "evidenceLocker",
"status": "deferred",
"count": 0,
"reason": "evidence_locker_join_pending"
},
"notify": {
"service": "notify",
"status": "deferred",
"count": 0,
"reason": "notify_join_pending"
}
},
"missingServices": [
"timeline",
"evidenceLocker",
"notify"
],
"signature": {
"alg": "ed25519",
"keyId": "stella-ed25519-sar",
"value": "<base64 detached Ed25519 signature>",
"payloadType": "application/vnd.stellaops.sar.v1+json",
"payload": "<base64 canonical signature-stripped bundle>",
"publicKey": "<base64 raw Ed25519 public key>"
}
}
The Platform aggregator declares typed projection sources for every service, so an operator always sees an explicit per-service entry rather than a silent omission.
- Authority —
ISubjectAccessAuthorityProjectionSourceresolves the Authority user + role assignments byexternal_subject. The default registration is an empty source until the live Authority adapter is wired in a follow-on sprint; the joiner reportsauthority_external_subject_missingorauthority_projection_missingwhen no projection is available. - Timeline —
ITimelineSubjectAccessProjectionSourcereturns redacted event projections for the actor (the Timeline-side redaction inaudit-retention.md§4 governs PII visibility on read). The default registration defers withtimeline_projection_source_not_configured. - EvidenceLocker —
IEvidenceLockerSubjectAccessProjectionSourcereturns capsule and bundle references (not full manifests; the operator follows the standard capsule read endpoint, which re-checks region residency). The default registration defers withevidence_locker_projection_source_not_configured. - Notify —
INotifySubjectAccessProjectionSourcereturns delivery references. The default registration defers withnotify_projection_source_not_configured. - Findings —
IFindingsSubjectAccessProjectionSourcereturns opaque ledger event references for the actor (eventId,findingId,eventType,actorType,occurredAt— no raw subject, email, IP, or user agent). The Findings ledger persists only an opaqueactor_ref(sprintSPRINT_20260519_078); the join key is(tenant_id, actor_ref). The default registration in Platform defers withfindings_projection_source_not_configureduntil the cross-service ledger adapter is wired (it ships with the HTTP actor identity transport follow-on,SPRINT_NNN_Web_actor_identity_consumer); once the adapter is registered the SAR reports Findings asavailableand drops it frommissingServices. Findings no longer writes any actor PII: a write-time guard (CapsulePiiGuard, reused from EvidenceLocker) rejects any event whose canonical envelope contains email/IP/user-agent/name-shaped values and emitsfindings.actor_pii_rejected.
The signature field is a real Ed25519 DSSE detached signature (Sprint SPRINT_20260519_079 / TASK-079-01). The producer canonicalises the signature-stripped bundle (JsonIgnoreCondition.WhenWritingNull, record property order preserved), embeds it as the base64 payload, computes the DSSE pre-authentication encoding (PAE) over payloadType + payload, and signs the PAE with the ExportCenter Ed25519 key wired through compose (Export:Attestation:Ed25519:PrivateKeyBase64; the SAR key id defaults to stella-ed25519-sar). The raw public key is echoed in publicKey for convenience. It is offline-verifiable via:
# Authoritative: verify against a local trust root (Ed25519 public key PEM,
# raw base64 SubjectPublicKeyInfo, or raw 32-byte base64 key).
stella verify sar path/to/bundle.json --trust-root path/to/sar-public-key.pem
# Integrity-only fallback (no trust root): verifies against the self-described
# public key in the bundle.
stella verify sar path/to/bundle.json
(See the stella verify sar command in src/Cli/StellaOps.Cli/Commands/VerifyCommandGroup.cs and the signer in src/Platform/StellaOps.Platform.WebService/Services/SubjectAccessRequestSigner.cs.)
Data Minimisation Boundary
Evidence capsules must contain opaque actor references, content hashes, and operational metadata only. Clear actor identity belongs in shared.actor_identity and is read by Platform (and by any other consumer through Platform.ActorIdentity.Client) at render time. Erasure can therefore remove mutable identity fields without rewriting sealed capsule bytes, and the DSSE signature of every legacy capsule remains valid after Art. 17 erasure.
