Security
Defense-in-depth security for sensitive customer data in regulated environments.
Overview
InnConnect handles customer conversations, personal data, and AI-generated content for organizations operating under GDPR, ISO 27001, NIS2, and sector-specific regulations. Every layer of the platform is designed with the assumption that it will be attacked.
Security controls operate at three levels — infrastructure, application, and AI — to provide defense in depth. A vulnerability in one layer does not compromise the system, because the next layer catches it. For compliance framework mapping, see the Compliance page.
Authentication & Access
Two-factor authentication, forced password changes, session management, and 7-role RBAC across all admin panel actions.
Input & Output Protection
Server-side validation, dual-layer HTML sanitization (HTMLPurifier + DOMPurify), CSRF protection, and Content Security Policy.
AI-Specific Defenses
Structural prompt injection defense, SSRF protection on URL scraping, and content sanitization in the AI pipeline.
Tenant Isolation
Separate PostgreSQL database per tenant, per-tenant encryption keys, isolated S3 storage, and Kubernetes Secrets for credentials.
Tenant Isolation
Tenant isolation is the foundational security measure in InnConnect. Every other control depends on it. A bug in one tenant's data handling cannot affect another tenant, because their data is physically separated at every layer.
| Layer | Isolation method |
|---|---|
| Database | Each tenant gets a dedicated PostgreSQL database (innconnect_t{id}). There are no shared tables between tenants. Cross-tenant queries are architecturally impossible. |
| Credentials | Database credentials for each tenant are stored in Kubernetes Secrets — never in the application database, configuration files, or environment variables. Secrets are mounted at runtime. |
| Connection pooling | PgBouncer handles connection pooling with transaction-level isolation. Each tenant connection is routed through a wildcard database proxy on port 6432. |
| Encryption keys | Field-level encryption uses per-tenant keys stored in Kubernetes Secrets. Compromising one tenant's key does not expose another tenant's encrypted fields. |
| Object storage | Each tenant's file uploads are stored in an isolated prefix within TransIP Object Storage (S3-compatible). Bucket policies prevent cross-tenant access. |
| Cache | Redis cache keys are prefixed with the tenant ID. Cache invalidation is scoped per tenant. |
Authentication
Admin panel access requires email and password authentication via Laravel Sanctum. Plain-text passwords are never stored, logged, or transmitted after initial hashing.
- Password hashing — bcrypt with an appropriate cost factor. Passwords are hashed before storage; the original is immediately discarded.
- Session-based auth — Authentication state is maintained via server-side sessions, not JWTs or stateless tokens. This enables immediate session invalidation on logout or deactivation.
- Secure cookies — The session cookie is HTTP-only (not accessible via JavaScript), Secure (transmitted only over HTTPS), and SameSite=Lax (prevents CSRF via cross-origin requests).
- Session rotation — The session token is regenerated on every login to prevent session fixation attacks.
- Failed login logging — Failed attempts are recorded with the email, IP address, and timestamp. Repeated failures trigger rate limiting (see below).
Two-Factor Authentication
All admin users can enable TOTP-based two-factor authentication. When enabled, login requires both a valid password and a 6-digit time-based code from an authenticator app (Google Authenticator, Authy, 1Password, or any TOTP-compatible app).
Setup process
- Navigate to Profile → Security.
- Click Enable Two-Factor Authentication.
- Scan the QR code with your authenticator app. The QR encodes a standard otpauth:// URI with the account name and secret.
- Enter the 6-digit verification code displayed by the app to confirm setup.
- Download your recovery codes and store them in a secure location (password manager, printed in a safe).
Screenshot: 2FA setup with QR code and verification input
Recovery codes
During 2FA setup, eight single-use recovery codes are generated. Each code can be used exactly once to bypass 2FA if you lose access to your authenticator app.
- Recovery codes are hashed before storage — InnConnect cannot display them after initial generation. If you lose them, you must regenerate a new set.
- Regenerating recovery codes invalidates all previously generated codes.
- After all 8 codes are consumed, you must regenerate recovery codes from your security settings to get new ones.
Force Password Change
New users invited to the platform receive a temporary password via email. The system forces a password change on first login before the user can access any other admin panel functionality. This ensures temporary credentials have a minimal window of exposure.
- Enforced before all other actions — The password change middleware runs before any route, including API endpoints. There is no way to use the platform with a temporary password.
- Strength requirements — New passwords must meet the configured minimum length and complexity rules. Passwords are checked against a list of commonly breached passwords.
- Temporary password cannot be reused — The new password must differ from the temporary one.
Session Management
Sessions are stored server-side in the PostgreSQL system database — not in cookies, not in Redis. The cookie contains only an opaque session token. This prevents client-side tampering and makes server-side session invalidation take effect immediately.
| Setting | Default | Configurable |
|---|---|---|
| Session lifetime | 120 minutes of inactivity | Yes (per tenant) |
| Session cookie flags | HTTP-only, Secure, SameSite=Lax |
No (hardcoded for security) |
| Concurrent sessions | Multiple sessions allowed | Yes (can restrict to single session) |
| Logout behavior | Invalidates current session only | Option to invalidate all sessions |
| Session storage | PostgreSQL system database | No |
Rate Limiting
All sensitive endpoints are rate-limited to prevent brute force attacks, API abuse, and excessive AI credit consumption. Rate limits are enforced at the application layer using Laravel's built-in throttle middleware. Each limit is keyed to prevent one user's limit from affecting another.
| Endpoint | Limit | Window | Keyed by |
|---|---|---|---|
| Admin login | 5 attempts | per minute | Email + IP address |
| Chat messages (widget API) | 20 messages | per minute | Session ID |
| KB Wizard (AI research) | 30 requests | per minute | Authenticated user ID |
| Credit checkout | 10 requests | per hour | Authenticated user ID |
| User registration | 5 attempts | per hour | IP address |
| Password reset | 5 attempts | per hour | User ID or IP address |
| API key regeneration | 10 requests | per hour | Authenticated user ID |
When a rate limit is exceeded, the server returns HTTP 429 Too Many Requests with a Retry-After header indicating when the limit resets. Repeated violations are logged to the security audit log with the offending IP and identity.
Input Validation
All data submitted to InnConnect — through admin panel forms or API endpoints — is validated server-side before processing. Client-side validation exists as a UX convenience; it is never trusted for security decisions.
- Strict type and format rules — Every form field is validated against explicit type, length, and format constraints defined in Laravel Form Request classes.
- Mass assignment prevention — Controllers use $request->validated() or $request->only([...]) to whitelist accepted fields. Raw $request->all() is never passed to models or services.
- File upload validation — Uploaded files are validated for MIME type (by content inspection, not just extension), maximum size, and allowed extensions. ClamAV scans files for malware before storage.
- Integer bounding — Numeric parameters (pagination offsets, IDs) are cast and bounded before use in database queries to prevent injection and out-of-range errors.
XSS Protection
Cross-Site Scripting is mitigated through two independent sanitization layers. If one layer fails, the other catches it.
Server-side: HTMLPurifier
The ContentSanitizer service runs HTMLPurifier on all HTML content before storage — AI-generated articles, user-submitted text, and scraped web pages. A strict whitelist of allowed tags and attributes is enforced. Everything else is stripped. Suspicious patterns are logged to the security audit log.
Client-side: DOMPurify
Markdown content rendered in the admin panel (KB articles, AI wizard output) passes through DOMPurify before DOM insertion via x-html. Allowed tags are restricted to safe formatting elements: p, strong, em, ul, ol, li, headings, code, pre, blockquote, and a (with href only).
In addition, Blade templates use double-curly-brace escaping by default, which HTML-encodes output. Unescaped output is used only when content has already passed through HTMLPurifier, and these usages are tracked and audited.
SSRF Protection
The KB Wizard allows admin users to provide external URLs for content scraping. Without validation, this creates a Server-Side Request Forgery (SSRF) vector — an attacker could trick the server into making requests to internal infrastructure, cloud metadata endpoints, or other restricted resources.
The UrlValidator service validates every URL before any outbound HTTP request is made. It blocks:
| Blocked target | Why |
|---|---|
| Non-HTTP(S) schemes (file://, ftp://, dict://, gopher://) | Prevents local file reads and protocol-based attacks |
| Loopback addresses (127.0.0.1, ::1, localhost) | Prevents access to services on the same host |
| Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) | Prevents access to internal network services |
| Link-local range (169.254.0.0/16) | Prevents access to cloud metadata endpoints (AWS, Azure, GCP instance metadata) |
| IP obfuscation (decimal, hex, octal encoding) | Prevents bypass via alternative IP representations like 0x7f000001 |
| DNS rebinding | Hostnames are resolved and the resulting IP is validated. A domain that resolves to 127.0.0.1 is blocked. |
Blocked URL attempts are logged to the security audit log with the requesting user's identity, the rejected URL, and the reason for rejection.
Prompt Injection Defense
The AI chat pipeline processes content from three external sources: Knowledge Base articles, scraped web pages, and end-user messages. Any of these could contain malicious text designed to override the AI's instructions — a class of attack known as prompt injection.
InnConnect uses structural defenses rather than content filtering. Content filtering (e.g. blocking the phrase "ignore all previous instructions") is fragile and easily bypassed. Structural separation is robust.
- XML-style delimiters — KB content is wrapped in <knowledge_base_context> tags. User messages are wrapped in <user_message> tags. The AI is instructed to treat content within these tags as reference data only, never as instructions.
- Instruction reinforcement — After external content is injected into the prompt, the system prompt reinforces the AI's core behavioral directives. This reduces the effectiveness of any injected override, because the reinforcement is positioned after the attack surface.
- Content sanitization — The ContentSanitizer service detects and flags known injection patterns in scraped and user-submitted content before it enters the prompt. Detected patterns are logged but not silently removed (which could break legitimate content).
CSRF Protection
All state-changing form submissions in the admin panel include a Laravel CSRF token (@csrf). Requests without a valid token are rejected with HTTP 419.
- All 24 POST forms verified — The automated test suite confirms that every POST form in the admin panel includes CSRF protection.
- API endpoints exempt — The chat widget API uses session-based token authentication, not cookies. Since CSRF exploits rely on the browser automatically sending cookies, token-authenticated endpoints are not vulnerable to CSRF.
- Webhook signature verification — Stripe webhooks are verified by cryptographic signature using the Stripe SDK before processing. Invalid signatures are rejected and logged.
Content Security Policy
InnConnect serves a Content Security Policy (CSP) header on all admin panel responses. The CSP restricts which resources can be loaded and executed in the browser.
- Inline script policy — Inline scripts are allowed via unsafe-inline because the application uses server-rendered inline scripts with dynamic PHP data. XSS is mitigated by Laravel's auto-escaping and the ContentSanitizer service.
- Style restriction — Inline styles are allowed via unsafe-inline. External stylesheets are loaded from the application origin and explicitly allowlisted CDN sources.
- CDN allowlisting — External resources (CDN fonts, libraries) are explicitly allowlisted by origin.
- Clickjacking prevention — The frame-ancestors directive prevents the admin panel from being embedded in iframes on other domains.
- HSTS in production — Strict-Transport-Security ensures browsers always connect over HTTPS after the first visit.
Security Headers
The following HTTP security headers are set on all admin panel responses by the SecurityHeaders middleware.
| Header | Value | Purpose |
|---|---|---|
X-Content-Type-Options |
nosniff |
Prevents browsers from MIME-sniffing responses away from the declared content type |
X-Frame-Options |
SAMEORIGIN |
Prevents the page from being embedded in frames on other domains (clickjacking protection). Same-origin embedding is allowed. |
X-XSS-Protection |
1; mode=block |
Enables the browser's built-in XSS filter in blocking mode. Complements CSP as a legacy defense layer. |
Referrer-Policy |
strict-origin-when-cross-origin |
Sends only the origin (not the full URL path) in the Referer header for cross-origin requests |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
Disables browser APIs that InnConnect does not use, reducing attack surface |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
Forces HTTPS for one year after the first visit, including all subdomains (production only) |