MSP AlignMSP Align Suggest a feature Docs GitHub

MSP Align security

This document describes how MSP Align protects the client information it holds, how the app and its server are hardened, what the security audits found, and what the operator (the MSP running it) is responsible for outside the application.

MSP Align holds a lot about your clients: who they are, what they run, what it costs and what's planned. It is built on a few principles: every account signs in with two-factor, each person sees only what their role or client allows, everything that matters is in a tamper-evident audit log, secrets and backups are encrypted, and updates are installed only when they're signed with the project's release key.

What Align stores

It isn't designed to hold regulated personal data such as health records, payment card numbers or government IDs. Keep that kind of information out of notes, meeting notes and documents.

Core controls

Area How Align handles it
Individual accounts Every staff and client-portal user has an individual account. There are no shared logins.
Access control Staff roles: viewer < tech < admin. Client-portal users are scoped to one client and to the sections ticked for them. They have a separate session cookie and user table, and every portal query uses the signed-in user's own client ID, never an ID from the URL.
Emergency access sudo align user:reset-password --email=… --clear-2fa on the server. It issues a one-time password that must be changed at next sign-in, is written to the audit log and raises a security alert (2.2.1).
Automatic sign-out Idle timeout defaults to 15 minutes (Settings → General, 5–60 minutes). Absolute session limit defaults to 12 hours. The browser warns one minute before sign-out and signs the page out itself, so nothing stays on screen. The tabs of one sign-in share the idle clock: working in one tab keeps the others signed in, and signing out in one signs out the others. The server enforces both limits independently.
Encryption At rest: MariaDB tables, the redo log, temp files and Aria tables are encrypted with a key file readable only by mysql. API keys (the AI provider's (2.29.0), NinjaOne, Tactical RMM, the Datto RMM secret key, each client's KnowBe4 token, ITFlow, the HaloPSA client secret, Veeam, Dell, Lenovo), the Microsoft 365 client secret, certificate key and refresh token, the Google service-account key, OAuth client secret and refresh token, the SMTP password, and 2FA secrets are also encrypted in the application with libsodium, using app_key. Backups: encrypted with age to a public key; the private key is kept offline.
Audit log Everything is logged: sign-ins, failures and timeouts; every change; exports and reports; client-portal actions. So are views of client records: overview, contacts, devices (including unassigned hardware), documents and document lists, contracts, meetings, compliance, client-portal users and portal pages, and every view of an onboarding page, each logged once per 15 minutes per session. An audit entry for a change made inside a database transaction is kept only if the change is kept (2.2.1). Each entry is sealed with an HMAC over its contents and the previous entry's hash, and the log's start and end markers have a seal of their own (1.45), so edits, insertions and deletions are detected, including entries cut off either end (Administration → Audit log, align audit:verify, and the nightly job). The nightly job also keeps the newest entry it saw outside the database, in the root agent's own folder, and checks it is still there the next night, so an old copy of the log written back is caught too. Entries are kept 6 years. The audit page checks every entry added since the last full check each time it opens; the whole chain is checked nightly and on demand (Check the whole log). Entries can be filtered by person, kind of action and text.
Integrity The hash-chained audit log, CSRF tokens on every form, and a strict allowlist HTML sanitizer for documents. Document versions are kept with full history.
Sign-in and two-factor Two-factor sign-in (TOTP) is required for every staff and portal account; no data is shown until it's set up. Each code works only once. Passwords must be at least 12 characters (counted as characters, not bytes) and aren't allowed to be common passwords or contain the user's name, any part of it, or their email. Authenticator keys are at least 128 bits. They're hashed with Argon2id. After 5 failed attempts an account is locked for 15 minutes (10 from one IP address, across accounts; an IPv6 client counts per /64, since 2.2.1), and fail2ban bans repeat offenders at the firewall. Wrong codes after a correct password are counted per account (2.2.1): admins get a security alert at 5 in a row and every 25 after, and at 50 the password is replaced with a random one and every session ended, since someone is guessing codes with a known password. Attempts are counted before the password is checked, so parallel guesses can't get past the limit, and the password check when changing your password is counted the same way. Changing a password or resetting 2FA ends every other session. Moving 2FA to a new phone needs a code from the current one. After the code, a browser can be remembered for up to 30 days (14 by default; Settings → General → Security, 0 turns it off): on it the password is still asked for, only the code is skipped. Only a hash of its random 256-bit cookie (HttpOnly, Secure, SameSite=Strict) is stored, and it stops working when the person changes their password or authenticator, signs out everywhere, has 2FA reset or is disabled. Each person sees and can forget their remembered browsers on their Account page; sign-ins that skipped the code say so in the audit log (with the browser's number from that list). Setting the days to 0 forgets every remembered browser at once. Confirming a restore always asks for the code. On a remembered browser a sign-in is the password plus that browser's cookie until the days run out; if your own policy or your cyber insurance requires a code at every sign-in, set it to 0. When an admin removes someone's 2FA, their password is replaced by a one-time password too, so whoever knew the old password can't set up their own authenticator. For client-portal users the password is cleared and a new link issued.
Connections TLS 1.2 or 1.3 only, with forward-secret AEAD ciphers. HSTS is on. Cookies are Secure, HttpOnly and SameSite=Lax, and carry the __Host-/__Secure- prefix. The ITFlow and Veeam connections must use https://. Email goes to Microsoft 365 over HTTPS through Microsoft Graph with OAuth 2.0 (client credentials with a secret or certificate, or authorization code with PKCE); With Google Workspace, mail goes over HTTPS through the Gmail API and invitations through the Google Calendar API, using a service account with domain-wide delegation (signed JWT) or authorization code with PKCE. With an SMTP server, mail goes over STARTTLS or TLS; its password, if any, is encrypted with app_key and never sent without encryption. Email bodies are cleared after the retention period set on Integrations → Email (30 days by default) and one-time invite/reset links are wiped as soon as they are sent. In proxy mode Apache and the firewall accept connections only from the proxy.

