Access Control for PII
Effective access control is critical for ISO 27018 compliance. This lesson covers implementing least privilege, role-based access control (RBAC), and monitoring PII access in cloud environments.
Access Control Principles
Core Requirements
ISO 27018 Position: "Access to PII shall be restricted to authorized personnel with a legitimate need to know for specified purposes."
Key Principles:
- Least Privilege - Minimum access necessary
- Need-to-Know - Access based on job function
- Separation of Duties - No single person has complete control
- Regular Review - Periodic access audits
- Revocation - Immediate removal when no longer needed
Role-Based Access Control (RBAC)
RBAC Architecture
Users → Roles → Permissions → Resources (PII)
Example:
- Customer Support Rep → Support Role → Read Customer Contact → Email, Phone
- Developer → Dev Role → Read Anonymous Data → Analytics DB
- Admin → Admin Role → Full Access → All Systems (heavily audited)
Implementing RBAC
Role Definition:
enum Role {
CUSTOMER_SUPPORT = 'customer_support',
DEVELOPER = 'developer',
DATA_ANALYST = 'data_analyst',
MARKETING = 'marketing',
ADMIN = 'admin',
COMPLIANCE_OFFICER = 'compliance_officer'
}
interface Permission {
resource: string;
actions: ('read' | 'write' | 'delete')[];
conditions?: AccessCondition[];
}
interface RoleDefinition {
role: Role;
permissions: Permission[];
piiAccess: PIIAccessLevel;
requiresMFA: boolean;
sessionTimeout: number; // minutes
}
const roleDefinitions: RoleDefinition[] = [
{
role: Role.CUSTOMER_SUPPORT,
permissions: [
{
resource: 'customer_profile',
actions: ['read'],
conditions: [{ field: 'support_ticket_exists', value: true }]
},
{
resource: 'support_tickets',
actions: ['read', 'write']
}
],
piiAccess: 'limited', // Only contact info, not payment
requiresMFA: true,
sessionTimeout: 30
},
{
role: Role.DEVELOPER,
permissions: [
{
resource: 'analytics_data',
actions: ['read']
},
{
resource: 'system_logs',
actions: ['read']
}
],
piiAccess: 'none', // No PII access for developers
requiresMFA: true,
sessionTimeout: 60
},
{
role: Role.DATA_ANALYST,
permissions: [
{
resource: 'aggregated_data',
actions: ['read']
},
{
resource: 'anonymized_datasets',
actions: ['read', 'write']
}
],
piiAccess: 'pseudonymized', // Only pseudonymized data
requiresMFA: true,
sessionTimeout: 120
}
];
Access Control Implementation
Middleware/Gateway:
class PIIAccessControl {
async checkAccess(
userId: string,
resource: string,
action: string,
context: AccessContext
): Promise<AccessDecision> {
// 1. Get user's roles
const roles = await this.getUserRoles(userId);
// 2. Get combined permissions from all roles
const permissions = await this.getRolePermissions(roles);
// 3. Check if action on resource is permitted
const permitted = this.isActionPermitted(permissions, resource, action);
if (!permitted) {
await this.auditLog.record('ACCESS_DENIED', {
userId,
resource,
action,
reason: 'insufficient_permissions'
});
return { allowed: false, reason: 'Insufficient permissions' };
}
// 4. Evaluate conditions
const conditionsMet = await this.evaluateConditions(
permissions,
resource,
context
);
if (!conditionsMet) {
await this.auditLog.record('ACCESS_DENIED', {
userId,
resource,
action,
reason: 'conditions_not_met'
});
return { allowed: false, reason: 'Access conditions not met' };
}
// 5. Check MFA if required
const role = roles[0]; // Primary role
if (this.requiresMFA(role) && !context.mfaVerified) {
return { allowed: false, reason: 'MFA required', requiresMFA: true };
}
// 6. Log successful access
await this.auditLog.record('ACCESS_GRANTED', {
userId,
resource,
action,
timestamp: new Date()
});
return { allowed: true };
}
private isActionPermitted(
permissions: Permission[],
resource: string,
action: string
): boolean {
return permissions.some(
p => p.resource === resource && p.actions.includes(action as any)
);
}
}
Attribute-Based Access Control (ABAC)
Advanced Access Control
When to Use ABAC:
- Complex, dynamic access requirements
- Fine-grained control needed
- Context-dependent access (time, location, device)
ABAC Policy Example:
interface ABACPolicy {
id: string;
description: string;
subject: SubjectAttributes;
resource: ResourceAttributes;
action: string;
environment: EnvironmentAttributes;
effect: 'allow' | 'deny';
}
interface SubjectAttributes {
roles?: string[];
department?: string;
clearanceLevel?: number;
mfaVerified?: boolean;
}
interface ResourceAttributes {
type: string;
sensitivity: 'low' | 'medium' | 'high' | 'critical';
owner?: string;
tags?: string[];
}
interface EnvironmentAttributes {
time?: TimeConstraint;
location?: LocationConstraint;
ipAddress?: string;
deviceTrusted?: boolean;
}
const piiAccessPolicies: ABACPolicy[] = [
{
id: 'policy_001',
description: 'Support can view customer contact during business hours',
subject: {
roles: ['customer_support'],
mfaVerified: true
},
resource: {
type: 'customer_contact',
sensitivity: 'medium'
},
action: 'read',
environment: {
time: {
startHour: 8,
endHour: 18,
timezone: 'UTC'
},
deviceTrusted: true
},
effect: 'allow'
},
{
id: 'policy_002',
description: 'Deny all access to payment data from untrusted devices',
subject: {},
resource: {
type: 'payment_info',
sensitivity: 'critical'
},
action: '*',
environment: {
deviceTrusted: false
},
effect: 'deny'
}
];
class ABACEngine {
async evaluatePolicy(
user: User,
resource: Resource,
action: string,
environment: Environment
): Promise<boolean> {
const applicablePolicies = this.findApplicablePolicies(
user,
resource,
action,
environment
);
// Deny takes precedence
if (applicablePolicies.some(p => p.effect === 'deny')) {
return false;
}
// At least one allow policy must match
return applicablePolicies.some(p => p.effect === 'allow');
}
}
Field-Level Access Control
Granular PII Access
Control at Field Level:
interface FieldAccessPolicy {
field: string;
roles: Role[];
masked: boolean;
conditions?: string[];
}
const customerFieldAccess: FieldAccessPolicy[] = [
{
field: 'email',
roles: [Role.CUSTOMER_SUPPORT, Role.ADMIN],
masked: false
},
{
field: 'phone',
roles: [Role.CUSTOMER_SUPPORT, Role.ADMIN],
masked: false
},
{
field: 'ssn',
roles: [Role.ADMIN],
masked: true, // Always masked, even for admins
conditions: ['has_valid_business_justification']
},
{
field: 'credit_card',
roles: [Role.ADMIN],
masked: true,
conditions: ['payment_issue_investigation']
}
];
class FieldLevelAccessControl {
async filterFields(
record: any,
userRole: Role,
context: AccessContext
): Promise<any> {
const filteredRecord: any = { id: record.id };
for (const [field, value] of Object.entries(record)) {
const policy = customerFieldAccess.find(p => p.field === field);
if (!policy) {
// Non-PII field, include by default
filteredRecord[field] = value;
continue;
}
// Check role permission
if (!policy.roles.includes(userRole)) {
continue; // Field not accessible to this role
}
// Check conditions
if (policy.conditions) {
const conditionsMet = await this.checkConditions(
policy.conditions,
context
);
if (!conditionsMet) {
continue;
}
}
// Apply masking if required
if (policy.masked) {
filteredRecord[field] = this.maskValue(field, value);
} else {
filteredRecord[field] = value;
}
}
// Audit field access
await this.auditFieldAccess(record.id, filteredRecord, userRole);
return filteredRecord;
}
private maskValue(field: string, value: string): string {
switch (field) {
case 'ssn':
return '***-**-' + value.slice(-4);
case 'credit_card':
return '****-****-****-' + value.slice(-4);
case 'email':
return value[0] + '***@' + value.split('@')[1];
default:
return '***';
}
}
}
Database Access Control
Row-Level Security
PostgreSQL Example:
-- Enable Row-Level Security
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
-- Policy: Support can only see assigned customers
CREATE POLICY support_access_policy ON customers
FOR SELECT
TO support_role
USING (
assigned_support_id = current_user_id()
AND support_ticket_active = true
);
-- Policy: Analysts see only anonymized data
CREATE POLICY analyst_access_policy ON customers
FOR SELECT
TO analyst_role
USING (
-- Returns anonymized view
false -- Force through anonymized_customers view instead
);
CREATE VIEW anonymized_customers AS
SELECT
md5(customer_id::text) as pseudo_id,
age_range(date_of_birth) as age_range,
zip_to_region(zip_code) as region,
-- No PII fields
FROM customers;
GRANT SELECT ON anonymized_customers TO analyst_role;
Column-Level Security
Restrict Column Access:
-- Revoke default access
REVOKE ALL ON customers FROM PUBLIC;
-- Grant specific column access to support role
GRANT SELECT (customer_id, email, phone, order_history)
ON customers TO support_role;
-- Admin gets full access
GRANT ALL ON customers TO admin_role;
-- Developers get no PII access
GRANT SELECT (customer_id, created_at, last_login)
ON customers TO developer_role;
API Access Control
OAuth 2.0 Scopes for PII
Scope Definition:
const piiScopes = {
'pii:read:contact': 'Read customer contact information',
'pii:read:payment': 'Read customer payment methods',
'pii:write:profile': 'Update customer profile',
'pii:delete:account': 'Delete customer account',
'pii:export:all': 'Export all customer PII'
};
// API endpoint protection
@RequireScope('pii:read:contact')
@RequireMFA()
async getCustomerContact(customerId: string): Promise<ContactInfo> {
// Only called if user has required scope and MFA
return await this.customerService.getContact(customerId);
}
// Scope validation middleware
function requireScope(scope: string) {
return async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
const decoded = await this.verifyToken(token);
if (!decoded.scopes.includes(scope)) {
return res.status(403).json({
error: 'insufficient_scope',
required: scope
});
}
next();
};
}
Multi-Factor Authentication (MFA)
MFA Requirements for PII Access
When MFA is Required:
- Administrative access to PII
- Bulk PII exports
- PII deletion operations
- Access from new devices/locations
- High-sensitivity PII access
Implementation:
class MFAService {
async requireMFA(userId: string, operation: string): Promise<boolean> {
const user = await this.getUser(userId);
// Check if MFA already verified in current session
if (user.session.mfaVerified && !this.sessionExpired(user.session)) {
return true;
}
// Require MFA for operation
const mfaChallenge = await this.generateMFAChallenge(userId, operation);
// Wait for MFA verification (webhook/polling)
const verified = await this.waitForMFAVerification(
mfaChallenge.id,
timeout: 300000 // 5 minutes
);
if (verified) {
await this.markSessionMFAVerified(user.session.id);
await this.auditLog.record('MFA_VERIFIED', {
userId,
operation,
timestamp: new Date()
});
}
return verified;
}
async generateMFAChallenge(
userId: string,
operation: string
): Promise<MFAChallenge> {
const user = await this.getUser(userId);
const code = this.generateTOTPCode(user.mfaSecret);
// Send via configured method
if (user.mfaMethod === 'sms') {
await this.sendSMS(user.phone, `Your code: ${code}`);
} else if (user.mfaMethod === 'email') {
await this.sendEmail(user.email, `Your code: ${code}`);
} else if (user.mfaMethod === 'totp') {
// User uses authenticator app
}
return {
id: generateId(),
userId,
operation,
expiresAt: new Date(Date.now() + 300000)
};
}
}
Access Monitoring and Auditing
Comprehensive Audit Logging
What to Log:
interface PIIAccessLog {
id: string;
timestamp: Date;
userId: string;
userName: string;
userRole: Role;
action: 'read' | 'write' | 'delete' | 'export';
resource: string;
resourceId: string;
piiFields: string[];
ipAddress: string;
deviceId: string;
sessionId: string;
mfaVerified: boolean;
accessDenied: boolean;
denialReason?: string;
businessJustification?: string;
}
class PIIAuditLogger {
async logAccess(log: PIIAccessLog): Promise<void> {
// Immutable append-only log
await this.auditDatabase.insert('pii_access_log', {
...log,
logHash: this.calculateHash(log) // Tamper detection
});
// Real-time alerting for suspicious access
if (this.isSuspicious(log)) {
await this.alertSecurityTeam(log);
}
// Compliance reporting
if (log.action === 'export' || log.action === 'delete') {
await this.notifyComplianceTeam(log);
}
}
private isSuspicious(log: PIIAccessLog): boolean {
return (
// Unusual access time
this.isOutsideBusinessHours(log.timestamp) ||
// High volume access
this.isHighVolumeAccess(log.userId) ||
// New device/location
this.isNewDevice(log.deviceId, log.userId) ||
// Access denied attempts
log.accessDenied
);
}
}
Anomaly Detection
Automated Monitoring:
class AccessAnomalyDetection {
async detectAnomalies(): Promise<Anomaly[]> {
const anomalies: Anomaly[] = [];
// 1. Unusual access patterns
const unusualAccess = await this.detectUnusualAccessPattern();
anomalies.push(...unusualAccess);
// 2. Privilege escalation attempts
const escalation = await this.detectPrivilegeEscalation();
anomalies.push(...escalation);
// 3. Data exfiltration indicators
const exfiltration = await this.detectPossibleExfiltration();
anomalies.push(...exfiltration);
// 4. Suspicious query patterns
const suspiciousQueries = await this.detectSuspiciousQueries();
anomalies.push(...suspiciousQueries);
return anomalies;
}
private async detectUnusualAccessPattern(): Promise<Anomaly[]> {
// ML-based anomaly detection
const baseline = await this.getBaselineAccessPattern();
const current = await this.getCurrentAccessPattern();
if (this.deviationScore(baseline, current) > threshold) {
return [{
type: 'unusual_access_pattern',
severity: 'high',
description: 'Access pattern significantly differs from baseline',
recommendation: 'Review recent PII access logs'
}];
}
return [];
}
}
Access Review Process
Regular Access Audits
Quarterly Review Process:
interface AccessReview {
id: string;
reviewDate: Date;
reviewer: string;
scope: 'all' | 'department' | 'role';
findings: AccessReviewFinding[];
remediationActions: RemediationAction[];
}
interface AccessReviewFinding {
userId: string;
currentAccess: Permission[];
justification: string;
approved: boolean;
revokeAccess: boolean;
modifyAccess: boolean;
notes: string;
}
class AccessReviewService {
async conductReview(scope: string): Promise<AccessReview> {
// 1. Get all users in scope
const users = await this.getUsersInScope(scope);
// 2. Review each user's access
const findings: AccessReviewFinding[] = [];
for (const user of users) {
const access = await this.getUserAccess(user.id);
const lastUsed = await this.getLastAccessDate(user.id);
const stillNeeded = await this.verifyAccessNeed(user.id, access);
findings.push({
userId: user.id,
currentAccess: access,
justification: user.accessJustification,
approved: stillNeeded,
revokeAccess: !stillNeeded || daysSince(lastUsed) > 90,
modifyAccess: false,
notes: ''
});
}
// 3. Generate review report
return {
id: generateId(),
reviewDate: new Date(),
reviewer: getCurrentUser(),
scope,
findings,
remediationActions: this.generateRemediationActions(findings)
};
}
}
Compliance Checklist
Access Control Implementation:
- RBAC implemented for all PII access
- Least privilege principle enforced
- MFA required for sensitive PII access
- Field-level access control implemented
- Database row/column-level security configured
- API access controlled via scopes/tokens
- Comprehensive audit logging in place
- Real-time monitoring and alerting
- Quarterly access reviews scheduled
- Immediate access revocation process
- Separation of duties enforced
Next Lesson: Secure data deletion - implementing cryptographic erasure and data destruction.