Compliance isnβt just a checkbox exercise. Done right, compliance requirements drive you toward better security architecture. Done wrong, they become expensive security theater that doesnβt actually protect your data.
Iβve spent the last year helping our organization achieve and maintain compliance with PCI-DSS, HIPAA, and SOC 2. Hereβs what Iβve learned about building systems that are both compliant and genuinely secure.
Understanding the Landscape
Different regulations protect different types of data:
PCI-DSS: Payment Card Industry Data Security Standard. Protects credit card data.
HIPAA: Health Insurance Portability and Accountability Act. Protects health information.
SOC 2: Service Organization Control 2. Validates internal controls for security, availability, and confidentiality.
GDPR: (Coming 2018) General Data Protection Regulation. Protects personal data of EU citizens.
Each has specific requirements, but common themes emerge:
- Encryption of sensitive data
- Access controls and authentication
- Audit logging
- Network segmentation
- Regular security assessments
- Incident response procedures
Encryption Requirements
Almost every regulation requires encryption of sensitive data.
Data at Rest
PCI-DSS Requirement 3.4: βRender PAN unreadable anywhere it is stored.β
HIPAA Β§ 164.312(a)(2)(iv): βImplement a mechanism to encrypt electronic protected health information.β
Implementation:
type ComplianceEncryption struct {
kms KeyManagementService
}
func (ce *ComplianceEncryption) EncryptPII(data []byte, dataType string) (*EncryptedData, error) {
// Use different keys for different data types
keyID := ce.getKeyForDataType(dataType)
// Encrypt with AES-256 GCM
ciphertext, nonce, err := ce.kms.Encrypt(keyID, data)
if err != nil {
return nil, err
}
// Store metadata for compliance tracking
return &EncryptedData{
Ciphertext: ciphertext,
Nonce: nonce,
KeyID: keyID,
Algorithm: "AES-256-GCM",
EncryptedAt: time.Now(),
DataType: dataType,
ComplianceTag: getComplianceTag(dataType), // "PCI", "HIPAA", etc.
}, nil
}
func (ce *ComplianceEncryption) getKeyForDataType(dataType string) string {
switch dataType {
case "credit_card":
return "pci-dss-key-2015"
case "health_info":
return "hipaa-key-2015"
default:
return "general-encryption-key"
}
}
Key points:
- Use strong encryption: AES-256 is the standard
- Separate keys for different data types: Limit blast radius if a key is compromised
- Track what was encrypted when: Essential for audit trails
- Use authenticated encryption: GCM mode provides both confidentiality and integrity
Data in Transit
All regulations require encryption of data in transit. TLS 1.2+ is the standard:
func createSecureServer() *http.Server {
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
// PCI-DSS approved cipher suites
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
PreferServerCipherSuites: true,
}
return &http.Server{
Addr: ":443",
TLSConfig: tlsConfig,
// PCI-DSS recommends against SSLv2, SSLv3, TLS 1.0, TLS 1.1
}
}
Access Control
Least Privilege
SOC 2 CC6.3: βLogical and physical access is restricted to authorized users.β
Every user and system should have the minimum permissions necessary:
type AccessControl struct {
rbac *RBACEngine
}
func (ac *AccessControl) CheckAccess(user *User, resource string, action string) error {
// Check if user has role that grants access
roles := ac.rbac.GetUserRoles(user.ID)
for _, role := range roles {
permissions := ac.rbac.GetRolePermissions(role)
for _, perm := range permissions {
if perm.Resource == resource && perm.Action == action {
// Log access for compliance audit
ac.logAccess(user, resource, action, true)
return nil
}
}
}
// Access denied - log for compliance
ac.logAccess(user, resource, action, false)
return errors.New("access denied")
}
func (ac *AccessControl) logAccess(user *User, resource string, action string, granted bool) {
auditLog.Record(AuditEvent{
Type: "access_control",
UserID: user.ID,
Resource: resource,
Action: action,
Granted: granted,
Timestamp: time.Now(),
})
}
Multi-Factor Authentication
PCI-DSS 8.3: βSecure all individual non-console administrative access using multi-factor authentication.β
For privileged access, require MFA:
func (auth *AuthService) AuthenticateAdmin(username, password, mfaToken string) (*Session, error) {
// First factor: password
user, err := auth.validatePassword(username, password)
if err != nil {
return nil, err
}
// Require MFA for admin users
if user.IsAdmin {
if mfaToken == "" {
return nil, errors.New("MFA required for admin access")
}
// Second factor: TOTP token
valid := auth.validateMFAToken(user.ID, mfaToken)
if !valid {
auth.logMFAFailure(user.ID)
return nil, errors.New("invalid MFA token")
}
}
// Log successful authentication for compliance
auth.logAuthentication(user.ID, true, user.IsAdmin)
return auth.createSession(user)
}
Audit Logging
Every regulation requires comprehensive audit logs.
What to Log
Log all security-relevant events:
- Authentication attempts (success and failure)
- Access to sensitive data
- Privileged operations
- Configuration changes
- System access (who logged into systems)
type AuditLogger struct {
storage *ImmutableStorage
}
func (al *AuditLogger) LogDataAccess(user *User, dataID string, dataType string) {
event := AuditEvent{
Type: "data_access",
UserID: user.ID,
DataID: dataID,
DataType: dataType,
Timestamp: time.Now(),
SourceIP: user.IP,
Compliance: getComplianceFramework(dataType),
}
// Write to immutable append-only log
al.storage.Append(event)
}
func getComplianceFramework(dataType string) []string {
switch dataType {
case "credit_card":
return []string{"PCI-DSS"}
case "health_info":
return []string{"HIPAA"}
default:
return []string{"SOC2"}
}
}
Immutable Audit Logs
Audit logs must be tamper-proof. Once written, they canβt be modified or deleted:
type ImmutableStorage struct {
writer *s3.Writer
}
func (is *ImmutableStorage) Append(event AuditEvent) error {
// Write to append-only S3 bucket with object lock
data, _ := json.Marshal(event)
// File name includes timestamp for ordering
filename := fmt.Sprintf("audit/%s/%s.json",
event.Timestamp.Format("2006/01/02"),
event.ID,
)
return is.writer.WriteWithLock(filename, data, 7*365*24*time.Hour) // 7 year retention
}
Use cloud provider features like S3 Object Lock or write-once-read-many (WORM) storage.
Log Retention
Different regulations require different retention periods:
- PCI-DSS: 1 year, with 3 months immediately available
- HIPAA: 6 years
- SOC 2: Depends on policy, typically 1+ years
Plan storage accordingly:
const (
PCILogRetention = 365 * 24 * time.Hour // 1 year
HIPAALogRetention = 6 * 365 * 24 * time.Hour // 6 years
SOC2LogRetention = 2 * 365 * 24 * time.Hour // 2 years
)
func (al *AuditLogger) GetRetentionPeriod(dataType string) time.Duration {
switch dataType {
case "credit_card":
return PCILogRetention
case "health_info":
return HIPAALogRetention
default:
return SOC2LogRetention
}
}
Network Segmentation
PCI-DSS Requirement 1: βInstall and maintain a firewall configuration to protect cardholder data.β
Segment your network so not everything can access sensitive data:
βββββββββββββββββββββββββββββββββββββββ
β Public Zone β
β - Web servers β
β - Public APIs β
βββββββββββββββ¬ββββββββββββββββββββββββ
β Firewall
βββββββββββββββΌββββββββββββββββββββββββ
β Application Zone β
β - Application servers β
β - Business logic β
βββββββββββββββ¬ββββββββββββββββββββββββ
β Firewall
βββββββββββββββΌββββββββββββββββββββββββ
β Data Zone β
β - Databases with PII/PHI β
β - Encryption key services β
β - No direct internet access β
βββββββββββββββββββββββββββββββββββββββ
Implement with security groups, network ACLs, or firewall rules:
# Example: AWS security group rules
data_zone_sg = SecurityGroup(
name="data-zone",
ingress_rules=[
# Only allow access from application zone
Rule(
protocol="tcp",
port=5432, # PostgreSQL
source_security_group="application-zone-sg"
)
],
egress_rules=[
# No outbound internet access
Rule(
protocol="tcp",
port=443,
destination="10.0.0.0/8" # Internal only
)
]
)
Vulnerability Management
SOC 2 CC7.1: βThe entity identifies and manages security vulnerabilities.β
Regular Scanning
Scan for vulnerabilities regularly:
#!/bin/bash
# vulnerability-scan.sh
# Scan infrastructure
nessus-cli --scan infrastructure --profile compliance
# Scan containers
for image in $(docker images --format "{{.Repository}}:{{.Tag}}"); do
trivy image --severity HIGH,CRITICAL $image
done
# Scan dependencies
for service in order-service auth-service encryption-service; do
cd /services/$service
snyk test
done
# Generate compliance report
generate-compliance-report --framework PCI-DSS --date $(date +%Y-%m-%d)
Automate this in CI/CD:
# .gitlab-ci.yml
security_scan:
stage: test
script:
- trivy image --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- snyk test
only:
- branches
allow_failure: false # Block deployment if vulnerabilities found
Patch Management
Track and remediate vulnerabilities within defined timeframes:
- Critical: 24-48 hours
- High: 7 days
- Medium: 30 days
type VulnerabilityTracker struct {
db *Database
}
func (vt *VulnerabilityTracker) Track(vuln *Vulnerability) error {
deadline := vt.calculateDeadline(vuln.Severity)
return vt.db.Insert(VulnerabilityRecord{
ID: vuln.ID,
Severity: vuln.Severity,
Component: vuln.Component,
Description: vuln.Description,
DetectedAt: time.Now(),
Deadline: deadline,
Status: "OPEN",
})
}
func (vt *VulnerabilityTracker) calculateDeadline(severity string) time.Time {
switch severity {
case "CRITICAL":
return time.Now().Add(48 * time.Hour)
case "HIGH":
return time.Now().Add(7 * 24 * time.Hour)
case "MEDIUM":
return time.Now().Add(30 * 24 * time.Hour)
default:
return time.Now().Add(90 * 24 * time.Hour)
}
}
Data Retention and Disposal
GDPR Article 17: βRight to erasureβ (right to be forgotten)
Data Lifecycle
Define lifecycle for different data types:
type DataLifecyclePolicy struct {
DataType string
RetentionPeriod time.Duration
DisposalMethod string
}
var policies = []DataLifecyclePolicy{
{
DataType: "credit_card",
RetentionPeriod: 90 * 24 * time.Hour, // 90 days
DisposalMethod: "crypto-shredding",
},
{
DataType: "health_info",
RetentionPeriod: 6 * 365 * 24 * time.Hour, // 6 years
DisposalMethod: "secure-deletion",
},
}
func (dlm *DataLifecycleManager) ScheduleDisposal(dataID string, dataType string) {
policy := dlm.getPolicyForDataType(dataType)
disposalDate := time.Now().Add(policy.RetentionPeriod)
dlm.scheduler.Schedule(disposalDate, func() {
dlm.disposeData(dataID, policy.DisposalMethod)
})
}
Cryptographic Shredding
When disposing of encrypted data, delete the encryption key instead of overwriting data:
func (dlm *DataLifecycleManager) cryptoShred(dataID string) error {
// Get the encryption key used for this data
keyID, err := dlm.getKeyForData(dataID)
if err != nil {
return err
}
// Delete the encryption key
// This makes the data unrecoverable
err = dlm.kms.DeleteKey(keyID)
if err != nil {
return err
}
// Log for compliance
dlm.auditLog.Record(AuditEvent{
Type: "data_disposal",
DataID: dataID,
Method: "crypto-shredding",
KeyID: keyID,
Timestamp: time.Now(),
})
return nil
}
This is faster and more reliable than overwriting data.
Incident Response
SOC 2 CC7.3: βThe entity responds to identified security incidents.β
Detection
Monitor for security incidents:
func (ids *IntrusionDetection) MonitorForIncidents() {
// Monitor failed authentication attempts
go ids.monitorFailedAuth()
// Monitor unusual data access patterns
go ids.monitorDataAccess()
// Monitor system integrity
go ids.monitorSystemIntegrity()
}
func (ids *IntrusionDetection) monitorFailedAuth() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for range ticker.C {
count := ids.getFailedAuthCount(5 * time.Minute)
if count > ids.threshold {
ids.raiseIncident(Incident{
Type: "excessive_failed_auth",
Severity: "HIGH",
Description: fmt.Sprintf("%d failed auth attempts in 5 minutes", count),
DetectedAt: time.Now(),
})
}
}
}
Response Procedures
Document and automate response procedures:
func (ir *IncidentResponse) HandleIncident(incident *Incident) error {
// 1. Contain
containErr := ir.contain(incident)
// 2. Notify
ir.notify(incident)
// 3. Investigate
investigation := ir.investigate(incident)
// 4. Remediate
ir.remediate(incident, investigation)
// 5. Document
return ir.document(incident, investigation, containErr)
}
func (ir *IncidentResponse) contain(incident *Incident) error {
switch incident.Type {
case "excessive_failed_auth":
// Block the source IP
return ir.firewall.BlockIP(incident.SourceIP, 1*time.Hour)
case "data_breach":
// Rotate encryption keys
return ir.kms.RotateAllKeys()
case "malware_detected":
// Isolate the affected system
return ir.isolateSystem(incident.SystemID)
default:
return nil
}
}
Compliance Automation
Manual compliance doesnβt scale. Automate everything possible:
Configuration as Code
Define compliant configurations in code:
# security_policy.py
class PCIDSSPolicy:
@staticmethod
def check_encryption_at_rest(resource):
"""PCI-DSS 3.4: Encrypt stored cardholder data"""
if resource.type == "database" and resource.contains_pci_data:
assert resource.encryption_enabled, "PCI data must be encrypted at rest"
@staticmethod
def check_network_segmentation(resource):
"""PCI-DSS 1.2: Segment cardholder data from DMZ"""
if resource.type == "database" and resource.contains_pci_data:
assert resource.subnet_type == "private", "PCI data must be in private subnet"
assert not resource.internet_accessible, "PCI data must not be internet-accessible"
Continuous Compliance Checking
Run compliance checks continuously:
func (cc *ComplianceChecker) RunContinuousChecks() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
violations := []ComplianceViolation{}
// Check encryption
violations = append(violations, cc.checkEncryption()...)
// Check access controls
violations = append(violations, cc.checkAccessControls()...)
// Check audit logging
violations = append(violations, cc.checkAuditLogging()...)
if len(violations) > 0 {
cc.reportViolations(violations)
}
}
}
Auditor Perspective
What auditors look for:
- Evidence: Audit logs, configuration files, scan results
- Consistency: Are controls actually in use, or just documented?
- Automation: Manual processes are error-prone
- Testing: Do you test your controls?
Make the auditorβs job easy:
// Generate compliance report for auditors
func (cr *ComplianceReporter) GenerateReport(framework string, period TimePeriod) (*Report, error) {
report := &Report{
Framework: framework,
Period: period,
}
// Collect evidence
report.EncryptionEvidence = cr.collectEncryptionEvidence(period)
report.AccessControlEvidence = cr.collectAccessControlEvidence(period)
report.AuditLogEvidence = cr.collectAuditLogEvidence(period)
report.VulnerabilityScanResults = cr.collectVulnerabilityScanResults(period)
report.IncidentResponseEvidence = cr.collectIncidentResponseEvidence(period)
return report, nil
}
Provide clear, organized evidence. Auditors appreciate it.
Lessons Learned
Compliance as Security Driver: Use compliance requirements to justify security improvements.
Automate Everything: Manual compliance doesnβt scale and is error-prone.
Build for Compliance: Retrofit is expensive. Design compliant systems from the start.
Document Everything: If itβs not documented, it doesnβt exist (from compliance perspective).
Test Your Controls: Auditors will test them. You should too.
Conclusion
Compliance is complex and sometimes frustrating. But it forces you to think about security systematically.
The key is to see compliance not as a burden but as a framework for building secure systems. The best compliance programs improve actual security, not just check boxes.
Start with understanding what you need to protect and which regulations apply. Build systems with compliance in mind. Automate checks and evidence collection. Document everything.
Your auditorsβand your customersβwill thank you.
In future posts, Iβll dive into specific frameworks (PCI-DSS deep dive, HIPAA implementation, SOC 2 preparation) and compliance automation tools.
Stay compliant, stay secure.