Last updated: March 2026
Armour Care is a wellbeing survey platform deployed as a Progressive Web App (PWA) at armourcare.uk. This document describes the security measures in place to protect user data, admin access, and the application infrastructure.
All passwords (user and admin) are hashed using bcrypt with a cost factor of 12 rounds before storage. Bcrypt is an industry-standard adaptive hashing algorithm specifically designed for passwords — it is intentionally slow, making brute-force and rainbow table attacks impractical.
- New accounts: Passwords are hashed with bcrypt at signup/creation time.
- Legacy accounts: Any accounts created before the bcrypt migration used SHA-256 hashing. These are automatically upgraded to bcrypt on the next successful login — the user does not need to take any action.
- No plaintext storage: Passwords are never stored or logged in plaintext. The
passwordHashfield is stripped from all API responses before being sent to the client.
All authenticated API requests use JWT bearer tokens for session management.
- Token issuance: A signed JWT is returned on successful login or signup.
- Token lifetime: Tokens expire after 7 days, after which the user must log in again.
- Token signing: Tokens are signed with a 512-bit random secret generated on first server start and persisted in a restricted file (
.jwt-secret, mode0600). - Token verification: Every admin API endpoint extracts the token from the
Authorization: Bearer <token>header and verifies it before processing the request. - Token payload: Contains only the user/admin ID and role (
useroradmin). No sensitive data is stored in the token.
Login endpoints are protected by an in-memory rate limiter to prevent brute-force attacks.
- Window: 1 minute
- Max attempts: 10 per IP address per window
- Scope: Separate rate limit buckets for user login (
/api/login) and admin login (/api/admin/login) - Response: Returns HTTP 429 (Too Many Requests) when the limit is exceeded
- Cleanup: Stale rate limit entries are automatically purged every 5 minutes
The system has three access levels:
| Role | Access |
|---|---|
| User | Can submit surveys, view own team stats, manage push notification subscription |
| Sub-Admin | Can view dashboard, users, and submissions for assigned teams. Access to Question Builder and Notifications is controlled by per-admin permission flags |
| Master Admin | Full access to all features including admin management, question editing, and all teams |
Master admins can grant or revoke granular permissions for each sub-admin:
- Question Builder Access — allows viewing and exporting questions (editing is master-only)
- Notifications Access — allows sending notifications and viewing notification history
These permissions are enforced both:
- Server-side: API endpoints check the admin's role and permissions before processing
- Client-side: The admin UI hides navigation items and sections that the admin does not have access to
Sub-admins are assigned to specific teams and can only view/manage:
- Users belonging to their assigned teams
- Submissions from their assigned teams
- Statistics for their assigned teams
Master admins have access to all teams.
All traffic to armourcare.uk is encrypted using TLS (HTTPS), terminated at the Nginx reverse proxy. This ensures:
- Data in transit (passwords, survey responses, tokens) cannot be intercepted
- The server's identity is verified via its SSL certificate
HTTP access is blocked:
- Nginx redirects all HTTP (port 80) requests to HTTPS (port 443) with a
301 Moved Permanentlyresponse - HSTS (HTTP Strict Transport Security) is enabled with a 1-year
max-ageandincludeSubDomains, instructing browsers to always use HTTPS and never attempt an insecure connection - HSTS is set at both the Nginx and Node.js application level
API access is restricted to trusted origins only:
https://armourcare.uk(production)http://localhost:8000(local development)http://127.0.0.1:8000(local development)
Requests from any other origin are rejected. The Authorization header is explicitly allowed for JWT token transmission.
Every response includes the following security headers:
| Header | Value | Purpose |
|---|---|---|
X-Content-Type-Options |
nosniff |
Prevents browsers from MIME-type sniffing |
X-Frame-Options |
DENY |
Prevents the site from being embedded in iframes (clickjacking protection) |
X-XSS-Protection |
1; mode=block |
Enables browser XSS filtering |
Referrer-Policy |
strict-origin-when-cross-origin |
Limits referrer information sent to external sites |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
Disables access to sensitive browser APIs not needed by the app |
All POST/PUT request bodies are limited to 1 MB. Requests exceeding this limit are immediately terminated to prevent memory exhaustion attacks.
- Armour numbers are normalised to uppercase
- Password minimum length is enforced (4 characters)
- Required fields are validated before processing
- JSON parsing errors are caught and return appropriate error responses
Static file serving validates that the resolved file path stays within the application directory. Requests attempting directory traversal (e.g., ../../etc/passwd) are blocked with a 403 Forbidden response.
Data is stored in JSON files on the server filesystem:
| File | Contents | Sensitivity |
|---|---|---|
users.json |
User accounts (armour numbers, teams, bcrypt hashes) | High |
admins.json |
Admin accounts (armour numbers, roles, bcrypt hashes, permissions) | High |
submissions.json |
Monthly survey responses | Medium |
questions.json |
Survey question definitions | Low |
notifications.json |
Notification history | Low |
push-subscriptions.json |
Web Push subscription endpoints | Medium |
auto-notif-config.json |
Automated notification settings | Low |
.jwt-secret |
JWT signing secret (file mode 0600) | Critical |
- Password hashes are never included in API responses
- The JWT secret file has restricted file permissions (
0600— owner read/write only) - VAPID keys can be provided via environment variables to avoid hardcoding in source
Push notifications use the Web Push API with VAPID (Voluntary Application Server Identification) authentication:
- The server identifies itself to push services using a VAPID key pair
- Push subscriptions are stored per-user and deduplicated by endpoint
- Expired or invalid subscriptions (HTTP 410/404) are automatically cleaned up
- VAPID keys can be configured via environment variables (
VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEY)
Session data is stored in the browser's localStorage:
- User app:
armourcare-session(user profile),armourcare-token(JWT) - Admin portal:
armourcare-admin(admin profile),armourcare-admin-token(JWT)
On logout, both the session data and token are cleared from localStorage.
JWT tokens expire after 7 days. After expiry, the user is effectively logged out and must re-authenticate. There is no refresh token mechanism — a full re-login is required.
- The application runs as a systemd service on the VPS, ensuring automatic restart on failure
- Nginx acts as a reverse proxy, handling TLS termination and forwarding to the Node.js app on port 8000
- The Node.js process listens on
0.0.0.0:8000but is only accessible externally through Nginx (port 443) - Data files (
users.json,admins.json, etc.) are not overwritten during deployment — only application code is updated - Pre-deploy backups: The deploy script automatically backs up
questions.json,users.json, andadmins.jsonlocally with a timestamp before each deployment - Post-deploy sync: After deployment, the server's copies of these data files are pulled to the local machine (server is the source of truth)
The application is configured to prevent indexing by search engines:
- Meta tags: Both
index.htmlandadmin.htmlinclude<meta name="robots" content="noindex, nofollow" />to instruct compliant crawlers not to index or follow links - robots.txt: A
robots.txtfile at the site root disallows all crawlers from all paths (Disallow: /)
This ensures the app and admin portal do not appear in search engine results.
| Area | Current State | Potential Improvement |
|---|---|---|
| Database | JSON files on disk | Migrate to SQLite or PostgreSQL for better concurrency and backup |
| CSRF protection | Not implemented | Add CSRF tokens for state-changing requests |
| Content Security Policy | Not implemented | Add CSP header to prevent XSS via inline scripts |
| Session storage | localStorage | Consider httpOnly cookies for token storage (immune to XSS) |
| Password policy | 4-character minimum | Enforce stronger passwords (length, complexity) |
| Audit logging | Basic console logging | Add structured audit log for admin actions |
| 2FA | Not implemented | Add TOTP-based two-factor authentication for admin accounts |
| Backup | Manual export/restore | Implement automated encrypted backups |
For security concerns, contact the system administrator.