Application hardening

Headers

Input and output

Uploads

Links and tokens

Errors

Contracts and e-signatures (2.2)

REST API (1.27)

Server hardening (install.sh / msp-align-update)

Docker installs (1.44) get the app's own controls, the database encrypted at rest and the backup encryption; the host's firewall, fail2ban and OS updates, and TLS at your proxy or the Caddy add-on, are yours to run (see Install with Docker). The rest of this section is the dedicated install.

Operating system

Apache

PHP

MariaDB

Backups

Security audit (1.45)

Before 2.0 every file of the app, installer, backup agent and Docker setup was reviewed line by line (340 files) against six areas:

  1. Data taken in from ITFlow, HaloPSA, NinjaOne, Tactical RMM, Datto RMM, KnowBe4, Microsoft 365, Google, backup tools and warranty lookups
  2. Database queries and isolation between clients
  3. Stored secrets
  4. Running programs
  5. The audit trail
  6. Dependencies

Each finding was then checked against a test server, including attempts to break in. Findings and their fixes:

tests/e2e/suites/sec145_e2e.py checks each fix.

Security audit (2.2.1)

2.2.1 is a code-quality and security release. Every file of the app, installer, backup agent, Docker setup, command line and workflows (480 files, about 70,000 lines) was reviewed twice:

  1. Line-by-line review in five batches: core, sign-in and REST API; the client portal, signing, contracts, PDFs and uploads; integrations, sync and mail; the root agent, installer, Docker, CLI and CI; then the remaining controllers, views and browser scripts. Each batch's changes were checked by an independent reviewer.
  2. A separate whole-program audit against the same six areas as the 1.45 audit, with a written result for every file. Its findings were fixed and checked again.

Every class and function now documents what it does, what it takes and what it assumes about security. Findings and their fixes:

Every fix has a check in the test suites added in 2.2.1 (tests/e2e/suites/*_q_e2e.py and the core_/api_ suites). - Checked and sound: - All SQL uses bound parameters, and every name built into SQL comes from code. - Every view escapes its output. - CSRF is checked on every form, and every route checks roles. - Portal users, onboarding and signing links, and client-limited API keys only reach their own client. - Secrets are encrypted with libsodium, tokens are 256-bit and stored as hashes, and TLS is verified. - The hash-chained audit log detects edits, insertions, deletions and cut-off ends. - Dependencies: there are still no Composer (PHP) dependencies. The browser libraries (AdminLTE, Bootstrap, Quill, FullCalendar, Font Awesome, PDF.js) are included in the repository. GitHub Actions are pinned to exact commits. - Known and accepted: - Integrations may reach private network addresses, so on-premises ITFlow or Veeam servers work. - The Docker base image is referenced by tag, not digest, so a rebuild picks up its security updates. - The 1.45 accepted risks still apply.

Operator responsibilities (outside the app)

  1. Store the backup private key offline. - The installer prints it once and leaves a copy at /root/msp-align-backup-key.txt. - Put it in your password manager, then sudo shred -u that file. - Test a restore at least yearly: Settings → Updates & backups → upload a backup → Test this backup opens and checks it without changing anything.
  2. Download backups regularly and store them off the server, away from the private key (for example on your file server, with the key in your password manager). - config.php holds app_key, which is needed to decrypt stored secrets and to verify the audit chain. Backups include it, encrypted.
  3. Harden SSH. - Use keys only (PasswordAuthentication no). - No root login. - Limit SSH to your management network or VPN.
  4. Use a real certificate (Let's Encrypt, or proxy mode behind BunkerWeb) for a public server. Don't use self-signed.
  5. Review regularly. - Review the audit log and user list (staff and portal) at least quarterly. - Disable accounts promptly when someone leaves.
  6. Your own security program. - Include the Align server in your risk assessment, staff security training and incident response plan. - Align's own policy, WISP and incident response templates can document these for your company too.
  7. Watch for alerts. Watch for ALERT: audit log verification failed in journalctl -u msp-align-nightly, and keep the Updates and Security alerts email notifications on.

Reporting a vulnerability

Please report security issues privately through GitHub: on github.com/MSP-ALIGN/MSP-ALIGN open the Security tab and choose Report a vulnerability. Don't open a public issue. Include the version (Settings → Updates & backups), what you found and how to reproduce it. You'll get a reply within a few days; fixes ship as a normal release, and the advisory is published once installs have had time to update. Only the latest release is supported.