Security Model
Security Model
Cognova is a single-user, self-hosted application. The security model focuses on protecting the instance from unauthorized network access and preventing filesystem escape.
Authentication
BetterAuth
Authentication is handled by BetterAuth with email/password credentials. The configuration lives in server/utils/auth.ts.
| Setting | Value | Description |
|---|---|---|
| Provider | emailAndPassword | No OAuth providers configured by default |
| Session lifetime | 7 days | Configurable via session.expiresIn |
| Session refresh | Every 24 hours | Keeps active sessions alive |
| Secure cookies | Auto-detected | Enabled when BETTER_AUTH_URL starts with https:// |
| Trusted origins | Auto-configured | Based on BETTER_AUTH_URL or dynamic when ACCESS_MODE=any |
Session Flow
- User submits email/password to
/api/auth/sign-in/email - BetterAuth verifies the hashed password in the
accounttable - A session row is created with a unique token, client IP, and user agent
- The session token is set as an HTTP-only cookie
- Subsequent requests include the cookie, which BetterAuth validates against the
sessiontable - Sessions expire after 7 days of inactivity
Default Admin Seeding
On first startup with an empty database, the seedIfEmpty() function creates a default user:
| Variable | Default |
|---|---|
ADMIN_EMAIL | admin@example.com |
ADMIN_PASSWORD | changeme123 |
ADMIN_NAME | Admin |
Change the default password immediately after first login. The seed logs a warning to the console as a reminder.
API Token Authentication
CLI tools and skills use a separate API token instead of cookie-based auth. The token is auto-generated on server startup and written to the .api-token file. You can override it by setting COGNOVA_API_TOKEN in your environment.
API routes check for either a valid session cookie or a matching Authorization: Bearer <token> header.
Authorization
Cognova uses a single-user model. There are no roles, permissions, or access control lists. If you are authenticated, you have full access to everything. The auth layer exists solely to prevent unauthorized network access to the instance.
Path Validation
All file operations are sandboxed to the vault directory via server/utils/path-validator.ts.
How It Works
The validatePath() function:
- Strips leading slashes from the requested path
- Resolves the path relative to
VAULT_ROOT - Computes the relative path from
VAULT_ROOTto the resolved path - Rejects the request with HTTP 403 if the relative path starts with
..or the resolved path does not start withVAULT_ROOT
Request: /api/fs/read?path=../../etc/passwd
Resolved: /etc/passwd
Relative to vault: ../../etc/passwd
Result: 403 "Path outside vault directory"
Vault Root Resolution
The vault root is determined once at server startup in this order:
VAULT_PATHenvironment variable (recommended)/tmp/cognova-vault(if it exists)~/Documents/vault(if it exists)- Current working directory (fallback)
Terminal Sandboxing
The PTY manager spawns shell sessions rooted in the vault directory. The initial cwd is always set to getVaultRoot(). Note that the terminal itself does not restrict navigation -- a user with shell access can cd anywhere the process has permission to read. The terminal is an authenticated-only feature.
Encryption
Secret Storage
Secrets are encrypted at rest using AES-256-GCM. The implementation is in server/utils/crypto.ts.
| Parameter | Value |
|---|---|
| Algorithm | aes-256-gcm |
| Key derivation | scrypt with fixed salt |
| Key source | BETTER_AUTH_SECRET environment variable |
| IV | 16 random bytes per secret (stored alongside) |
| Auth tag | Appended to ciphertext, separated by : |
The encryption flow:
- Derive a 32-byte key from
BETTER_AUTH_SECRETusingscryptSync - Generate a random 16-byte IV
- Encrypt with
aes-256-gcm, producing ciphertext + auth tag - Store
encrypted_valueasciphertext:authTag(hex) andivas hex
The encryption key is derived from BETTER_AUTH_SECRET. If you rotate this secret, all stored secrets become unreadable. Back up the secret value before changing it.
Password Hashing
User passwords are hashed by BetterAuth using its built-in hashing (bcrypt). The hash is stored in the account.password column.
Database Security
Connection Security
The database connection auto-detects whether to use SSL based on the DATABASE_URL:
- Neon (*.neon.tech) -- SSL required
- Local (localhost, 127.0.0.1) -- SSL disabled
- Other -- SSL disabled (configure manually if needed)
Graceful Degradation
If the database is unavailable, the server continues running with database features disabled. The requireDb() guard returns HTTP 503 for API routes that need the database, rather than crashing.
Advisory Locking
Migrations use PostgreSQL advisory locks (pg_try_advisory_lock) to prevent concurrent migration runs when multiple server instances start simultaneously. The lock is always released in a finally block.
Environment Variables
Critical secrets are loaded from environment variables, never hardcoded:
| Variable | Purpose | Required |
|---|---|---|
BETTER_AUTH_SECRET | Session signing, secret encryption key derivation | Yes |
DATABASE_URL | PostgreSQL connection string | No (degrades gracefully) |
VAULT_PATH | Filesystem sandbox root | Yes |
BETTER_AUTH_URL | Auth callback base URL, cookie domain | Recommended |
COGNOVA_API_TOKEN | Fixed API token override | No (auto-generated) |
The 00.env-validate plugin checks these on startup and logs warnings or errors for missing values.
See the Configuration page for the full environment variable reference.
Production Hardening Checklist
When deploying Cognova to a production environment:
- Set
BETTER_AUTH_URLto your HTTPS URL so cookies use theSecureflag - Generate a strong
BETTER_AUTH_SECRETwithopenssl rand -base64 32 - Change the default admin password immediately after first login
- Use a strong, unique
DATABASE_URLpassword - Run behind a reverse proxy (nginx, Caddy) that terminates TLS
- Restrict network access to the Cognova port (firewall or VPN)
- Set
VAULT_PATHto a directory with appropriate filesystem permissions - Do not expose the PostgreSQL port to the public internet
- Back up
BETTER_AUTH_SECRET-- losing it makes stored secrets unrecoverable - Consider setting
ACCESS_MODEto a specific origin rather thanany