Security Overview
Last updated: 24 April 2026
Udyamik handles student records, parent contact information, school financials and payment events. Security is not a feature we add on top — it is the floor of the platform. This page documents our posture at the level of detail an RFP evaluator, a school principal, or a curious engineer should find useful. Nothing here is marketing; each bullet maps to concrete code, a migration, or a configuration file in our repository.
Hosting & network
- Primary infrastructure is a single hardened VPS in Mumbai, India (Hostinger region). Data residency stays within Indian jurisdiction for all primary storage and nightly backups.
- Operating system: Debian 12 with unattended security updates (
unattended-upgrades) enabled. - Reverse proxy: Caddy terminating TLS 1.3 with Let's Encrypt certificates (automatic renewal).
- Firewall:
ufwallowlist — only ports 80 (redirects to 443), 443, and a non-standard SSH port accessible via SSH-key authentication. Password SSH disabled. - Intrusion protection:
fail2banon SSH + Caddy.
Encryption
- In transit: TLS 1.3 only. HSTS is enabled with a 1-year max-age and
includeSubDomains. We do not serve any unencrypted variant of the app. - At rest — tenant secrets: Razorpay keys, Meta WhatsApp access tokens, Meta app secrets, Interakt API keys, and similar are encrypted at rest using AES-GCM with a 128-bit authentication tag. Our
SecretBoxprimitive prepends a fresh 12-byte IV to each ciphertext, and the master key is supplied via thePAYMENT_CONFIG_ENCRYPTION_KEYenvironment variable (32 bytes, base64-encoded). A boot-time canary encrypts and decrypts a fixed plaintext before the application accepts traffic — a key mismatch stops the JVM from starting (SETTINGS_003). - At rest — PINs: Device-bound PINs are hashed with Argon2id (memory-hard, side-channel-resistant) and keyed by
(user_id, device_id)so a leaked hash without the device id is useless. - At rest — recipients: We never store raw phone numbers or email addresses in our communication logs. The
communication_eventstable keeps only a SHA-256 hash of the normalised recipient; the plaintext is held in memory for the duration of a single provider call only. - Backups: Nightly
pg_dumpencrypted at rest withage(AES-256) before transfer to off-site storage. The matching private key is held offline, never on the server. See the dedicated Backups & disaster recovery subsection below for the full pipeline.
Backups & disaster recovery
Nightly encrypted backups are a first-class feature, not an afterthought. The pipeline is documented, tested, and engineered so we cannot lose your data even if we wanted to. Every step below maps to a committed file in our deploy repository — if you are an RFP evaluator who wants to see the actual shell script, ask and we will share it.
- Encryption algorithm: age — a modern, audited, standards-based file-encryption format. We use age in public-key recipient mode: the production server holds only the public key (the
age1...recipient string), which means the server can produce ciphertext but cannot decrypt its own output. Under the hood, age uses X25519 + ChaCha20-Poly1305 (authenticated AES-equivalent at 256-bit security). - Dump format:
pg_dump --format=custom(the compressed, parallel-restore-capable Postgres format). Theudyamik_backupdatabase role is dedicated to backups and hasBYPASSRLSso it can capture every tenant's rows into the dump — the app's own database role deliberately does not haveBYPASSRLS, so a forgotten tenant-context in application code fails closed rather than leaking across tenants. - Schedule:
systemdtimer fires at 02:30 IST every day — the quietest window (no teacher attendance, no parent payments, no admin activity). If the VPS was off at 02:30,Persistent=truecatches up on next boot. - Retention: 14 days on-box, with a roadmap to add 90-day offsite retention on a Hostinger Storage Box or Backblaze B2. Old dumps are pruned automatically; disk pressure never silently truncates backups.
- Key management: The age private key never touches the production server. It lives on the founder's laptop (mode
0600), with copies in 1Password and on an offline USB cold backup. Key rotation is supported via a dual-recipient overlap window — both old and new public keys encrypt during the rotation period so no retained dump becomes unreadable. - Restore path: The entire decrypt-and-restore flow is a single shell pipe that never writes plaintext to disk:
age --decrypt --identity ~/udyamik-age.key YYYY-MM-DD.dump.age | pg_restore -d edusuite. We run this as a weekly rehearsal against a scratch database so we never find out the key is wrong during a real incident. - Recovery objective: RTO < 4 hours end-to-end for a full restore from the most recent dump. RPO is 24 hours minus however long ago the last nightly fired; typically < 20 hours.
- No proprietary lock-in: If we vanished tomorrow, any customer with the private key could decrypt their dump and stand the database up on any Postgres 16 instance anywhere. age is open-source and audited; the dump format is standard Postgres.
Authentication
- No passwords. By design. We authenticate users through a one-time password (OTP) sent over WhatsApp or email, followed by a JWT SESSION token. For quick re-auth on trusted devices, users can set an optional 4–6 digit PIN that is Argon2id-hashed server-side and keyed to a specific device id.
- Rate limiting on OTP request, PIN verify and webhook endpoints via Bucket4j. PIN verify locks after 5 consecutive failures per device (with a 15-minute cool-down) and tracks a 20-per-hour per-user ceiling.
- Refresh-token rotation. Refresh tokens are device-bound (
device_idclaim) and rotated on every use — a replayed refresh token is detected and rejected. - 30-day idle invalidation. A trusted device that has not been used in 30 days is downgraded; re-auth requires a fresh OTP.
Access control
- Multi-tenant isolation via PostgreSQL Row-Level Security (RLS). Every tenant-scoped business table carries a
tenant_idcolumn and an RLS policy that filters oncurrent_setting('app.current_tenant'). The application writes this session variable before each transaction — a SQL query cannot leak cross-tenant data even if application code has a bug. - Code-constant permissions. Roles are fixed enums per App, permissions are
public static final Stringkeys in Java — there is no database column containing a CSV of permissions and no LIKE-scan authorisation check to fuzz past. - Platform-role catalog. Cross-tenant “super-admin”-style actions live behind a platform-role layer (
SUPER_ADMIN,CUSTOMER_SUPPORT,SALES_PERSON) with per-role permission grants enumerated in code. No one is a super-admin by accident. - Principle of least privilege for service accounts (Razorpay webhook, Meta webhook) — each uses a dedicated path, HMAC-verified, never a shared admin key.
Audit logging
- Every sensitive mutation — fee waivers, discount grants, invoice corrections, role changes, subscription changes, entitlement overrides, report publishes, platform-role grants/revokes, tenant status flips — writes an immutable
public.audit_logrow with a before/after JSON diff, the actor user id, the actor IP, and the device id. - Audit rows are written through a Spring
@EventListenerin aREQUIRES_NEWtransaction so the audit record lands even if the outer business transaction rolls back — the attempt is always captured. - PII is masked at write time — phone numbers appear as
••••••••9010, APAAR IDs as••••••••9010, and PIN / JWT tokens are never written to logs.
PII minimisation
- 10-digit Indian mobile numbers stored without country code (canonical form). The
+91/91prefix is added at the provider boundary only; see the Privacy Policy §2. - APAAR IDs are validated against the Verhoeff checksum at write time, masked in logs, and never accepted as a login identifier.
- Raw card / UPI credentials are handled entirely by Razorpay's PCI-DSS-compliant checkout — they never touch our servers.
Operations
- All production changes ship through a single branch (
master) with a pre-merge end-to-end smoke script (scripts/e2e-smoke.sh) exercising ~50 HTTP assertions. - Dependencies track Spring Boot 4 LTS, Java 26, PostgreSQL 16 (current LTS). Security advisories are monitored through GitHub Dependabot.
- Secrets live in an environment file (
/etc/udyamik/udyamik.env) readable only by the service user; never committed to Git.
Incident response
- Security issues: security@udyamik.com. We acknowledge within 24 hours.
- Machine-readable vulnerability-disclosure contact:
/.well-known/security.txt(RFC 9116). - If we confirm a personal-data breach, we notify affected data principals and the Data Protection Board of India per DPDP Act §8(6) within the statutory timeframe.
Responsible disclosure
We welcome good-faith security research. Email security@udyamik.com before any public disclosure, with proof-of-concept and reproduction steps. We do not currently run a paid bug-bounty programme, but will publicly credit researchers who report valid issues (on request) and will not take legal action against research carried out in good faith within the scope we define on our security page.
Please do NOT:
- Run automated scanners against production (they generate noise that masks real attackers).
- Test denial-of-service or resource-exhaustion conditions.
- Access data belonging to other customers — a proof-of-concept against a demo tenant you control is always preferable.
- Disclose a vulnerability publicly before we have had a reasonable chance to remediate.
Sub-processors
Current sub-processors receiving personal data as part of operating the Services:
| Sub-processor | Purpose | Location |
|---|---|---|
| Razorpay Software Pvt. Ltd. | Payment processing | India |
| Meta Platforms Ireland Ltd. | WhatsApp Business message delivery (Meta Direct) | EU / global |
| Interakt Marketing Technologies Pvt. Ltd. | WhatsApp Business message delivery (Interakt) | India |
| OpenAI, L.L.C. | Sahayak AI query processing (when enabled) | USA |
| Hostinger International Ltd. | VPS hosting (Mumbai region) | India |
| Let's Encrypt (ISRG) | TLS certificate issuance | Global |
Material changes to this list are notified in advance per our Privacy Policy §12.
Roadmap — what we are actively working on
Published as a commitment to transparency, not a promise. These are current security-track line items from our backlog:
- SOC 2 Type II preparation (targeted for 2027 once customer profile justifies it).
- WAF in front of Caddy (currently weighing ModSecurity vs a managed offering).
- Hardware-backed key storage for the
SecretBoxmaster key (migrating off environment-variable storage). - Regular third-party penetration testing, starting with one engagement pre-GA.
Contact
Security enquiries: security@udyamik.com.
Privacy and DPDP rights: privacy@udyamik.com.
Grievance officer: /legal/grievance.