This document describes how TrustM365 handles sensitive data, what it stores, what it never stores, and how to harden a production deployment.
TrustM365 stores credentials that have read (and optionally write) access to your Microsoft 365 tenant. The primary risk is unauthorised access to the TrustM365 database or .env file, which would expose encrypted tenant credentials. Mitigations are described throughout this document.
TrustM365 contacts no server other than Microsoft Graph. There is no telemetry, no licence server, no update check, and no analytics of any kind.
When you register a tenant, TrustM365 stores:
| Field | How it is stored |
|---|---|
| Tenant ID | Plaintext — not sensitive; visible in any Entra URL |
| Application (Client) ID | Plaintext — not sensitive; visible in the Entra portal |
| Client Secret Value | AES-256-GCM encrypted using a key that lives only in your .env |
The client secret is the only sensitive credential. It is encrypted before being written to the database and decrypted in memory only at the moment a Graph API token is being acquired. It is never written to logs.
- Algorithm: AES-256-GCM — authenticated encryption that protects both confidentiality and integrity
- Key: 256-bit (64 hex chars) random key generated by
npm run generate:key - Key storage: Your
.envfile or App Service application settings — never in the database - IV: Randomly generated per encryption operation — stored alongside the ciphertext in the DB
If the database file is exfiltrated without the encryption key, the stored secrets cannot be decrypted.
Critical: Protect your
.envfile. Anyone with access to both the database file and the encryption key can decrypt all stored tenant credentials. Never commit.envto version control.
Access tokens (used to call the Graph API) are:
- Never written to disk — held in process memory only for the duration of the API call
- Never logged — the logger automatically redacts any field named
token,secret,key, orpassword - Short-lived — MSAL Node manages token lifetime and refresh automatically
- Scoped to app-only — the client credentials flow is used; no user identity or delegated access is involved
The backend API binds to 127.0.0.1:3001 — accessible only from the same machine, not from other devices on the network. The Vite dev server (localhost:5173) proxies all /api requests to the backend internally.
CORS is restricted to localhost:5173 and 127.0.0.1:5173 only. API calls from any other origin are rejected.
Setting NODE_ENV=production makes two changes:
-
The backend binds to
0.0.0.0— necessary for Docker (nginx routes to the backend container over the internal network), Azure App Service (the platform's load balancer routes external traffic to the process), and any reverse-proxy setup. -
CORS is relaxed — in Docker and single-App-Service Azure deployments, the frontend and backend share the same origin via nginx proxying, so the browser never makes a cross-origin request and CORS is effectively not needed. If
FRONTEND_URLis set (e.g. for split-origin Azure deployments), CORS is locked to that specific origin only.
| Scenario | Binds to | CORS |
|---|---|---|
| Local dev | 127.0.0.1 |
localhost:5173 only |
| Docker Compose | 0.0.0.0 |
Same origin via nginx — not applicable |
| Azure single App Service | 0.0.0.0 |
Same origin via nginx — not applicable |
| Azure split App Services | 0.0.0.0 |
Locked to FRONTEND_URL value |
Do not run production deployments with
NODE_ENV=development. The backend will bind to localhost only and will be unreachable from Docker's internal network or Azure's platform routing.
HTTPS is provided automatically via the *.azurewebsites.net domain — Azure terminates TLS, no certificate management is required.
Restrict access to your organisation's IP range:
az webapp config access-restriction add \
--name trustm365-yourorg \
--resource-group trustm365-rg \
--rule-name "CorporateOnly" \
--action Allow \
--ip-address YOUR.CORP.IP.RANGE/24 \
--priority 100For maximum isolation, deploy inside an Azure Virtual Network with a private endpoint. See Azure App Service VNet integration.
By default the nginx container binds to 0.0.0.0:80. For production:
- Bind to a specific interface in
docker-compose.yml:ports: - "127.0.0.1:80:80"
- Place a reverse proxy (nginx, Caddy, Traefik) in front with TLS termination
- Restrict inbound access via your host firewall or cloud security group
TrustM365 uses Helmet.js on the Express backend, which sets the following headers on all API responses:
| Header | Value |
|---|---|
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
X-XSS-Protection |
0 — disabled; modern browsers handle this natively |
Referrer-Policy |
no-referrer |
Strict-Transport-Security |
max-age=15552000 on HTTPS deployments |
The nginx container (Docker/Azure) adds X-Frame-Options: SAMEORIGIN and X-Content-Type-Options: nosniff at the frontend layer.
TrustM365 works with read-only permissions. If you do not need the restore feature, omit all ReadWrite permissions from the App Registration. The dashboard shows drift without displaying restore buttons.
This reduces your attack surface significantly: a compromised TrustM365 deployment with read-only permissions cannot modify your M365 tenant configuration.
Custom collectors are always read-only regardless of the App Registration's write permissions — the restore path is deliberately blocked in the collector layer.
See prerequisites.md for the complete permissions breakdown.
TrustM365 uses Pino for structured JSON logging.
Sensitive fields are automatically redacted before any log is written:
secret, token, key, password, clientSecret, encryptionKey
Log level is controlled by LOG_LEVEL in .env. Default is info. Use error in production to minimise log volume. Do not use debug in production — request bodies may be included.
Logs are written to stdout. In Docker they are captured by the container runtime. On Azure App Service they are available via Log Stream and Application Insights.
The SQLite database (trustm365.db) contains:
- Encrypted tenant credentials (client secrets)
- All baseline definitions and version history
- Live configuration snapshots
- Drift detection results and history
- Restore audit logs (including what was patched and when)
- Custom collector definitions
- Tenant overview and insights cache
- Security check results
- MSSP settings
Protect this file as you would a password vault.
| Environment | Recommendation |
|---|---|
| Local | chmod 600 data/trustm365.db — restrict to the owning user |
| Docker | Do not bind-mount the data volume to a world-readable host path |
| Azure App Service | The /home filesystem is private to your App Service instance |
Database backups created by npm run db:backup contain the same encrypted credentials as the live database. Apply the same access controls to backup files.
Never store backups in the same location as the .env file — if both are exfiltrated together, all tenant credentials can be decrypted.
Please do not open a public GitHub issue for security vulnerabilities.
Email the maintainer directly. Include:
- A description of the vulnerability
- Steps to reproduce
- Potential impact
You will receive a response within 48 hours. Responsible disclosure is appreciated.