SMP Data Classification + Encryption Policy¶
Audience: Security, Backend, DevOps, Legal/Compliance · Updated: v3.3
1. Data classification levels¶
| Level | Definition | Examples | Handling |
|---|---|---|---|
| L0 · Public | Có thể công khai | Service catalog, partner names (if consented) | No restriction |
| L1 · Internal | Chỉ dùng trong công ty | Internal docs, dashboards, ops notes | Authenticated access |
| L2 · Confidential | Phải bảo vệ, leak gây hại business | Order data, partner contracts, financial reports | Encryption + RBAC |
| L3 · Restricted (PII) | Personal data, regulated | CCCD, address, phone, email | Encryption + audit + access controls |
| L4 · Highly Restricted | Most sensitive | Bank account, payment intent, password hash | Encryption + minimal access + extra audit |
2. Data inventory · Per domain¶
2.1 Customer data (mostly in inside, SMP holds references)¶
| Field | Level | Storage | Notes |
|---|---|---|---|
| Full name | L3 | inside | SMP cache 30s |
| Phone | L3 | inside | Masked in UI: +84••••2841 |
| L3 | inside | ||
| Address | L3 | inside + SMP order | SMP store per-order snapshot |
| Date of birth | L3 | inside | SMP not store |
| CCCD | L4 | inside | SMP NEVER store |
| Payment method | L4 | inside | SMP only get intent_id |
| Order history (in SMP) | L2 | smp_order MySQL | RBAC scoped |
| Rating + review | L2 | smp_quality | RBAC scoped |
2.2 Agent data¶
| Field | Level | Storage | Notes |
|---|---|---|---|
| Full name | L3 | smp_agent MySQL | |
| Phone | L3 | smp_agent | Masked in non-privileged views |
| L3 | smp_agent | ||
| Home address | L3 | smp_agent | Used for dispatch radius |
| CCCD number | L4 | smp_agent (encrypted) | Application-level AES-256 |
| CCCD photos | L4 | S3/R2 + smp_agent.kyc_docs | URLs signed, expire 15 min |
| Bank account | L4 | smp_agent (encrypted) | Application-level AES-256 |
| Skills + levels | L1 | smp_agent | |
| Location (realtime) | L3 | Redis (TTL 60s) | |
| Earnings | L2 | smp_finance | Self-view OK, others RBAC |
2.3 Partner data (v3.3)¶
| Field | Level | Storage | Notes |
|---|---|---|---|
| Business name | L0/L1 | smp_partner | Public if consent |
| Tax code | L2 | smp_partner | |
| Rep name + CCCD | L4 | smp_partner (encrypted) | Application-level AES-256 |
| Rep phone, email | L3 | smp_partner | |
| Wallet balance | L2 | smp_partner | RBAC scoped |
| Contract docs | L2 | S3/R2 + smp_partner | |
| Pricing config | L2 | smp_partner | Confidential per partner |
| Commission rate | L2 | smp_partner |
2.4 Order data¶
| Field | Level | Storage | Notes |
|---|---|---|---|
| Order ID, timestamps | L1 | smp_order | |
| Customer address (snapshot) | L3 | smp_order | Per-order copy |
| Service + steps | L1 | smp_order | |
| Pricing breakdown | L2 | smp_order | |
| Photo proof | L3 | S3/R2 + meta | URLs signed |
| Customer notes | L3 | smp_order | May contain PII |
| Internal notes (ops) | L2 | smp_order |
2.5 System data¶
| Field | Level | Storage | Notes |
|---|---|---|---|
| Audit logs | L2 | smp_audit MongoDB | Encrypted at rest |
| Event logs | L1 | smp_events MongoDB | TTL 90 days |
| Access logs | L1 | Loki | Retention 30 days |
| Session tokens | L4 | Redis | TTL 8h |
| API keys | L4 | DB (hashed) + Vault | Bcrypt hash |
| JWT signing private key | L4 | Vault only | RS256 private |
| DB passwords | L4 | Vault | Quarterly rotation |
| Webhook signing secrets | L4 | Vault | Per partner |
3. Encryption requirements¶
3.1 At rest¶
| Storage | Method | Key management |
|---|---|---|
| MySQL data files | InnoDB encryption (AES-256) | Managed by cloud provider (AWS KMS / GCP KMS) |
| MongoDB data files | WiredTiger encryption (AES-256) | Cloud KMS |
| Redis | Disk encryption (provider managed) | N/A (in-memory primarily) |
| S3/R2 objects | SSE-KMS (AES-256) | KMS per environment |
| K8s secrets | Sealed Secrets + KMS | Vault for app secrets |
| Backups (DB snapshots) | Encrypted (cloud default) | KMS |
3.2 In transit¶
| Connection | Protocol | Cert |
|---|---|---|
| Client ↔ API Gateway | TLS 1.3 (1.2 min) | Cloudflare-managed (ACM-like) |
| Service ↔ Service (internal) | mTLS via Istio | Auto-rotated by Istio CA |
| App ↔ MySQL | TLS required | Cloud-managed |
| App ↔ Redis | TLS required (prod) | Cloud-managed |
| App ↔ MongoDB | TLS required | Atlas-managed |
| App ↔ External (inside, wms) | TLS 1.2+ | Public CA |
3.3 Application-level field encryption¶
L4 fields: PII identifiers, bank accounts → encrypt in application BEFORE save to DB.
package crypto
// AES-256-GCM with separate key per env
type FieldCipher struct {
aead cipher.AEAD
}
func (c *FieldCipher) Encrypt(plaintext string) (string, error) {
nonce := make([]byte, c.aead.NonceSize())
if _, err := rand.Read(nonce); err != nil { return "", err }
ct := c.aead.Seal(nil, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(append(nonce, ct...)), nil
}
func (c *FieldCipher) Decrypt(encoded string) (string, error) {
b, err := base64.StdEncoding.DecodeString(encoded)
if err != nil { return "", err }
if len(b) < c.aead.NonceSize() { return "", errors.New("invalid") }
nonce, ct := b[:c.aead.NonceSize()], b[c.aead.NonceSize():]
pt, err := c.aead.Open(nil, nonce, ct, nil)
return string(pt), err
}
Store encrypted: agents.cccd_number_encrypted VARCHAR(255).
Storage of plain field name still kept in app code but DB column is _encrypted.
Searchable fields (vd search by CCCD):
- Use deterministic encryption OR
- Use hash column for equality search: cccd_hash CHAR(64) = SHA-256(CCCD + global_salt)
- Don't enable substring search on encrypted PII
3.4 Hashing (one-way)¶
| Data | Algorithm | Cost |
|---|---|---|
| User passwords | bcrypt | cost = 12 |
| API keys | bcrypt | cost = 10 (faster lookup) |
| Session ID validation | HMAC SHA-256 with rotating secret | N/A |
4. Key management¶
4.1 Vault layout¶
secret/smp/<env>/
├── service-keys/
│ ├── jwt-signing-private.pem
│ ├── jwt-signing-public.pem
│ └── webhook-signing-secret
├── field-encryption-keys/
│ ├── agent-pii-aes256-v1
│ ├── partner-pii-aes256-v1
│ └── field-encryption-salt
├── db-credentials/
│ ├── mysql-app-password
│ ├── mysql-readonly-password
│ ├── mongo-app-password
│ └── redis-password
└── external-integrations/
├── inside-api-token
├── wms-api-token
├── sendgrid-api-key
└── twilio-auth-token
4.2 Key rotation schedule¶
| Key type | Rotation | Process |
|---|---|---|
| JWT signing | Annual | Issue new kid, keep old for 30d, then expire |
| Field encryption | Annual | Add v2 key, re-encrypt async, retire v1 |
| Webhook signing | Annual | Coordinate with partners, transition period 30d |
| DB passwords | Quarterly | Use Vault dynamic secrets or scheduled rotation |
| API keys | On request OR annual | User-initiated regeneration |
| TLS certs | Auto (Let's Encrypt-like) | Cloudflare-managed |
| MFA backup codes | On user request | Regenerate set of 10 |
4.3 Key compromise response¶
- Detect (alert from SIEM, suspicious logs)
- Revoke compromised key in Vault
- Force rotate to new key
- Re-encrypt data using old key (background job)
- Audit log inventory of all operations during exposure window
- Notify affected users if PII exposed
- Post-mortem + regulatory notification (PDPA: within 72h if "significant risk")
5. PII handling rules¶
5.1 Display masking¶
UI hiển thị PII với masking by default:
| Field | Full | Masked |
|---|---|---|
| Phone | +84907842931 |
+84•••842931 (last 4) |
hung@example.com |
h***@example.com |
|
| CCCD | 079203012345 |
079•••12345 (first 3 + last 5) |
| Bank account | 0123456789012345 |
••••••••5678 |
| Full name | Nguyễn Văn A |
N*** A (first letter only) |
Privileged roles (ops_admin với scope data.pii.view) có thể click "Reveal" → audit log entry.
5.2 Logs¶
NEVER log full PII. Mask before logging:
logger.Info("create agent",
zap.String("agent_id", agent.ID),
zap.String("name_masked", mask.Name(agent.FullName)),
zap.String("phone_masked", mask.Phone(agent.Phone)),
)
Zap logger has middleware MaskPII for known field names.
5.3 Exports¶
Data exports (CSV reports, debug dumps):
- Default: PII masked
- Full PII export: requires data.export.pii scope + manager approval + audit
- Export file: encrypted ZIP, password sent separately
- Download link: expires 24h, IP-bound
5.4 Customer data sharing with partners¶
Partners see end-customer data ONLY for their orders: - Name + phone (for service delivery) - Service address (operational need) - NOT: payment method, other orders, customer history outside partner's orders
Enforced at API + DB row-level filtering.
5.5 Dynamic Data Masking (v4.0)¶
Implementation pattern cho PII masking ở API Gateway middleware. Default-deny: response chứa masked data, scope unlock specific fields.
5.5.1 Masking patterns catalog¶
Mỗi field type có pattern riêng. Pattern phải: - Đủ để identify match (cho CSKH "có phải khách đầu 0912... cuối ...890?") - Không đủ để leak (không thấy full)
| Field | Plain | Masked pattern | Display |
|---|---|---|---|
| Phone (VN) | 0912345890 |
First 3 + last 3 | 0912****890 |
| Phone (intl) | +14155551234 |
First 4 + last 3 | +1415*****234 |
nguyen.van.a@gmail.com |
First 3 of local + domain | ngu***@gmail.com |
|
| CCCD | 001202012345 |
First 4 + last 4 | 0012****2345 |
| Old CMND | 024567890 |
First 3 + last 2 | 024****90 |
| Bank account | 1234567890123 |
First 4 + last 4 | 1234*****0123 |
| Tax code (VN MST) | 0123456789-001 |
Full only with scope | 01234****89-001 |
| Credit card | 4532123456789010 |
First 6 + last 4 (PCI-DSS) | 453212******9010 |
| Full name | Nguyễn Văn A |
First name + initial | Nguyễn V. A. (light masking) hoặc Nguyễn *** (strong) |
| Address line | 123 Lê Lợi, P. Bến Nghé, Q.1 |
District + city only | *** Q.1, TP.HCM |
| Date of birth | 1990-05-28 |
Year only | 1990-**-** |
| ID photo URL | https://s3.../front.jpg |
Whole URL hidden | [CCCD photo - request unmask] |
5.5.2 API Gateway middleware (Go)¶
// pkg/masking/middleware.go
package masking
import (
"encoding/json"
"net/http"
"regexp"
"strings"
)
// Tag-based masking config
// Usage: tag struct field với mask info
type Customer struct {
ID string `json:"id"`
Name string `json:"name" mask:"name,scope=pii.unmask.name"`
Phone string `json:"phone" mask:"phone,scope=pii.unmask.phone"`
Email string `json:"email" mask:"email,scope=pii.unmask.email"`
CCCD string `json:"cccd" mask:"cccd,scope=pii.unmask.cccd"`
BankAcc string `json:"bank_acc" mask:"bank,scope=pii.unmask.bank_account"`
Address string `json:"address" mask:"address,scope=pii.unmask.address_full"`
}
// Middleware applied AFTER service returns response, BEFORE serialize
func MaskingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Wrap ResponseWriter to capture
rec := newResponseRecorder(w)
next.ServeHTTP(rec, r)
// Parse response if JSON
if !strings.Contains(rec.Header().Get("Content-Type"), "application/json") {
rec.Flush()
return
}
scopes := scopesFromContext(r.Context())
var body interface{}
if err := json.Unmarshal(rec.body, &body); err != nil {
rec.Flush()
return
}
// Walk tree, apply masking based on struct tags
masked := applyMasking(body, scopes)
// Audit each unmasked field access
if rec.metadata.UnmaskedFields != nil {
audit.LogPIIAccess(r.Context(), audit.PIIAccess{
ActorID: userFromContext(r.Context()),
Fields: rec.metadata.UnmaskedFields,
Subject: rec.metadata.Subject,
RequestID: requestIDFromContext(r.Context()),
})
}
out, _ := json.Marshal(masked)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(rec.statusCode)
w.Write(out)
})
}
5.5.3 Field-level masker functions¶
// pkg/masking/maskers.go
var maskers = map[string]MaskerFunc{
"phone": maskPhone,
"email": maskEmail,
"cccd": maskCCCD,
"bank": maskBank,
"name": maskName,
"address": maskAddress,
}
type MaskerFunc func(plain string) string
func maskPhone(plain string) string {
// Normalize first
digits := digitsOnly(plain)
if len(digits) < 7 { return "***" }
// Vietnam phone format
if strings.HasPrefix(plain, "+84") || strings.HasPrefix(plain, "0") {
// Show first 3 (0912 or +84) + last 3
return digits[:3] + strings.Repeat("*", len(digits)-6) + digits[len(digits)-3:]
}
// International: first 4 + last 3
return plain[:4] + strings.Repeat("*", len(plain)-7) + plain[len(plain)-3:]
}
func maskEmail(plain string) string {
parts := strings.SplitN(plain, "@", 2)
if len(parts) != 2 { return "***@***" }
local := parts[0]
if len(local) <= 3 {
return strings.Repeat("*", len(local)) + "@" + parts[1]
}
return local[:3] + strings.Repeat("*", len(local)-3) + "@" + parts[1]
}
func maskCCCD(plain string) string {
digits := digitsOnly(plain)
if len(digits) != 12 { return "************" }
return digits[:4] + "****" + digits[8:]
}
func maskBank(plain string) string {
if len(plain) < 8 { return "********" }
return plain[:4] + strings.Repeat("*", len(plain)-8) + plain[len(plain)-4:]
}
5.5.4 Service-side helper (selective unmask)¶
Đôi khi service muốn explicitly return unmasked data (vd internal service call). Dùng wrapper:
// Trong handler
func (h *Handler) GetCustomer(c *gin.Context) {
customer := h.repo.Get(c.Param("id"))
// Default behavior: middleware masks based on scope
c.JSON(200, customer)
}
// Special case: financial reconciliation cron — bypass masking
func (h *Handler) RunReconciliation(ctx context.Context) {
customers := h.repo.ListAll()
// Mark context to skip masking
ctx = masking.SkipMaskingContext(ctx, "reason:reconciliation_job")
// Audit log records skip with reason
audit.LogMaskSkip(ctx, customers)
// Process with full data
for _, c := range customers {
h.reconcile(c.BankAccount, ...)
}
}
5.5.5 Tokenization vs Masking · Decision tree¶
Is the field used for:
│
├─ Display only to certain roles?
│ └─► MASKING (reversible, server-side based on scope)
│
├─ PCI-DSS compliance (credit card)?
│ └─► TOKENIZATION (vault lookup, no plaintext in app DB)
│
├─ Search by exact value (vd lookup by phone)?
│ └─► HASHED INDEX + MASKED VALUE (hash for lookup, masked for display)
│
├─ Aggregation (count, sum)?
│ └─► No masking needed (no PII exposed)
│
└─ Used in expression (vd CCCD as login)?
└─► HASHED with pepper (one-way, store hash only)
5.5.6 Performance considerations¶
- Mask functions = pure functions, no I/O → ~100ns per field
- Reflection overhead: cache field tags per type (lazy init)
- Total overhead: ~10-50µs per response (negligible vs 50-500ms total latency)
- Benchmark:
go test -bench=BenchmarkMaskCustomer
5.5.7 Multi-country masking patterns¶
Mỗi country có rules riêng:
var countryMaskingProfile = map[string]Profile{
"VN": {PhoneRule: "vn_format", IDField: "cccd", IDLength: 12},
"US": {PhoneRule: "intl_format", IDField: "ssn", IDLength: 9},
"CN": {PhoneRule: "intl_format", IDField: "id_card_cn", IDLength: 18},
"SG": {PhoneRule: "intl_format", IDField: "nric", IDLength: 9},
}
Khi user country VN truy cập customer country US, áp dụng US masking profile (vd SSN format 123-**-6789).
6. Data lifecycle¶
6.1 Creation¶
- Validate input (sanitize, length limits)
- Classify level → apply storage controls
- Audit log: resource_created
6.2 Access¶
- Authenticate + authorize (scope check)
- Apply masking based on role
- Audit log: pii.viewed (for L3/L4 access)
6.3 Modification¶
- Audit log: before + after snapshot
- Encryption re-applied if PII field changed
6.4 Deletion / Anonymization¶
User-requested deletion (PDPA):
1. Verify user identity (re-auth, fresh token)
2. Mark account deletion_pending (30-day grace period for rollback)
3. After 30 days, automated process:
- PII fields → anonymized (vd Nguyễn Văn A → deleted_user_<uuid>)
- Order history retained (anonymized customer_id)
- Audit logs retained (PII anonymized in actor_name)
- Photos: customer-uploaded deleted, agent-uploaded retained anonymized
- Bank accounts deleted
4. Audit log: account.deletion.completed
5. Notify user via email if reachable
System-driven retention expiry: - Order data > 5 years → archive to cold storage, then delete after legal review - Event log > 90 days → MongoDB TTL auto-delete - Quality photos > 2 years → archive
7. Vulnerability management¶
7.1 Dependency scanning¶
govulncheckin CI per servicenpm auditfor frontend (no critical allowed)- Trivy for container images
- Alert: any HIGH/CRITICAL → block deploy
7.2 Secret scanning¶
- Gitleaks pre-commit hook (developer machine)
- Gitleaks in CI (PR check)
- TruffleHog for repos history scan (monthly)
7.3 Security testing¶
- Static analysis: SonarCloud (weekly)
- DAST: OWASP ZAP scan (weekly staging)
- Pen test: annually + before major release
- Bug bounty: planned for GA (Phase 2)
8. Compliance checklist¶
8.1 PDPA Việt Nam¶
- Privacy policy published, customer accept on signup
- Consent recorded (audit_log entry on signup)
- Right to information: customer can see their data via "Hồ sơ"
- Right to erasure: account deletion flow (30-day grace)
- Right to correction: edit profile in app
- Data minimization: only collect what's needed
- Purpose limitation: each field has documented use
- Security: encryption + access controls (this doc)
- Breach notification: incident response within 72h
- Cross-border transfer: only to providers in approved list (Cloudflare, AWS, MongoDB Atlas)
8.2 Tax + Accounting (Việt Nam)¶
- Financial transactions retained 10 years
- Invoice records retained 10 years
- Audit log for financial operations
- VAT calculation correct
- Reports export-able for tax filing
8.3 PCI DSS (Payment Card Data)¶
SMP does NOT store, process, or transmit card data:
- Payment handled entirely by inside (payment partner)
- SMP only stores payment_intent_id reference
- No PAN, CVV, magstripe ever touches SMP systems
- → SMP is out of PCI scope
If we ever need to take cards directly (future), follow PCI DSS Level 4 minimum.
8.5 Multi-jurisdiction compliance (v4.0)¶
Khi expand global, mỗi country/region có luật riêng. Bảng so sánh + checklist mapping per region.
8.5.1 Regulation comparison matrix¶
| Aspect | PDPA VN | GDPR EU | CPRA US-CA | PIPL CN |
|---|---|---|---|---|
| Effective | 1/7/2023 | 25/5/2018 | 1/1/2023 | 1/11/2021 |
| Data localization | Optional (recommended) | Within EU/EEA | None | Mandatory for CN citizen data |
| Cross-border transfer | Approval for some categories | SCC / BCR / adequacy decision | Notice to consumer | CAC security assessment required |
| Breach notification (authority) | 72h | 72h | "without unreasonable delay" | Immediate + 24h for high-risk |
| Breach notification (users) | Without delay | 72h if high risk | "without unreasonable delay" | When required by authority |
| Right to erasure | 30 days | 30 days (1 month) | 45 days | 30 days |
| Right to data portability | Yes (7 days) | Yes (30 days) | Yes (45 days) | Yes |
| Consent | Specific + informed | Specific + informed + revocable | Opt-out of sale | Specific + revocable |
| DPO mandate | Required if processing 10k+ records | Required if large-scale or sensitive | Not mandatory | Required for large processors |
| Penalty (max) | 5% revenue | 4% global revenue or €20M | $7.5k per intentional violation | 5% revenue or ¥50M |
| Children data (special protection) | < 16 | < 16 (state varies 13-16) | < 13 + opt-in 13-16 | < 14 |
| Sensitive PII categories | health, race, religion, biometric | + political opinion, sexual orientation, genetic | + immigration, geolocation | + medical, financial, biometric, religious |
8.5.2 Per-country checklist (mappable to deployment)¶
PDPA Việt Nam (default · v3.x baseline)¶
- ✅ See section 8.1 above
- Specific: register processing activities với Cục An toàn thông tin
- Specific: appoint DPO if processing sensitive data > 10k records
- Specific: data subject requests via email/in-app form
GDPR (EU - future, when launch)¶
- Lawful basis documentation (consent vs legitimate interest vs contract)
- DPIA (Data Protection Impact Assessment) cho high-risk processing
- Records of Processing Activities (ROPA) - Article 30
- Designate EU representative if no establishment in EU
- Cookie consent banner (no pre-checked boxes)
- "Privacy by design and default" engineering principles
- 72h breach notification with specific format to relevant DPA
- Cross-border: SCC (Standard Contractual Clauses) cho non-adequate countries
CPRA (California, US)¶
- "Do Not Sell or Share My Personal Information" link prominently displayed
- Opt-out preference signal (Global Privacy Control - GPC) honored
- Sensitive PII opt-in (not opt-out default)
- Annual privacy notice update
- Consumer request portal: access, deletion, correction, opt-out
- Service provider contracts must include CPRA terms
- 45-day response window cho all requests
PIPL (China)¶
- Critical: data of CN citizens MUST stored in CN
- Cross-border export requires CAC (Cyberspace Administration of China) security assessment if:
- Transferring sensitive PII
- Processing > 1M CN individuals' data
- Critical Information Infrastructure Operator (CIIO)
- Consent must be "separate consent" for sensitive data (not bundled)
- Local DPO + local representative required
- Audit logs accessible to authorities upon request
- No data export without explicit consent + assessment
8.5.3 Deployment implications¶
| Action | VN | EU | US (CA) | CN |
|---|---|---|---|---|
| Where data stored | SG or VN | Frankfurt (eu-central-1) | Virginia (us-east-1) | CN-only cluster (Beijing) |
| Where backups | Same region | Same region (EU) | Same region (US) | CN-only |
| Cross-region transfer | Yes with conditions | SCC required | Notice | CAC assessment + consent |
| User consent flow | Standard checkbox | Granular (per purpose) | Opt-out for sale | Separate per sensitive category |
| Marketing/analytics tracking | Opt-in | Opt-in | Opt-out | Opt-in + separate consent |
Implementation pattern (xem Doc 02 section 11.5 sharding strategy):
- smp-china cluster fully isolated, no cross-region replication
- smp-eu cluster với encryption keys managed in EU
- smp-us cluster với CPRA-specific UI
8.5.4 DPO + Contact info per region¶
Mỗi region phải có DPO + DPA contact:
# config/dpo-contacts.yaml
dpo_contacts:
VN:
dpo_name: "Nguyễn Văn DPO"
email: "dpo.vn@smp.vn"
address: "HCMC, Vietnam"
dpa: "Cục An toàn thông tin - Bộ TT&TT"
EU:
dpo_name: "TBD when launch"
representative: "TBD"
email: "dpo.eu@smp.vn"
dpa_per_country:
DE: "BfDI (Bundesbeauftragte für den Datenschutz)"
FR: "CNIL"
US-CA:
privacy_officer: "TBD"
email: "privacy.us@smp.vn"
agency: "California Privacy Protection Agency (CPPA)"
CN:
dpo_name: "TBD when launch"
local_rep: "TBD"
email: "dpo.cn@smp.cn"
authority: "CAC (Cyberspace Administration of China)"
8.5.5 Compliance review cadence¶
| Review | Frequency | Owner |
|---|---|---|
| Privacy policy audit | Annual + on major feature change | Legal + DPO |
| Compliance gap analysis per region | Quarterly | DPO + Security |
| Vendor DPA review | Annual | Legal |
| Penetration test (per region) | Annual | Security + external |
| Cross-border transfer documentation | Quarterly | DPO |
| Employee training (compliance) | Annual | HR + DPO |
| Incident response drill (breach simulation) | Semi-annual | Security + Ops |
9. Vendor risk¶
External services with access to data:
| Vendor | Data shared | Level | Agreement |
|---|---|---|---|
| Cloudflare | Web traffic (CDN, WAF) | L1-L2 (metadata) | DPA signed |
| AWS (compute, S3) | All data (encrypted) | L1-L4 | DPA + BAA |
| MongoDB Atlas | smp_events, smp_audit | L1-L2 | DPA |
| Twilio | Phone numbers (SMS) | L3 | DPA |
| SendGrid | Email addresses | L3 | DPA |
| Sentry | Error traces (no PII) | L1 | DPA, scrub config |
| inside | All customer + payment | L3-L4 | Internal · same group |
| wms | Material refs | L1 | Internal |
Annual vendor review.
10. Incident response (data breach)¶
If suspected PII leak:
- 0-1h: contain (revoke keys, isolate systems)
- 1-4h: assess scope (how much data, how many users)
- 4-24h: notify internal stakeholders (legal, exec)
- 24-72h (PDPA): notify Ministry of Information & Communications if "significant risk"
- Within 72h: notify affected users with:
- What data was exposed
- When and how
- What we're doing
- What they should do
- Post: post-mortem, regulatory cooperation, customer support
Maintain incident-response-plan.md separate doc with detailed playbook.
11. Training¶
All engineers complete (annual): - Secure coding course (2h) - This data classification doc - PDPA training (1h) - Incident response drill (table-top, 1x/year)
Track completion in HR system.
12. Self-audit¶
Quarterly review: - [ ] All L3/L4 fields encrypted at rest - [ ] Key rotation up to date - [ ] Access logs reviewed for anomalies - [ ] Vulnerability scan results addressed - [ ] Vendor DPAs current - [ ] Sample 10 PII access events → verify legitimate - [ ] Document any gaps + assign owners