Skip to main content

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.

STRIDEThreatWhereControlsResidual risk
SpoofingCredential stuffing or phishing of admin accountsLoginargon2id, breached-password check, rate limits per account and IP, mandatory TOTP for admin roles, SSO, login anomaly alertsMedium (phishing of TOTP); move admins to passkeys later
SpoofingBuddy punching (one employee punching for another)Mobile, kioskDevice binding, face verification with liveness, geo-fence, selfie evidence, manager review of unverified punchesLow–medium
SpoofingFake biometric device posting punchesIngestRegistered serials only, per-device secret, optional IP allow-list, quarantine of unknown serialsMedium (weak device protocols); compensated by anomaly checks
SpoofingForged webhook to a client, or forged Razorpay webhook to usWebhooksHMAC SHA-256 signatures with timestamps; Razorpay signature verificationLow
TamperingEmployee edits own attendance or salary via API manipulationAPIServer-side authorisation on every mutation (permission + scope), zod validation, no client-trusted fields (tenant, employee, amounts derived server-side)Low
TamperingPayroll changed after approvalPAYImmutable locked runs (snapshots), segregation of duties, audit with before/after hashes, Object Lock on payslips and statutory filesLow
TamperingGPS spoofing (mock location apps)MobileAndroid mock-location detection, iOS simulated-location flag, velocity checks, Wi-Fi/BSSID and QR fallbacks, flags rather than silent acceptanceMedium (rooted devices)
Repudiation"I never approved that payroll / leave / bank change"AllAppend-only audit log (actor, session, IP, device, before/after hash), tenant-exportable; notifications on sensitive changes to the affected employeeLow
Information disclosureCross-tenant data leak in a pooled cellApp, DB, cache, storageTenant context from host/token only, Prisma extension with set_config, forced RLS, composite FKs, tenant-prefixed keys, isolation test suite, CrossTenantGuard alertLow (defence in depth)
Information disclosureSalary or ID numbers visible to colleagues or managersAppField-level protection (pay.compensation.read, emp.sensitive.read), masking in lists and exports, scope checksLow
Information disclosureSensitive data in logs, traces, error reportsObservabilityRedaction at logger, collector and Sentry; weekly log sampling (observability)Low–medium
Information disclosurePayslips or files reachable by guessed URLS3Private buckets, presigned URLs (5–15 min), object keys with random UUIDs, prefix-scoped signingLow
Information disclosureSupport staff browsing tenant dataControl planeNo standing access; consented, time-boxed, audited break-glass (administration)Low
Denial of serviceNoisy tenant at month-end starves othersCellPer-tenant rate limits, BullMQ group concurrency, quotas, cells cap the blast radiusMedium (until more cells)
Denial of serviceVolumetric or L7 attacksEdgeCloudFront + AWS WAF managed rules, rate-based rules, Shield StandardMedium
Elevation of privilegeTenant admin grants self a platform capabilityIAMPlatform and tenant roles in separate systems and databases; platform tokens issued only by the control plane IdPLow
Elevation of privilegeCustom role abuse (an HR user gives themselves payroll approval)IAMiam.role.manage restricted; SoD rules checked on assignment; role changes notify org_owner; you cannot edit roles you holdLow–medium
Elevation of privilegeKiosk device used to open admin screensMobileKiosk token is punch-only and location-scoped; the app has no navigation outside kiosk mode with that tokenLow
Elevation of privilegeSSRF via webhooks or integrations reaching the metadata serviceWorkersURL allow rules, private-range blocking after DNS resolution, IMDSv2 only, egress through NATLow

The model is reviewed at each release planning (R1–R4) and whenever a new integration or data class is added.

2. Data classification​

ClassExamplesStorage controlsAccessMasking
C4 RestrictedAadhaar number, PAN, bank account number, face templates/embeddings, passwords (hashes), MFA secrets, connector secretsApplication-level envelope encryption + RDS/S3 encryptionExplicit permission (emp.sensitive.read, pay.bank_file.generate); every read auditedAlways masked by default (Aadhaar last 4, PAN last 4, account last 4)
C3 ConfidentialSalary, CTC, payslips, tax declarations, loans, F&F, health/medical leave reasons, punch selfies, precise GPS trails, disciplinary notesRDS/S3 encryption (KMS), Object Lock for locked payroll artefactsPermission + scope (pay.compensation.read, ffm.location.read)Masked in lists and exports unless the permission is held
C2 InternalEmployee directory (name, designation, work email, work phone), attendance status, leave dates, org structureRDS encryptionTenant users per scope—
C1 PublicTenant branding, public job titles in the company code lookup—Anyone—
PlatformTenant registry, billing, usage, audit of platform actionsControl-plane DBPlatform rolesBilling 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​

