Security Overview
Spyrl holds the most sensitive data an SME has about its people: salaries, bank accounts, PAN, Aadhaar, face templates and location trails. This page deepens §6 and §7 of the architecture (ADR-006, ADR-007). Permissions are in the RBAC permission matrix; personal-data obligations are in privacy and DPDP.
What we learned from the reference product. Spryple creates every account with the shared default password "Welcome1!" and emails it in plaintext together with the company code and username; its roles documentation is a copy of the login page; face enrolment shows no liveness check; and its store listings say "no data collected" while sharing location (research, §4.5 and §21). Each of these is a control below.
1. Threat model (STRIDE)
Scope: web SPA, mobile app (ESS/MSS/field/kiosk), public API, biometric ingest, app cells, control plane, admin console, third-party providers.
| STRIDE | Threat | Where | Controls | Residual risk |
|---|---|---|---|---|
| Spoofing | Credential stuffing or phishing of admin accounts | Login | argon2id, breached-password check, rate limits per account and IP, mandatory TOTP for admin roles, SSO, login anomaly alerts | Medium (phishing of TOTP); move admins to passkeys later |
| Spoofing | Buddy punching (one employee punching for another) | Mobile, kiosk | Device binding, face verification with liveness, geo-fence, selfie evidence, manager review of unverified punches | Low–medium |
| Spoofing | Fake biometric device posting punches | Ingest | Registered serials only, per-device secret, optional IP allow-list, quarantine of unknown serials | Medium (weak device protocols); compensated by anomaly checks |
| Spoofing | Forged webhook to a client, or forged Razorpay webhook to us | Webhooks | HMAC SHA-256 signatures with timestamps; Razorpay signature verification | Low |
| Tampering | Employee edits own attendance or salary via API manipulation | API | Server-side authorisation on every mutation (permission + scope), zod validation, no client-trusted fields (tenant, employee, amounts derived server-side) | Low |
| Tampering | Payroll changed after approval | PAY | Immutable locked runs (snapshots), segregation of duties, audit with before/after hashes, Object Lock on payslips and statutory files | Low |
| Tampering | GPS spoofing (mock location apps) | Mobile | Android mock-location detection, iOS simulated-location flag, velocity checks, Wi-Fi/BSSID and QR fallbacks, flags rather than silent acceptance | Medium (rooted devices) |
| Repudiation | "I never approved that payroll / leave / bank change" | All | Append-only audit log (actor, session, IP, device, before/after hash), tenant-exportable; notifications on sensitive changes to the affected employee | Low |
| Information disclosure | Cross-tenant data leak in a pooled cell | App, DB, cache, storage | Tenant context from host/token only, Prisma extension with set_config, forced RLS, composite FKs, tenant-prefixed keys, isolation test suite, CrossTenantGuard alert | Low (defence in depth) |
| Information disclosure | Salary or ID numbers visible to colleagues or managers | App | Field-level protection (pay.compensation.read, emp.sensitive.read), masking in lists and exports, scope checks | Low |
| Information disclosure | Sensitive data in logs, traces, error reports | Observability | Redaction at logger, collector and Sentry; weekly log sampling (observability) | Low–medium |
| Information disclosure | Payslips or files reachable by guessed URL | S3 | Private buckets, presigned URLs (5–15 min), object keys with random UUIDs, prefix-scoped signing | Low |
| Information disclosure | Support staff browsing tenant data | Control plane | No standing access; consented, time-boxed, audited break-glass (administration) | Low |
| Denial of service | Noisy tenant at month-end starves others | Cell | Per-tenant rate limits, BullMQ group concurrency, quotas, cells cap the blast radius | Medium (until more cells) |
| Denial of service | Volumetric or L7 attacks | Edge | CloudFront + AWS WAF managed rules, rate-based rules, Shield Standard | Medium |
| Elevation of privilege | Tenant admin grants self a platform capability | IAM | Platform and tenant roles in separate systems and databases; platform tokens issued only by the control plane IdP | Low |
| Elevation of privilege | Custom role abuse (an HR user gives themselves payroll approval) | IAM | iam.role.manage restricted; SoD rules checked on assignment; role changes notify org_owner; you cannot edit roles you hold | Low–medium |
| Elevation of privilege | Kiosk device used to open admin screens | Mobile | Kiosk token is punch-only and location-scoped; the app has no navigation outside kiosk mode with that token | Low |
| Elevation of privilege | SSRF via webhooks or integrations reaching the metadata service | Workers | URL allow rules, private-range blocking after DNS resolution, IMDSv2 only, egress through NAT | Low |
The model is reviewed at each release planning (R1–R4) and whenever a new integration or data class is added.
2. Data classification
| Class | Examples | Storage controls | Access | Masking |
|---|---|---|---|---|
| C4 Restricted | Aadhaar number, PAN, bank account number, face templates/embeddings, passwords (hashes), MFA secrets, connector secrets | Application-level envelope encryption + RDS/S3 encryption | Explicit permission (emp.sensitive.read, pay.bank_file.generate); every read audited | Always masked by default (Aadhaar last 4, PAN last 4, account last 4) |
| C3 Confidential | Salary, CTC, payslips, tax declarations, loans, F&F, health/medical leave reasons, punch selfies, precise GPS trails, disciplinary notes | RDS/S3 encryption (KMS), Object Lock for locked payroll artefacts | Permission + scope (pay.compensation.read, ffm.location.read) | Masked in lists and exports unless the permission is held |
| C2 Internal | Employee directory (name, designation, work email, work phone), attendance status, leave dates, org structure | RDS encryption | Tenant users per scope | — |
| C1 Public | Tenant branding, public job titles in the company code lookup | — | Anyone | — |
| Platform | Tenant registry, billing, usage, audit of platform actions | Control-plane DB | Platform roles | Billing contact details treated as C2 |
Every column in the Prisma schema carries a classification comment (/// @class C4 @purpose payroll) that drives masking, export filtering, log redaction lists and the data dictionary (data model).
3. Encryption
| Layer | Mechanism |
|---|---|
| In transit | TLS 1.2+ (TLS 1.3 preferred) at CloudFront and ALB; TLS to RDS and ElastiCache enforced; HSTS with preload on spyrl.app. Mobile certificate pinning is not used by default (architecture §7) |
| At rest | KMS-encrypted RDS, snapshots, EBS, S3 (SSE-KMS), ElastiCache. One KMS key per cell; dedicated cells can bring a customer-managed key |
| Application-level (C4 fields) | Envelope encryption, below |
3.1 Envelope encryption for Aadhaar, PAN, bank and face templates
- Per-tenant data key (DEK) generated with
GenerateDataKey, stored wrapped intenant_keyring(with a key version). Plaintext DEKs are cached in process memory for at most 5 minutes, never in Redis or logs. - Field format:
version || nonce || ciphertext || tag, with AES-256-GCM and the tenant ID plus column name as additional authenticated data, so a ciphertext copied to another tenant or column fails to decrypt. - Search and uniqueness: a keyed HMAC (
pan_hash) per tenant for duplicate detection and exact-match lookup;*_last4for masked display. No other derived forms. - Rotation: DEKs rotate yearly or on demand (a background job re-encrypts by key version); KMS master keys rotate automatically every year.
- Crypto-shredding: deleting a tenant destroys its wrapped DEKs, making any C4 data left in backups unreadable once backups age out.
- Aadhaar minimisation: collect Aadhaar only when the tenant has a documented need; store encrypted with last 4 for display; never use it as an identifier or a login; never print it on payslips or letters.
- Face templates: store provider template references or encrypted embeddings, not raw enrolment images (architecture §7).
4. Secrets
- AWS Secrets Manager / SSM Parameter Store per environment and cell; nothing in images, repositories, Dokploy environment variables or CI logs. Gitleaks runs in CI and as a pre-commit hook.
- Workloads read secrets with their own IAM role (EC2 instance role in Stage A, ECS task role in Stage B), limited to their cell's paths.
- Rotation every 90 days (architecture §7): RDS credentials through Secrets Manager rotation, provider API keys through a scripted runbook, JWT signing keys through a key set with overlap (new key signs; old keys verify until their tokens expire).
- Tenant-level connector secrets use envelope encryption in the database (integrations).
- Break-glass AWS root and account credentials are held offline by two people with hardware MFA.
5. Authentication
| Control | Design |
|---|---|
| Invite activation, no default passwords | Every account starts with a single-use activation link (72 h, hashed token in DB) by email, SMS or WhatsApp. The user sets their own password or links SSO. Admins can resend but never see or set a password. Bulk import sends invites, not credentials |
| Password storage | argon2id (memory 64 MiB, iterations 3, parallelism 1; benchmarked to about 100–250 ms on production hardware, re-tuned yearly) with a per-hash salt; parameters stored with the hash so they can be upgraded on next login |
| Password policy | Typed per-tenant policy (minimum 10 characters by default, no composition rules forced, breached-password check against a k-anonymity range API or a local list, no periodic forced rotation unless the tenant insists) |
| Reset | Self-service reset link (30 min, single use); admins can trigger a reset link but cannot set a password. All sessions revoked on reset |
| MFA | TOTP with recovery codes. Mandatory for org_owner, hr_admin, finance_admin, payroll_manager and all platform roles; optional for others, enforceable by tenant policy. Step-up MFA for high-risk actions (approve payroll, generate bank file, change bank details, export sensitive data) within the last 15 minutes |
| SSO | OIDC (Google Workspace, Microsoft Entra ID) on Standard+; SAML 2.0 on Enterprise. Just-in-time linking to existing employees by verified email only. Tenant can disable passwords; the owner keeps a password + MFA break-glass login |
| Mobile login | Company code → tenant resolution → username/password or SSO (system browser with PKCE). OTP login by mobile number for shop-floor employees without email, when the tenant enables it |
| Device binding | First login registers the device (key pair in the secure enclave/keystore; the refresh token is bound to it). New devices need line-manager approval when policy enables it (iam.device.approve). Remote sign-out per device |
| Kiosk | A kiosk is enrolled by an admin with a one-time code; it receives a location-scoped, punch-only kiosk token. Employees identify by face (with liveness) or PIN plus selfie. The kiosk token cannot read any data beyond the employee list needed for matching at that location |
| Account lockout | Progressive delays and CAPTCHA after 5 failures, temporary lock after 10 (per account), with a notification to the user; IP-level throttles to resist stuffing without letting attackers lock out whole tenants |
6. Session management
- Access token: JWT, 15 minutes, signed with an asymmetric key (ES256), claims
sub,tid,sid,roles_hash. Permissions are resolved server-side per request (cached byroles_hash), not trusted from the token. - Refresh token: opaque, rotating, stored hashed; reuse of an old refresh token revokes the whole session family (theft detection). Web:
HttpOnly; Secure; SameSite=Laxcookie scoped to the tenant host. Mobile: secure storage, bound to the device key. - Idle timeout per tenant policy (default 30 minutes web, 30 days mobile with biometric app unlock); absolute lifetime 12 hours web, 90 days mobile.
- Users can list and revoke their sessions; admins can revoke any user's sessions (
iam.session.revoke). Role changes, password resets, deactivation and SSO de-provisioning revoke sessions immediately. - CSRF: SameSite cookies plus a double-submit token for cookie-authenticated mutations; the API rejects mutations without the header.
7. Application security controls (OWASP ASVS Level 2)
| ASVS area | Spyrl control |
|---|---|
| V1 Architecture | Threat model per release; module boundaries enforced in CI; security review for new integrations |
| V2 Authentication | Section 5 |
| V3 Sessions | Section 6 |
| V4 Access control | Deny by default; one guard for every route (@Requires('lve.request.approve')) plus a scope resolver; object-level checks in repositories; matrix-driven authorisation tests (testing) |
| V5 Validation and encoding | zod schemas for every input (shared contracts); Prisma parameterised queries, raw SQL only through tagged templates reviewed by two people; React escaping; sandboxed templates (no code execution) for letters and notifications; CSV/Excel export escapes formula characters (=, +, -, @) |
| V6 Cryptography | Section 3; only vetted libraries (Node crypto, argon2) |
| V7 Errors and logging | Generic error messages to clients with a request ID; structured logs with redaction |
| V8 Data protection | Classification-driven masking; Cache-Control: no-store on sensitive responses; no sensitive data in URLs |
| V9 Communications | TLS everywhere, HSTS |
| V10 Malicious code | Dependency pinning, lockfiles, Renovate, provenance and signatures on images |
| V11 Business logic | Payroll and leave invariants enforced server-side (property-based tests); SoD rules; idempotency keys on payments and punches |
| V12 Files | Section 8 |
| V13 API | OAuth scopes, rate limits, OpenAPI-validated requests, no mass assignment (explicit DTOs) |
| V14 Configuration | Hardened distroless images, non-root user, read-only root filesystem, IMDSv2 only, security headers (CSP without unsafe-inline for scripts, frame-ancestors 'none', Referrer-Policy, Permissions-Policy) |
8. File upload security
| Control | Detail |
|---|---|
| Upload path | Browser/app uploads directly to S3 with a presigned POST limited to one key under t/<tenant_id>/<module>/, content-length range and content type |
| Quarantine | Objects land in a quarantine/ prefix; an S3 event triggers a scanning worker (ClamAV, signatures updated daily); only clean files are moved to the final key and become referenceable |
| Type checks | Allow-list per purpose (documents: PDF, JPEG, PNG, HEIC; imports: XLSX, CSV). Magic-byte check, not just extension. Max sizes (documents 10 MB, imports 20 MB, selfies 1 MB after client-side compression) |
| Processing | Images re-encoded (strips EXIF, including GPS, except for punch selfies where location is captured separately); PDFs never rendered server-side in the API process; spreadsheets parsed in workers with limits on rows, columns and formula evaluation disabled |
| Download | Presigned GET with 5–15 min expiry and Content-Disposition: attachment; served from a separate domain to avoid same-origin script risk |
| Imports | Parsed, validated and never executed; macro-enabled formats rejected |
9. Tenant isolation tests
Run in CI on every PR, and nightly against staging with production-like data volume (testing strategy).
- RLS coverage: a query on
information_schemaasserts that every table with atenant_idcolumn has RLS enabled,FORCE ROW LEVEL SECURITY, and a policy usingapp.tenant_id. New tables without a policy fail the build. - Role check: the application DB role has
NOBYPASSRLSand does not own tables. - Repository fuzz: for every repository method, seed tenants A and B; call the method in tenant B's context with A's IDs; assert empty results or not-found, never A's data.
- Missing-context test: queries without
app.tenant_idset return zero rows (the policy fails closed), not all rows. - API tests: for each endpoint, a token for tenant B with IDs from tenant A gets
404. - Cache, queue and storage: keys and presigned URLs contain the tenant prefix; a job whose payload tenant differs from the verified context is rejected.
- Production canary: a synthetic "canary tenant" in each pooled cell holds marker records; a scheduled check confirms no other tenant's API calls can return them, and
CrossTenantGuardalerts on any mismatch.
10. Audit logging
- Append-only
audit_eventtable per cell:tenant_id, actor (user, support session, API client, system job), action (permission key or event name), entity type and ID, before/after hashes (and before/after values for non-sensitive fields), IP, user agent, device ID, request ID, timestamp. - Logged: every create/update/delete on C3/C4 data; every read of C4 data (unmasking); every authorisation deny; logins, MFA events, session revocations; role and permission changes; exports; support sessions; payroll lifecycle transitions; flag and policy changes.
- Tamper evidence: a hash chain per tenant per day; the daily export to an Object Lock bucket includes the chain head.
- The app role can
INSERTbut notUPDATEorDELETEaudit rows (enforced by grants and a trigger). - Tenants can search and export their audit log (
iam.audit.read,iam.audit.export); platform actions on a tenant appear in the same log.
11. Vulnerability management
| Source | Tooling | SLA to fix (from discovery) |
|---|---|---|
| Code (SAST) | CodeQL or Semgrep on every PR | Critical 48 h, high 7 days, medium 30 days, low next quarter |
| Dependencies | Renovate + OSV/npm audit in CI (maintenance) | Same as above, by CVSS adjusted for reachability |
| Containers | Trivy on build and weekly on running digests | Same |
| Cloud configuration | AWS Security Hub (foundational best practices), GuardDuty, IAM Access Analyzer (Stage B; a subset in Stage A) | High 7 days |
| DAST | OWASP ZAP baseline on staging per release; authenticated full scan monthly | High 7 days |
| External reports | security.txt and a responsible-disclosure page; a bug bounty after the first pentest cycle | Acknowledge in 2 business days |
Exceptions need a written risk acceptance by the security lead with an expiry date.
12. Penetration testing cadence
| When | Scope | By |
|---|---|---|
| Before MVP GA (R1) | Web, API, mobile, tenant isolation, auth flows | External CERT-In empanelled firm (many Indian enterprise buyers ask for this) |
| Annually after that | Full scope + new modules | External firm, rotated every 2–3 years |
| Before each major integration surface ships (public API in R2, ingest gateway in R2, FFM tracking in R3) | Targeted | External or a qualified internal reviewer |
| Enterprise customer requests | Their own tests on a dedicated sandbox tenant, under a test agreement | Customer |
Findings follow the vulnerability SLAs above; a summary letter is shared with customers under NDA.
13. Incident response
| Step | Security-specific detail |
|---|---|
| Triage | Severity uses the levels in backup and DR; any confirmed personal-data breach is at least SEV2, cross-tenant exposure is SEV1 |
| Evidence | Preserve logs, snapshots and the damaged resources before changing them; keep a timeline in the incident document |
| CERT-In | Report reportable cyber incidents to CERT-In within 6 hours of noticing them (CERT-In directions of April 2022), and keep ICT logs for 180 days in India (the security log retention in observability). Verify current requirements with counsel |
| DPDP | As a data processor, notify the affected tenants (the data fiduciaries) without delay so they can meet their obligations; support their notification to the Data Protection Board and to data principals (privacy). For Spyrl's own fiduciary data (tenant admin contacts, billing), Spyrl notifies directly |
| Contracts | Enterprise DPAs may set shorter notification times (for example 24 h); the control plane stores each tenant's contractual notification time |
| Communication | Status page and tenant notices are written by the communications lead and reviewed by the security lead; no speculation about cause before evidence |
| Tabletop | One security tabletop exercise per half-year (for example "a support engineer's laptop is stolen", "a webhook secret leaked") |