Security
Authentication, 2FA, passkeys, session management, encryption, rate limiting, and network security.
oCore implements defense-in-depth security across authentication, authorization, encryption, and network layers. This page consolidates all security features and best practices. Security documentation is also distributed across relevant feature pages throughout this guide.
Authentication
Password Authentication
oCore enforces strong password requirements:
| Requirement | Detail |
|---|---|
| Minimum length | 8 characters |
| Complexity | At least 1 letter and 1 number |
| Hashing algorithm | Argon2id (OWASP recommended parameters) |
| Parameters | 19 MiB memory, 2 iterations, 1 parallelism, 16-byte salt |
| Storage format | $argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash> |
Why Argon2id
Argon2id is the winner of the Password Hashing Competition and is recommended by OWASP for its resistance to GPU and ASIC attacks. The memory-hard property makes brute force attacks prohibitively expensive.
JWT Token Authentication
oCore uses JSON Web Tokens (JWT) for session authentication:
| Property | Value |
|---|---|
| Token type | Access token + Refresh token |
| Access token lifetime | Short-lived (configurable, default 1 hour) |
| Refresh token lifetime | Longer-lived (configurable, default 30 days) |
| Signing algorithm | HMAC-SHA256 |
| Secret requirement | Minimum 32 characters (JWT_SECRET env var) |
The access token is included in the Authorization: Bearer header for every API request. When it expires, the client uses the refresh token to obtain a new access token without re-entering credentials.
Two-Factor Authentication (TOTP)
Add a second factor to your account using a time-based one-time password (TOTP):
Go to Account Settings > Security and click Enable 2FA.
Scan the QR code with an authenticator app (Google Authenticator, Authy, 1Password, etc.).
- Issuer: oCore
- Algorithm: TOTP (RFC 6238)
Enter the 6-digit code from your authenticator to verify.
Save the 8 recovery codes displayed. Each code is single-use and can be used if you lose access to your authenticator.
Recovery Codes
Recovery codes are 8 characters each, lowercase alphanumeric. They are hashed (SHA-256) before storage -- oCore cannot recover them. Store them securely (printed or in a password manager). Each code can only be used once.
Passkeys (WebAuthn)
oCore supports passwordless authentication via WebAuthn passkeys:
- Hardware keys -- YubiKey, Titan Key
- Platform authenticators -- Touch ID, Face ID, Windows Hello
- Synced passkeys -- iCloud Keychain, Google Password Manager
Go to Account Settings > Security > Passkeys and click Register Passkey.
Follow your browser's prompt to create the passkey using your preferred authenticator.
Name the passkey for identification (e.g., "MacBook Touch ID", "YubiKey 5").
Security Settings
Manage 2FA, passkeys, and security preferences.
Session Management
Active Sessions
View and manage your active sessions:
curl https://ocore.example.com/api/user/sessions \
-H "Authorization: Bearer $TOKEN"Each session shows:
- Device and browser information
- IP address
- Last active timestamp
- Creation time
Revoking Sessions
Revoke a specific session:
curl -X DELETE https://ocore.example.com/api/user/sessions/{sessionId} \
-H "Authorization: Bearer $TOKEN"CORS Configuration
oCore configures Cross-Origin Resource Sharing (CORS) to control which domains can access the API:
- The
APP_URLenvironment variable defines the allowed origin - The documentation site domain is also allowed for auth-aware features
- API endpoints return appropriate
Access-Control-Allow-*headers
For self-hosted deployments, ensure APP_URL matches your dashboard domain to avoid CORS errors.
Rate Limiting
oCore implements rate limiting to protect against abuse:
Authentication Endpoints
| Endpoint | Limit |
|---|---|
Login (POST /api/auth/login) | 5 requests per minute per IP |
Signup (POST /api/auth/signup) | 5 requests per minute per IP |
| Password reset | 5 requests per minute per IP |
| TOTP verification | 5 requests per minute per IP |
Transfer Callback
| Endpoint | Limit |
|---|---|
| Presign callback | 60 requests per minute per IP |
General API
The general API does not have per-endpoint rate limiting by default but can be configured via reverse proxy settings (Traefik, Nginx).
Rate Limit Response
When rate limited, the API returns HTTP 429 with a retryAfter field indicating seconds to wait.
SSH Gateway Security
The SSH gateway provides terminal access to Odoo instances via port 2222:
How It Works
- User connects:
ssh -p 2222 <instance-slug>@ocore.example.com - oCore authenticates using the user's registered SSH public key
- The connection is routed to the target instance container
- Session is logged in the audit trail
Security Measures
| Measure | Description |
|---|---|
| Key-based auth only | No password authentication on the SSH gateway |
| User SSH keys | Users register public keys via the dashboard |
| Session logging | All SSH sessions are recorded in audit logs |
| Instance isolation | Each connection is scoped to one instance |
| Session limits | Configurable max sessions per user |
See SSH Access for detailed SSH management.
Encryption at Rest
SSH Credentials
Server SSH private keys and passwords are encrypted at rest:
| Property | Value |
|---|---|
| Algorithm | AES-256 |
| Key source | SSH_ENCRYPTION_KEY environment variable |
| Key requirement | Minimum 32 characters |
| Encryption scope | SSH private keys, SSH passwords |
The plaintext credentials exist only in memory during SSH operations. They are never stored in plaintext in the database.
Backup Destination Credentials
Backup destination configurations (S3 access keys, SFTP passwords, etc.) are similarly encrypted:
| Property | Value |
|---|---|
| Storage | Encrypted JSON blob in database |
| Encryption | AES-256 using server-side key |
| Decryption | On-demand when performing backup operations |
Recovery Code Hashing
TOTP recovery codes are hashed with SHA-256 before storage. The plaintext codes are shown only once at TOTP setup time.
Network Security
IP Access Control
Restrict access to instances by IP address or CIDR range. See Domains and DNS for IP access rule configuration.
Firewall Recommendations
For self-hosted deployments, expose only necessary ports:
| Port | Service | Access |
|---|---|---|
| 443 | HTTPS (dashboard + API) | Public |
| 80 | HTTP (redirect to HTTPS) | Public |
| 2222 | SSH gateway | Restricted to known IPs recommended |
| 5432 | PostgreSQL | Internal only (never expose) |
| 8080 | Backend API | Internal only (behind reverse proxy) |
TLS/SSL
- All public endpoints should use TLS 1.2 or higher
- The reverse proxy handles TLS termination
- Self-signed certificates are acceptable for internal services only
- Use Let's Encrypt or a commercial CA for public endpoints
Security Best Practices Checklist
- Enable 2FA for all administrator accounts
- Use passkeys for the highest security accounts (Owner)
- Rotate API keys regularly (quarterly recommended)
- Set
SSH_ENCRYPTION_KEYto a strong random value (64+ characters) - Set
JWT_SECRETto a strong random value (64+ characters) - Configure a reverse proxy with TLS for all public endpoints
- Restrict SSH gateway access (port 2222) to known IP ranges
- Never expose PostgreSQL (port 5432) to the public internet
- Review audit logs weekly for suspicious activity
- Remove unused API keys and revoke unused sessions
- Set IP access rules on production instances
- Neutralize databases when copying production to staging
- Use separate organizations for separate security domains
OAuth 2.1 Security
oCore's OAuth Authorization Server implements defense-in-depth measures to protect the MCP authorization flow.
PKCE S256 Enforcement
All authorization code flows require Proof Key for Code Exchange (PKCE) with the S256 method. Plain code challenges are rejected. This prevents authorization code interception attacks even for public clients that cannot securely store a client secret.
SSRF Protection for CIMD
When oCore resolves a CIMD (Client ID Metadata Document) -- where the client_id is an HTTPS URL -- it fetches the document with SSRF safeguards:
- Connections to RFC 1918, loopback, link-local, and IPv6 unique-local addresses are blocked at the dialer level
- Cloud metadata IPs (169.254.169.254, 100.100.100.200) are explicitly blocked
- HTTPS only, no redirects, 5-second timeout, 1 MB response limit
- The
client_idfield in the fetched document must exactly match the URL that was fetched
CSRF Tokens on Consent
The consent flow uses per-request CSRF tokens to prevent cross-site request forgery:
- A 32-byte random token is generated when the consent page loads and stored server-side keyed by request ID
- The token is validated with constant-time comparison on consent submission
- Tokens are single-use (deleted after validation) and expire after 10 minutes
Constant-Time Secret Comparisons
All secret comparisons use crypto/subtle.ConstantTimeCompare to prevent timing attacks:
- Client secret verification
- CSRF token validation
- Authorization code hash comparison
Refresh Token Rotation with Family Tracking
OAuth refresh tokens are rotated on every use with replay detection:
| Behavior | Detail |
|---|---|
| Normal rotation | Each refresh returns a new access + refresh token pair. The old refresh token is marked as rotated. |
| Grace period | If a rotated token is replayed within 60 seconds (e.g., network retry), a new pair is issued. |
| Replay detection | If a rotated token is replayed after the grace period, the entire token family is revoked. This detects token theft. |
| Family tracking | All tokens in a rotation chain share a family_id. Revoking the family soft-deletes every token in the chain. |
Per-Org OAuth Logout Policy
When a user logs out or an administrator triggers session revocation for an organization:
- All active OAuth tokens for that user in the organization are soft-deleted
- Auto-created API keys with no remaining active tokens are cleaned up
- Manually-linked API keys are left untouched
Token Introspection Access
The /oauth/introspect endpoint (RFC 7662) is restricted to confidential clients only. Public clients receive 401 invalid_client. This prevents unauthorized token metadata queries.
Authorization Code Protections
- Codes are hashed (SHA-256) before storage and consumed atomically (single-use)
- Codes expire after 10 minutes
redirect_uriis verified via exact byte-for-byte string comparison against registered URIs- PAR (Pushed Authorization Request) entries expire after 60 seconds
API Key Scoping
oCore API keys support granular scoping to limit what a key can access, both for REST API usage and MCP (Model Context Protocol) sessions.
Instance Scoping
Restrict an API key to specific instances rather than granting access to all instances in the organization:
curl -X POST https://ocore.example.com/api/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Staging MCP Key",
"instanceIds": ["INSTANCE_UUID_1", "INSTANCE_UUID_2"]
}'When instanceIds is set, the key can only operate on the listed instances. When empty, the key has access to all instances in the organization.
MCP Tool-Level Permissions
For API keys used with MCP clients (AI assistants), oCore provides tool-level access control:
| Setting | Description |
|---|---|
| MCPReadOnly | When true, only read-only MCP tools are available (no writes, no destructive actions) |
| MCPToolAllowlist | List of tool suffixes that the key can access (e.g., ["list_users", "read_logs"]) |
| MCPAllowlistMode | none (no filtering), allow (only listed tools), or deny (all except listed tools) |
| MCPModelAllowlist | Restrict ORM access to specific Odoo models (e.g., ["res.partner", "sale.order"]). Read-only via API — set directly in the database. |
Project Scoping
API keys can be scoped to specific projects within the organization:
curl -X POST https://ocore.example.com/api/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Project-scoped Key",
"projectIds": ["PROJECT_UUID_1"]
}'IP Allowlist on API Keys
Each API key can have an IP allowlist. Requests from IPs not in the list are rejected:
{
"name": "Office-only Key",
"ipAllowlist": ["203.0.113.0/24", "198.51.100.42"]
}Rate Limits per Key
MCP sessions enforce per-organization rate limits:
- SSE transport -- Configurable requests per minute (org setting)
- Stdio transport -- Separate rate limit for local CLI usage
- Concurrency -- Maximum concurrent MCP sessions per organization
IP Access Rules
oCore provides a three-tier IP access control system to restrict traffic to your instances by source IP address or CIDR range.
Scope Hierarchy
IP access rules follow a three-tier inheritance model:
| Scope | Applies To | Inherits From |
|---|---|---|
| Organization | All instances in the organization | -- |
| Project | All instances in the project | Organization rules |
| Environment | Instances in the specific environment | Organization + Project rules |
When evaluating access, oCore merges rules from all applicable scopes. A request is allowed if it matches any active rule at any scope level.
Traffic Scopes
Each rule specifies which types of traffic it covers:
| Scope | Description |
|---|---|
http | Web browser and REST API access |
websocket | WebSocket connections (live chat, bus) |
ssh | SSH terminal access via the gateway |
ide | IDE integrations and code editor access |
When no traffic scopes are specified, the rule applies to all four types by default.
Creating Rules
# Organization-level rule
curl -X POST https://ocore.example.com/api/ip-access-rules \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"organizationId": "ORG_UUID",
"cidr": "203.0.113.0/24",
"label": "Office VPN",
"trafficScopes": ["http", "websocket"]
}'
# Environment-level rule
curl -X POST https://ocore.example.com/api/ip-access-rules \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"environmentId": "ENV_UUID",
"cidr": "198.51.100.42",
"label": "Developer Home IP",
"trafficScopes": ["http", "ssh"]
}'Navigate to Settings > IP Access Rules in your organization, project, or environment settings. Click Add Rule, enter the CIDR range and optional label, select the traffic scopes, and save.
CIDR Validation
oCore validates all IP inputs using Go's net/netip package:
- Full CIDR ranges are accepted (e.g.,
192.168.1.0/24) - Bare IP addresses are accepted and stored in canonical form (e.g.,
10.0.0.1) - Both IPv4 and IPv6 addresses are supported
- Invalid inputs are rejected with a descriptive error message
Activating and Deactivating Rules
Each rule has an is_active flag. Deactivating a rule temporarily disables it without deleting:
curl -X PATCH https://ocore.example.com/api/ip-access-rules/{ruleId} \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"isActive": false}'Rule Application
When IP access rules are created or modified, oCore enqueues a background job to apply the rules to the target instances. The job resolves the three-tier inheritance and updates the instance-level firewall configuration via SSH.
Cross-References
Security features are documented in context throughout the documentation:
- SSH key encryption -- Servers (SSH Key Storage callout)
- Backup credential encryption -- Backups
- IP access control -- Domains and DNS
- Database neutralization -- Databases
- API key security -- API Keys
- Team access control -- Roles and Permissions
- SSH session management -- SSH Access
- Webhook signature verification -- Webhooks
- Login tracking -- Audit Logs
- OAuth 2.1 Authorization Server -- MCP Server (OAuth section)
- OAuth client registry -- OAuth Clients
- MCP invite links -- MCP Invites
Troubleshooting
2FA code not accepted
- Check that your device clock is synchronized (TOTP is time-based)
- Ensure you are entering the code for the correct account
- If locked out, use one of your 8 recovery codes
- Contact your organization Owner to disable 2FA if recovery codes are lost
Passkey not working
- Ensure your browser supports WebAuthn (all modern browsers do)
- Try a different authenticator (hardware key vs platform authenticator)
- Check that the domain matches exactly (no www. prefix mismatch)
- Re-register the passkey if issues persist
Rate limited on login
- Wait 60 seconds before retrying
- Ensure your IP is not shared with many users (NAT/VPN)
- If persistently rate limited, check for automated scripts hitting the login endpoint
SSH connection refused on port 2222
- Verify port 2222 is open in the server's firewall
- Check that your SSH public key is registered in the dashboard
- Ensure the SSH gateway is enabled in the deployment configuration
- Verify the instance slug in your SSH command matches an active instance