LayerMechanism
In transitTLS 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 restKMS-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 in tenant_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; *_last4 for 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​

ControlDesign
Invite activation, no default passwordsEvery 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 storageargon2id (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 policyTyped 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)
ResetSelf-service reset link (30 min, single use); admins can trigger a reset link but cannot set a password. All sessions revoked on reset
MFATOTP 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
SSOOIDC (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 loginCompany 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 bindingFirst 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
KioskA 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 lockoutProgressive 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 by roles_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=Lax cookie 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 areaSpyrl control
V1 ArchitectureThreat model per release; module boundaries enforced in CI; security review for new integrations
V2 AuthenticationSection 5
V3 SessionsSection 6
V4 Access controlDeny 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 encodingzod 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 CryptographySection 3; only vetted libraries (Node crypto, argon2)
V7 Errors and loggingGeneric error messages to clients with a request ID; structured logs with redaction
V8 Data protectionClassification-driven masking; Cache-Control: no-store on sensitive responses; no sensitive data in URLs
V9 CommunicationsTLS everywhere, HSTS
V10 Malicious codeDependency pinning, lockfiles, Renovate, provenance and signatures on images
V11 Business logicPayroll and leave invariants enforced server-side (property-based tests); SoD rules; idempotency keys on payments and punches
V12 FilesSection 8
V13 APIOAuth scopes, rate limits, OpenAPI-validated requests, no mass assignment (explicit DTOs)
V14 ConfigurationHardened 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​

ControlDetail
Upload pathBrowser/app uploads directly to S3 with a presigned POST limited to one key under t/<tenant_id>/<module>/, content-length range and content type
QuarantineObjects 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 checksAllow-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)
ProcessingImages 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
DownloadPresigned GET with 5–15 min expiry and Content-Disposition: attachment; served from a separate domain to avoid same-origin script risk
ImportsParsed, 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).

  1. RLS coverage: a query on information_schema asserts that every table with a tenant_id column has RLS enabled, FORCE ROW LEVEL SECURITY, and a policy using app.tenant_id. New tables without a policy fail the build.
  2. Role check: the application DB role has NOBYPASSRLS and does not own tables.
  3. 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.
  4. Missing-context test: queries without app.tenant_id set return zero rows (the policy fails closed), not all rows.
  5. API tests: for each endpoint, a token for tenant B with IDs from tenant A gets 404.
  6. Cache, queue and storage: keys and presigned URLs contain the tenant prefix; a job whose payload tenant differs from the verified context is rejected.
  7. 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 CrossTenantGuard alerts on any mismatch.

10. Audit logging​

  • Append-only audit_event table 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 INSERT but not UPDATE or DELETE audit 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​

SourceToolingSLA to fix (from discovery)
Code (SAST)CodeQL or Semgrep on every PRCritical 48 h, high 7 days, medium 30 days, low next quarter
DependenciesRenovate + OSV/npm audit in CI (maintenance)Same as above, by CVSS adjusted for reachability
ContainersTrivy on build and weekly on running digestsSame
Cloud configurationAWS Security Hub (foundational best practices), GuardDuty, IAM Access Analyzer (Stage B; a subset in Stage A)High 7 days
DASTOWASP ZAP baseline on staging per release; authenticated full scan monthlyHigh 7 days
External reportssecurity.txt and a responsible-disclosure page; a bug bounty after the first pentest cycleAcknowledge in 2 business days

Exceptions need a written risk acceptance by the security lead with an expiry date.

12. Penetration testing cadence​

WhenScopeBy
Before MVP GA (R1)Web, API, mobile, tenant isolation, auth flowsExternal CERT-In empanelled firm (many Indian enterprise buyers ask for this)
Annually after thatFull scope + new modulesExternal firm, rotated every 2–3 years
Before each major integration surface ships (public API in R2, ingest gateway in R2, FFM tracking in R3)TargetedExternal or a qualified internal reviewer
Enterprise customer requestsTheir own tests on a dedicated sandbox tenant, under a test agreementCustomer

Findings follow the vulnerability SLAs above; a summary letter is shared with customers under NDA.

13. Incident response​

StepSecurity-specific detail
TriageSeverity uses the levels in backup and DR; any confirmed personal-data breach is at least SEV2, cross-tenant exposure is SEV1
EvidencePreserve logs, snapshots and the damaged resources before changing them; keep a timeline in the incident document
CERT-InReport 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
DPDPAs 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
ContractsEnterprise DPAs may set shorter notification times (for example 24 h); the control plane stores each tenant's contractual notification time
CommunicationStatus page and tenant notices are written by the communications lead and reviewed by the security lead; no speculation about cause before evidence
TabletopOne security tabletop exercise per half-year (for example "a support engineer's laptop is stolen", "a webhook secret leaked")