Skip to content

Latest commit

 

History

History
604 lines (436 loc) · 23.7 KB

File metadata and controls

604 lines (436 loc) · 23.7 KB

Security Architecture

This document describes the security measures implemented in the LemonLDAP::NG PAM module.

Overview

The PAM module authenticates users against a LemonLDAP::NG portal using a secure token-based protocol. Multiple layers of defense protect against various attack vectors.

Authentication Flow

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

Event Type Classification

Audit events use differentiated codes for SIEM integration:

Event Type Description
AUDIT_AUTH_SUCCESS Successful authentication
AUDIT_AUTH_FAILURE Failed authentication
AUDIT_AUTHZ_DENIED Authorization denied (valid user, no permission)
AUDIT_SECURITY_ERROR Cryptographic/security failure (invalid signature, malformed JWT)
AUDIT_RATE_LIMITED Rate limit triggered
AUDIT_USER_CREATED Local user account created
AUDIT_SERVER_ERROR Backend communication error

This classification enables security teams to distinguish between:

  • Authorization failures (user lacks permission) → AUDIT_AUTHZ_DENIED
  • Security incidents (attack attempt) → AUDIT_SECURITY_ERROR

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

Script Security

The shell scripts (ob-ssh-proxy, ob-enroll, ob-ssh-cert) implement security measures:

JSON Construction

Scripts use jq for JSON payload construction instead of string interpolation:

# Safe - uses jq argument passing
json_payload=$(jq -n --arg user "$user" --arg host "$host" '{user: $user, host: $host}')

# Unsafe - vulnerable to injection (NOT USED)
# json_payload="{\"user\": \"$user\"}"

This prevents JSON injection attacks where malicious input could break out of string context.

Configuration File Validation

Scripts verify configuration file security before sourcing:

# Check owner is root
# Check no group/world write permissions
# Refuse to source if checks fail

This prevents privilege escalation via malicious configuration injection.

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

SSH Key Policy

The PAM module can enforce restrictions on which SSH key types are allowed and their minimum sizes. This prevents connections using weak or deprecated cryptographic algorithms.

Configuration

Setting Default Description
ssh_key_policy_enabled false Enable SSH key type restrictions
ssh_key_allowed_types all Comma-separated list of allowed key types
ssh_key_min_rsa_bits 2048 Minimum RSA key size in bits
ssh_key_min_ecdsa_bits 256 Minimum ECDSA key size in bits

Supported Key Types

Type Algorithm Recommendation
ed25519 Ed25519 Recommended - Modern, fast, secure
sk-ed25519 Ed25519 with FIDO2 Recommended - Hardware-bound
sk-ecdsa ECDSA with FIDO2 Recommended - Hardware-bound
ecdsa ECDSA (P-256/P-384/P-521) Acceptable
rsa RSA Acceptable with ≥3072 bits
dsa DSA Deprecated - Should be disabled

Security Considerations

  • DSA keys: Should be disabled. DSA is considered deprecated and has fixed 1024-bit key size.
  • RSA keys: Should require at least 2048 bits, preferably 3072 bits for long-term security.
  • ECDSA keys: P-256 (256 bits) is the minimum recommended curve.
  • Ed25519 keys: Always 256 bits, no size configuration needed.
  • FIDO2/Security Keys: sk-ed25519 and sk-ecdsa provide hardware-bound private keys.

Example: High Security Configuration

ssh_key_policy_enabled = true
ssh_key_allowed_types = ed25519, sk-ed25519, sk-ecdsa

This configuration only allows Ed25519 keys and FIDO2 hardware security keys.

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
Weak SSH keys SSH key policy enforcement with type/size restrictions
Cache brute-force Rate limiting for offline cache lookups with exponential backoff