Skip to content

Security: linagora/open-bastion

Security

SECURITY.md

Security Policy

Reporting a Vulnerability

We take security vulnerabilities seriously. If you discover a security issue, please report it responsibly.

Contact

Email: [email protected]

sequenceDiagram
    participant User
    participant PAM as PAM Module
    participant LLNG as LLNG Portal

    User->>PAM: One-time token
    PAM->>LLNG: POST /pam/verify
    LLNG-->>PAM: User attributes + authorization
    Note over LLNG: Token consumed (single-use)
    PAM-->>User: Session established
Loading
  1. User provides a one-time token generated by the LLNG portal
  2. PAM module verifies token via /pam/verify endpoint
  3. Token is consumed (single-use) and cannot be replayed
  4. Server returns user attributes and authorization status

Transport Security

TLS Configuration

Setting Default Description
min_tls_version 13 (TLS 1.3) Minimum TLS version (12=1.2, 13=1.3)
verify_ssl true Verify server certificate
ca_cert system Custom CA certificate path
cert_pin none Certificate pin (sha256//base64 format)

Certificate Pinning: When configured, the module validates the server's public key against the pinned value, preventing MITM attacks even with compromised CAs.

# Example configuration
min_tls_version = 13
cert_pin = sha256//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

Request Signing (Optional)

When request_signing_secret is configured, requests include:

  • X-Timestamp: Unix timestamp (server should reject if too old)
  • X-Nonce: Unique timestamp_ms-uuid format (server should reject duplicates)
  • X-Signature-256: HMAC-SHA256 signature of the request

This provides defense-in-depth against request tampering, even if TLS is somehow compromised.

Server Authentication

The PAM module authenticates to the LLNG server using:

Setting Description
server_token_file Path to file containing server bearer token
server_group Server group name (default: "default")
token_rotate_refresh Automatically rotate refresh tokens (default: true)

The server token should be stored in a file with restricted permissions (0600) owned by root.

OAuth2 Client Authentication

For OAuth2 token introspection and refresh operations, the module uses JWT Client Assertion (RFC 7523) instead of HTTP Basic Authentication. This provides enhanced security:

  • The client_secret is never transmitted over the network
  • Each request includes a unique JWT signed with HMAC-SHA256
  • JWT contains: iss, sub, aud, exp, iat, and unique jti (UUID v4)
  • JWT validity is 5 minutes to prevent replay attacks

Automatic Token Rotation

When token_rotate_refresh = true (default), the module automatically rotates the refresh token after each successful token refresh. This limits the window of opportunity if a token is compromised, as stolen tokens become invalid after the next legitimate use.

Bastion-to-Backend Authentication (JWT)

In bastion/backend architectures, the PAM module supports cryptographic verification that SSH connections to backends originate from authorized bastion servers.

Architecture

flowchart LR
    subgraph Bastion["Bastion Server"]
        proxy["llng-ssh-proxy"]
    end

    subgraph LLNG["LLNG Portal"]
        bastion_token["/pam/bastion-token"]
        jwks["/.well-known/jwks.json"]
    end

    subgraph Backend["Backend Server"]
        pam["pam_llng.so"]
        cache["JWKS Cache"]
    end

    proxy -->|1. Request JWT| bastion_token
    bastion_token -->|2. Signed JWT| proxy
    proxy -->|3. SSH + JWT| pam
    pam -->|4. Get public keys| jwks
    jwks -->|5. Cache keys| cache
    pam -->|6. Verify signature| cache
Loading

Security Benefits

Threat Without Bastion JWT With Bastion JWT
Direct backend access Possible if network accessible Blocked (no valid JWT)
VPN bypass to backend Possible Blocked
Firewall misconfiguration Exposes backends Backends still protected
Compromised bastion keys Access to backends Each hop still verified

Configuration (Backend)

# /etc/security/pam_llng.conf
bastion_jwt_required = true
bastion_jwt_issuer = https://auth.example.com
bastion_jwt_jwks_url = https://auth.example.com/.well-known/jwks.json
bastion_jwt_jwks_cache = /var/cache/pam_llng/jwks.json
bastion_jwt_cache_ttl = 3600
bastion_jwt_clock_skew = 60
# Optional: restrict to specific bastions
bastion_jwt_allowed_bastions = bastion-01,bastion-02
# /etc/ssh/sshd_config
AcceptEnv LLNG_BASTION_JWT

JWT Claims

Claim Description
iss LLNG portal URL (must match bastion_jwt_issuer)
sub Username being proxied
aud pam:bastion-backend
exp Expiration timestamp (short-lived)
bastion_id Identifier of the bastion server
bastion_group Server group of the bastion
target_host Target backend hostname
user_groups User's LLNG groups

Offline Verification

The JWKS cache enables JWT verification without network access to LLNG:

  1. First connection fetches JWKS from LLNG portal
  2. Public keys cached locally with configurable TTL
  3. Subsequent verifications use cached keys
  4. Cache refreshed when TTL expires or unknown key ID encountered

This provides resilience against LLNG outages while maintaining security.

Token Cache Security

Encryption at Rest

When cache_encrypted = true (default), cached tokens are encrypted using:

  • Algorithm: AES-256-GCM (authenticated encryption)
  • Key Derivation: PBKDF2-SHA256 with 100,000 iterations
  • Key Source: Machine ID (/etc/machine-id) + cache username as salt
  • Authentication: GCM tag prevents tampering
File format:
[Plaintext: "expires_at\n"][Magic: LLNGCACHE02][IV: 12 bytes][Tag: 16 bytes][Ciphertext]

The plaintext timestamp header allows quick expiration checks without decryption (performance optimization). However, the timestamp is duplicated inside the encrypted payload for integrity verification. If an attacker modifies the plaintext header to extend cache validity, the mismatch with the encrypted timestamp causes immediate rejection and cache file deletion.

Cache Isolation

  • Each user's cache is stored in a separate file
  • File permissions: 0600 (owner read/write only)
  • Directory permissions: 0700

Cache Invalidation

When cache_invalidate_on_logout = true (default):

  • User's cache is cleared when their PAM session closes
  • Prevents stale tokens from being reused

Risk-Based TTL

Service Type Default TTL
Normal services 300 seconds
High-risk services 60 seconds

Configure high-risk services via high_risk_services (comma-separated).

Rate Limiting

Protection against brute-force attacks:

Setting Default Description
rate_limit_enabled true Enable rate limiting
rate_limit_max_attempts 5 Failures before lockout
rate_limit_initial_lockout 30s Initial lockout duration
rate_limit_max_lockout 3600s Maximum lockout duration
rate_limit_backoff_mult 2.0 Exponential backoff multiplier

Lockout state is stored per-user in rate_limit_state_dir.

Auto-Create User Security

When create_user_enabled = true, users can be automatically created on first login.

Path Validation

All paths are validated before use:

Shell Validation (approved_shells):

  • Must be in approved list (default: common shells like /bin/bash, /bin/zsh)
  • Must be absolute path
  • No path traversal sequences (.., //)
  • No shell metacharacters

Home Directory Validation (approved_home_prefixes):

  • Must start with approved prefix (default: /home, /var/home)
  • Same safety checks as shell

Skeleton Directory Validation:

  • Must be absolute path
  • Must be owned by root
  • No symlinks in path components
  • No dangerous patterns

UID Generation

  • UIDs are generated deterministically from username hash
  • Range: 10000-60000 (configurable)
  • Collision handling: If UID exists, operation fails safely (returns 0)
  • No fallback to random UIDs that could cause unpredictable behavior

NSS Module Security

The NSS module (libnss_llng.so) provides user resolution:

  • Buffer overflow protection: All string copies use bounds-checked safe_strcpy()
  • Server input validation: Shell and home paths from server are validated against approved lists
  • UID range enforcement: Server-provided UIDs must be within configured min_uid/max_uid range
  • Fail-safe: Returns appropriate error codes on any failure; invalid paths fall back to defaults

Direct /etc/passwd and /etc/shadow Manipulation

User accounts are created by directly writing to /etc/passwd and /etc/shadow rather than using external tools like useradd. This design choice was made for:

Advantages:

  • Portability: No dependency on useradd which may not exist or have different options across distributions
  • Atomicity: Single-process control over file locking ensures consistent state
  • Predictability: No external tool behavior variations or unexpected prompts

Trade-offs:

  • PAM account creation hooks are not triggered (this module IS the PAM hook)
  • SELinux contexts must be handled separately if required
  • System audit logs only see file modifications, not semantic "user created" events

Mitigations:

  • The module emits its own structured audit events when audit_enabled = true
  • File operations use exclusive locks (flock) to prevent race conditions
  • If /etc/shadow write fails after /etc/passwd succeeds, rollback is attempted via userdel
  • TOCTOU protection: user existence is re-checked after acquiring locks

Audit Logging

When audit_enabled = true:

Setting Default Description
audit_log_file none JSON audit log file path
audit_to_syslog true Also emit to syslog
audit_level 1 0=critical, 1=auth events, 2=all

Audit events include:

  • Authentication attempts (success/failure)
  • Authorization decisions
  • Rate limit triggers
  • User creation events

Webhook Notifications

For real-time security monitoring:

Setting Description
notify_enabled Enable webhooks
notify_url Webhook endpoint URL
notify_secret HMAC secret for webhook signatures

Configuration Security

Secrets Management

Setting Default Description
secrets_encrypted true Encrypt secrets at rest
secrets_use_keyring true Use kernel keyring
secrets_keyring_name "pam_llng" Keyring identifier

File Permissions

Recommended permissions:

File Permissions Owner
/etc/pam_llng.conf 0600 root
Server token file 0600 root
Cache directory 0700 root
Rate limit state dir 0700 root

Operational Security Considerations

Debug Logging Warning

CRITICAL: Never enable debug logging in production environments.

When log_level = debug, the module may log sensitive information to syslog:

  • SSH certificate metadata (key_id, serial, principals)
  • Token validation details
  • Authorization request parameters

Risk: If debug logs are captured by a log aggregator or accessed by unauthorized users, this information could be used to:

  • Identify infrastructure topology
  • Track user movements across systems
  • Correlate sessions for targeting

Recommendation:

  • Use log_level = warn or log_level = error in production
  • If debug logging is temporarily needed, ensure syslog access is restricted
  • Rotate and purge logs containing debug output promptly

Machine-ID Stability Requirement

The encryption key for cached tokens and secrets is derived from /etc/machine-id.

Impact of machine-id change:

  • All cached tokens become unreadable (automatic re-authentication required)
  • Encrypted secrets in the secret store become permanently unrecoverable
  • Server enrollment tokens must be re-issued

Scenarios causing machine-id change:

  • VM cloning without regenerating machine-id
  • System reinstallation
  • Container image reuse across hosts
  • Some cloud provider instance recreation

Recommendations:

  1. Document machine-id stability as a deployment requirement
  2. Before system migration: Backup enrollment tokens or plan for re-enrollment
  3. VM cloning: Always regenerate machine-id (systemd-machine-id-setup) and re-enroll
  4. Monitoring: Alert on machine-id changes via configuration management

Re-enrollment procedure after machine-id change:

# 1. The old token file is now unusable - remove it
rm /etc/security/pam_llng.token

# 2. Re-run enrollment
llng-pam-enroll --portal https://auth.example.com --client-id pam-access

Service Accounts Security

Service accounts (ansible, backup, deploy, etc.) are local accounts that authenticate via SSH key only, bypassing OIDC authentication. They are defined in a local configuration file.

Configuration File Security

Requirement Description
Ownership Must be owned by root (uid 0)
Permissions Must be 0600 (owner read/write only)
Symlinks File must not be a symlink (O_NOFOLLOW)
Location /etc/open-bastion/service-accounts.conf (configurable)

Account Validation

Service accounts are validated against the same security rules as regular users:

Field Validation
name Lowercase letters, digits, underscore, hyphen; max 32 chars
key_fingerprint Must start with SHA256: or MD5:, valid base64 chars only
shell Must be in approved_shells list
home Must match approved_home_prefixes
uid/gid Must be in valid range (0-65534)

SSH Server Requirement

Important: The SSH server must have ExposeAuthInfo yes in /etc/ssh/sshd_config:

# /etc/ssh/sshd_config
ExposeAuthInfo yes

This setting allows the PAM module to access the SSH key fingerprint via the SSH_USER_AUTH environment variable, which is required for fingerprint validation.

Authentication Flow

sequenceDiagram
    participant SA as Service Account
    participant SSH as SSH Server
    participant PAM as PAM Module

    SA->>SSH: SSH key authentication
    SSH->>PAM: pam_sm_authenticate
    Note over SSH: ExposeAuthInfo provides<br/>SSH_USER_AUTH with fingerprint
    PAM->>PAM: Extract fingerprint from SSH_USER_AUTH
    PAM->>PAM: Check service_accounts.conf
    PAM->>PAM: Validate fingerprint matches config
    Note over PAM: Fingerprint OK = authorized
    PAM-->>SSH: PAM_SUCCESS
    SSH-->>SA: Session established
Loading
  1. Service account connects via SSH with its configured key
  2. SSH server exposes key fingerprint via SSH_USER_AUTH (requires ExposeAuthInfo yes)
  3. PAM module extracts fingerprint and checks if user is in service_accounts.conf
  4. PAM module validates that the SSH key fingerprint matches the configured value
  5. If fingerprint matches, account is authorized locally (no LLNG call needed)
  6. sudo permissions are checked from the same configuration file

Security Benefits

Feature Benefit
Local configuration No network dependency for service accounts
Per-server control Each server explicitly lists allowed service accounts
SSH key binding Fingerprint validation prevents key substitution
Audit logging All service account access is logged
sudo control Fine-grained sudo permissions per account

Limitations

Limitation Mitigation
No centralized management Use configuration management (Ansible, Puppet)
Manual key rotation Implement key rotation procedures
Local file dependency Monitor file integrity with AIDE/Tripwire

Example Configuration

[ansible]
key_fingerprint = SHA256:abc123def456
sudo_allowed = true
sudo_nopasswd = true
gecos = Ansible Automation
shell = /bin/bash
home = /var/lib/ansible

Offline Credential Cache Security

The offline cache enables Desktop SSO authentication when the LLNG server is unreachable. This section describes the security architecture and considerations.

Cryptographic Design

Password Hashing (Argon2id):

Parameter Value Rationale
Memory cost 64 MiB Prevents GPU/ASIC attacks
Iterations 3 Balance of security and latency
Parallelism 4 Utilizes multi-core CPUs
Hash length 32 bytes 256-bit output
Salt length 16 bytes Unique per user, random

These parameters follow OWASP guidelines for high-security password storage.

Data Encryption (AES-256-GCM):

File format:
[Magic: OBCRED01 (8 bytes)][AES-256-GCM encrypted JSON]
Component Description
Algorithm AES-256-GCM authenticated encryption
Key source Root-only key file (/etc/open-bastion/cache.key), fallback to machine-id derivation
Key derivation PBKDF2-SHA256 (100,000 iterations) with per-cache-directory salt
IV 12 bytes, random per encryption
Auth tag 16 bytes, prevents tampering

GCM authentication ensures any tampering (bit flips, truncation) is detected and the entry is rejected.

Machine Binding

The encryption key is derived from a root-only key file or /etc/machine-id, ensuring:

  • Portability prevention: Cache files are useless on other machines
  • Cloning detection: VM clones with same machine-id must re-enroll
  • Hardware binding: Physical theft of disk provides no access without key file

Impact of machine-id/key change:

  • All cached credentials become permanently unreadable
  • Users must authenticate online to re-cache credentials
  • No security risk (encrypted data remains encrypted)

Cache Entry Lifecycle

stateDiagram-v2
    [*] --> Stored: Successful online auth
    Stored --> Verified: Correct password (offline)
    Stored --> Failed: Wrong password
    Failed --> Failed: Increment failures
    Failed --> Locked: Max attempts reached
    Locked --> Stored: Lockout expires
    Stored --> Expired: TTL exceeded
    Expired --> [*]: Entry removed
    Verified --> [*]: User authenticated
Loading

Brute Force Mitigation

Protection Description
Per-user lockout 5 failed attempts triggers lockout
Lockout duration 5 minutes
Failure persistence Stored encrypted in cache file (failed_attempts, locked_until)
Timing attack prevention Constant-time hash comparison

Note: The lockout thresholds (5 attempts, 5 minutes) are compile-time constants defined in offline_cache.h (OFFLINE_CACHE_MAX_FAILED_ATTEMPTS and OFFLINE_CACHE_LOCKOUT_DURATION). For environments requiring stricter lockout, recompile with lower thresholds (minimum recommended: 3 attempts, 15 minutes).

Lockout state is stored within the encrypted cache entry, ensuring it cannot be reset by file manipulation.

Error Codes

The greeter and PAM module communicate via structured error codes (must match include/offline_cache.h):

Code Constant Meaning
0 OFFLINE_CACHE_OK Success
-1 OFFLINE_CACHE_ERR_NOMEM Out of memory
-2 OFFLINE_CACHE_ERR_IO File system error
-3 OFFLINE_CACHE_ERR_CRYPTO Decryption failed
-4 OFFLINE_CACHE_ERR_NOTFOUND User not in cache
-5 OFFLINE_CACHE_ERR_EXPIRED Cache entry expired
-6 OFFLINE_CACHE_ERR_LOCKED Account locked out
-7 OFFLINE_CACHE_ERR_INVALID Invalid cache data
-8 OFFLINE_CACHE_ERR_PASSWORD Password mismatch

The PAM module sends structured messages (OFFLINE_ERROR:code[:locktime]) via PAM conversation so the greeter can display appropriate feedback.

File System Security

Requirement Implementation
Directory permissions 0700 (owner only)
File permissions 0600 (owner read/write)
Symlink protection O_NOFOLLOW on all opens
Race condition Atomic file operations (rename)
Filename SHA-256("cred:username") to prevent enumeration
Secure deletion shred used by admin tool

Security Boundaries

Threat Vector Protection
Cache file theft AES-256-GCM + machine/key binding
Memory analysis Secure memory clearing (explicit_bzero/sodium_memzero)
Timing attacks Constant-time comparison for hashes
Symlink attacks O_NOFOLLOW on all file operations
Race conditions Atomic file operations (rename)
Privilege escalation Cache directory is 0700 root-owned
Lockout bypass Lockout state encrypted in cache
User enumeration SHA-256 hashed filenames

Operational Considerations

When to enable offline mode:

  • Corporate workstations with network reliability concerns
  • Laptops used in areas with poor connectivity
  • Business continuity during LLNG maintenance

When NOT to enable offline mode:

  • High-security environments requiring real-time authorization
  • Shared/public workstations
  • Systems requiring immediate access revocation

Emergency procedures:

# Immediate user revocation
ob-cache-admin invalidate username

# Force online-only authentication
touch /etc/open-bastion/force_online

# Complete cache flush
ob-cache-admin invalidate-all

Administrative Controls

The ob-cache-admin tool provides secure cache management:

# List cached credentials (metadata only, no secrets)
ob-cache-admin list

# Show details for a specific user
ob-cache-admin show username

# Invalidate specific user's cache (secure deletion)
ob-cache-admin invalidate username

# Invalidate all cached credentials
ob-cache-admin invalidate-all

# Unlock a locked account
ob-cache-admin unlock username

# Remove invalid/orphaned files
ob-cache-admin cleanup

Security notes:

  • Requires root privileges (cache files owned by root)
  • Never displays passwords or hashes
  • Uses shred for secure file deletion
  • Unlock cannot modify encrypted lockout state directly; offers invalidation instead

Audit and Monitoring

Offline authentication events are logged to:

  • Syslog (via PAM)
  • Structured audit log (/var/log/open-bastion/audit.json)

Look for:

  • offline_auth_success: User authenticated via cache
  • offline_auth_failure: Failed offline authentication attempt
  • offline_cache_locked: User locked out due to failed attempts

Recommendations

  1. Generate a key file: Use ob-desktop-setup --offline or create manually (dd if=/dev/urandom of=/etc/open-bastion/cache.key bs=32 count=1)

  2. Set appropriate TTL: Default 7 days; reduce for high-security environments

  3. Enable disk encryption: Use LUKS or similar for the system drive

  4. Monitor lockouts: Check ob-cache-admin stats for unusual patterns

  5. Regular cleanup: Run ob-cache-admin cleanup periodically via cron

  6. Disable when not needed: Set auth_cache_enabled = false to eliminate the attack surface entirely

Network Revalidation

When a user authenticates offline and the network returns, the system revalidates the session via three mechanisms:

Mechanism Trigger Action
Screen unlock (PAM) User enters password Online LLNG auth attempted
Token refresh (Greeter) Screen unlock Refresh token exchanged for new access token
ob-session-monitor Periodic (60s) Check user validity via /pam/userinfo

Anti-firewall-bypass protection: If the SSO portal is unreachable despite network being available (possible local firewall manipulation), all offline sessions are terminated after offline_max_sso_unreachable seconds (default: 1h).

Threat Mitigations

Threat Mitigation
Token replay Single-use tokens, cache invalidation
MITM attacks TLS 1.3, certificate pinning
Brute force Rate limiting with exponential backoff
Cache tampering AES-256-GCM authenticated encryption
Path injection Strict path validation, approved lists
Buffer overflow Bounds-checked string operations, snprintf with null-termination
UID collision Fail-safe collision detection
Request tampering Optional HMAC request signing with nonces
Memory exhaustion DoS Response size limits (256KB), group limits (256 max)
Integer overflow Input validation in base64 encoding, backoff calculations
Malformed JSON Type validation for critical response fields
Client secret exposure JWT Client Assertion (RFC 7523) - secret never transmitted
Bastion bypass Bastion JWT verification on backends (RS256 signed)
Direct backend access JWT required + JWKS-based offline verification
Offline cache theft AES-256-GCM encryption + machine-id binding
Offline brute force Argon2id + per-user lockout after 5 attempts
Stale offline credentials Configurable TTL (default 7 days)

Security Reporting

To report security vulnerabilities, please email [email protected] with:

  1. Description of the vulnerability
  2. Steps to reproduce
  3. Potential impact
  4. Any suggested fixes (optional)

Response Timeline

Stage Timeline
Initial response Within 48 hours
Vulnerability assessment Within 7 days
Fix development Depends on severity
Public disclosure After fix is released

What to Expect

  • We will acknowledge your report within 48 hours
  • We will keep you informed of our progress
  • We will credit you in the security advisory (unless you prefer anonymity)
  • We will not take legal action against researchers who follow responsible disclosure

Supported Versions

Version Supported
1.x Yes
< 1.0 No

Only the latest minor version receives security updates. We recommend always running the latest release.

Security Disclosure Policy

  • Security issues are fixed in private and released as part of a new version
  • Security advisories are published after the fix is available
  • Critical vulnerabilities may receive expedited patches

Security Documentation

For detailed information about the security architecture and implementation:

Security Best Practices

When deploying Open Bastion:

  1. Use TLS 1.3 - Set min_tls_version = 13 in configuration
  2. Enable audit logging - Set audit_enabled = true for security monitoring
  3. Enable rate limiting - Enabled by default, protects against brute-force
  4. Restrict file permissions - Configuration files should be 0600 owned by root
  5. Use certificate pinning - For high-security environments, pin the LLNG server certificate
  6. Never enable debug logging in production - Debug logs may contain sensitive information

There aren’t any published security advisories