Network & Security

Cloudillo’s network architecture emphasizes security, privacy, and ease of deployment. The system uses modern TLS with automatic certificate management via ACME (Let’s Encrypt) and supports both HTTP/1.1 and HTTP/2.

TLS/HTTPS Architecture

Rustls Integration

Cloudillo uses Rustls, a modern, memory-safe TLS library written in pure Rust:

Advantages:

  • Memory-safe: No buffer overflows or memory corruption
  • No unsafe code: Aligns with Cloudillo’s #![forbid(unsafe_code)]
  • Modern protocols: TLS 1.2 and TLS 1.3
  • High performance: Zero-copy design
  • Small footprint: Minimal dependencies

Version:

[dependencies]
rustls = "0.23"
tokio-rustls = "0.26"

HTTP/2 Support

Cloudillo supports HTTP/2 via ALPN (Application-Layer Protocol Negotiation):

TLS Configuration Steps:
1. Create ServerConfig builder with no client auth
2. Set certificate resolver
3. Configure ALPN protocols:
   - "h2" (HTTP/2, preferred)
   - "http/1.1" (fallback for compatibility)
4. Client selects protocol during TLS handshake

Benefits:

  • Multiplexing (multiple requests over single connection)
  • Server push (future feature)
  • Header compression
  • Binary protocol efficiency

SNI (Server Name Indication)

SNI enables hosting multiple domains on one IP address:

CertResolver Structure:

  • certs: RwLock - In-memory cache of domain → CertifiedKey
  • auth_adapter: Reference to persistent certificate storage

Resolution Algorithm:

  1. Extract domain from ClientHello SNI field
  2. Look up domain in in-memory cache
  3. Return cached certificate if found
  4. (On-demand loading if not cached - see Dynamic Loading)

Flow:

Client → TLS ClientHello with SNI="alice.example.com"
  ↓
CertResolver extracts "alice.example.com"
  ↓
Looks up certificate for alice.example.com
  ↓
Returns appropriate certificate
  ↓
TLS handshake completes

ACME (Let’s Encrypt) Integration

Automatic Certificate Management

Cloudillo uses instant-acme for automatic certificate provisioning:

Algorithm: Request ACME Certificate

1. Create ACME account:
   - Contact email: mailto:{email}
   - Agree to terms of service
   - Connect to Let's Encrypt production server

2. Create order for domain
   - Specify domain identifiers

3. Get authorization challenges
   - Find HTTP-01 challenge (not DNS-01)
   - Extract challenge token

4. Store challenge response in AuthAdapter
   - key: challenge.token
   - value: key_authorization(challenge)

5. Validate challenge
   - ACME server verifies HTTP endpoint

6. Poll for order completion (max 10 attempts, 5s intervals)
   - Retry until OrderStatus::Ready

7. Generate Certificate Signing Request (CSR)
   - Create certificate parameters for domain
   - Serialize to DER format

8. Finalize order with CSR

9. Download signed certificate chain

10. Store certificate in AuthAdapter
    - Persist for reuse and renewal

HTTP-01 Challenge

Cloudillo uses HTTP-01 challenge for domain validation:

Challenge Endpoint:

HTTP Route: GET /.well-known/acme-challenge/{token}

Handler Algorithm:
1. Extract token from URL path
2. Query AuthAdapter for stored challenge response
3. Return challenge value (plain text)

This endpoint must be publicly accessible over HTTP (port 80)
for ACME validation.

Challenge Flow:

ACME server → GET http://example.com/.well-known/acme-challenge/{token}
  ↓
Cloudillo → Lookup token in AuthAdapter
  ↓
Cloudillo → Return challenge response
  ↓
ACME server validates
  ↓
Domain ownership confirmed
  ↓
Certificate issued

Certificate Renewal

Certificates auto-renew before expiration:

Algorithm: Auto-Renew Certificates (runs every 12 hours)

1. Loop continuously:
   a. Sleep 12 hours
   b. Get all domains from AuthAdapter
   c. For each domain:
      - Read certificate from storage
      - Parse expiration date
      - Calculate days until expiry
      - If < 30 days:
        * Request new certificate via ACME
        * On success:
          - Log certificate renewed
          - Update in-memory certificate cache
        * On error:
          - Log error
          - Continue to next domain

Certificate Storage

Certificates stored in AuthAdapter via trait interface:

AuthAdapter Methods:

  • create_cert(domain, cert_chain) - Store new certificate chain
  • read_cert(domain) - Retrieve certificate (returns Option<Vec>)
  • delete_cert(domain) - Remove certificate
  • list_domains() - Get all registered domains

SQLite Schema:

CREATE TABLE certificates (
    domain TEXT PRIMARY KEY,
    cert_chain BLOB NOT NULL,
    private_key BLOB NOT NULL,
    created_at INTEGER NOT NULL,
    expires_at INTEGER NOT NULL
);

CREATE INDEX idx_certs_expiry ON certificates(expires_at);

Certificate Resolver

In-Memory Cache

Certificates cached in memory for performance:

Data Structures:

  • App.certs: RwLock<HashMap<domain, CertifiedKey»
  • CertResolver.certs: RwLock<HashMap<domain, CertifiedKey»
  • CertResolver.auth_adapter: Persistent storage reference

load_certificate(domain) Algorithm:

  1. Query AuthAdapter for certificate bytes
  2. Parse certificate chain and private key
  3. Acquire write lock on certs
  4. Insert domain → CertifiedKey into cache

reload_all() Algorithm:

  1. Get all domains from AuthAdapter
  2. For each domain, call load_certificate
  3. Repopulate entire in-memory cache

Dynamic Loading

Certificates loaded on-demand if not in cache:

Algorithm: ResolvesServerCert::resolve(client_hello)

1. Extract domain from ClientHello SNI
2. Try read lock on cache:
   - If domain found: Return cached certificate
   - If not found: Continue to step 3

3. Load from storage (async I/O in blocking task):
   a. Read certificate bytes from AuthAdapter
   b. Parse certificate and key
   c. Acquire write lock on cache
   d. Insert into cache
   e. Return certificate

This pattern ensures:
- Fast path (cached): Lock-free read
- Slow path (on-demand): Blocking async I/O off async runtime

Server Architecture

Dual-Server Setup

Cloudillo runs two servers:

HTTPS Server (primary, always running):

Algorithm: HTTPS Server

1. Configure TLS with Rustls:
   - No client authentication
   - Dynamic certificate resolver (SNI-based)
2. Bind TCP listener to configured address (e.g., 0.0.0.0:443)
3. Accept connections in loop:
   a. Accept TCP stream
   b. Upgrade to TLS connection
   c. Spawn async task to handle connection

HTTP Server (conditional, for ACME only):

Algorithm: HTTP Server (if ACME enabled)

1. Bind TCP listener to 0.0.0.0:80
2. Spawn separate async task to run in parallel
3. Accept connections in loop:
   a. Accept TCP stream
   b. Parse HTTP request
   c. If path is /.well-known/acme-challenge/{token}:
      - Handle ACME challenge request
   d. Else:
      - Send HTTP 301 redirect to HTTPS

Graceful Shutdown

Algorithm: Server Shutdown

1. Create oneshot channel for shutdown signal (tx, rx)
2. Spawn server task (runs in background)
3. Wait for either:
   a. Ctrl+C signal (SIGINT), or
   b. Shutdown signal sent to tx
4. On shutdown trigger:
   a. Log "Shutting down gracefully..."
   b. Abort server task
   c. Allow in-flight connections to complete
   d. Return success

This allows clean shutdown without dropping active requests.

Cryptography

Algorithms

TLS:

  • TLS 1.2 (minimum)
  • TLS 1.3 (preferred)
  • Cipher suites: Modern, secure defaults from Rustls

Action Signatures:

  • Algorithm: ES384 (ECDSA with P-384 and SHA-384)
  • Key size: 384 bits
  • Signature size: ~96 bytes

Content Hashing:

  • Algorithm: SHA256
  • Used for: File IDs, action IDs, content addressing
  • Output: 256 bits (32 bytes)

Password Hashing:

  • Algorithm: bcrypt
  • Work factor: 12 (default)
  • Salt: Random, unique per password

Key Management

Profile Signing Keys (ES384):

Algorithm: Generate and Store Profile Key

1. Generate random key pair using P-384 curve
   - Use OS random number generator
   - Both signing_key (private) and verifying_key (public)

2. Store in AuthAdapter:
   - tenant_id: Identifies user/organization
   - key_id: Identifies specific key version
   - public_key: Shared publicly, used for verification
   - private_key: Kept secret, used for signing

TLS Certificates (RSA or ECDSA):

  • Generated by Let’s Encrypt
  • Stored in AuthAdapter
  • Auto-renewed before expiration

Secure Random Generation

Algorithm: Generate Secure Session ID

1. Request 32 random bytes from OS entropy source (OsRng)
2. Encode bytes as URL-safe base64 string
3. Return base64-encoded session ID

This produces 256-bit random session identifiers suitable for
authentication tokens and nonces.

Security Best Practices

No Unsafe Code

Cloudillo enforces memory safety:

#![forbid(unsafe_code)]

All dependencies must also be free of unsafe code where possible.

Input Validation

URL Validation Algorithm:

  1. Parse URL string
  2. Verify scheme is “https” (reject “http”, “ftp”, etc.)
  3. Extract domain/host from URL
  4. Validate domain format (valid characters, not IP, proper TLD)
  5. Return error if any check fails

JWT Validation Algorithm:

  1. Decode token (checks base64 format)
  2. Verify signature using ES384 algorithm with public key
  3. Validate expiration time (exp claim < current time)
  4. Extract and return claims

Size Limits:

  • MAX_REQUEST_SIZE: 100 MB (entire HTTP request)
  • MAX_JSON_SIZE: 10 MB (JSON request body)
  • MAX_ACTION_SIZE: 1 MB (individual action token)

Rate Limiting

Per-IP Rate Limiter Algorithm:

Data: RateLimiter with HashMap<IpAddr → VecDeque<Instant>>

check(ip) Algorithm:
1. Acquire write lock on limits map
2. Get or create rate limit entry for IP
3. Get current time
4. Remove all requests older than 1 minute
5. If requests >= max_per_minute: return false (rate limited)
6. Record new request timestamp
7. Return true (request allowed)

This sliding-window approach counts requests in the last 60 seconds.

Per-User Rate Limits (hardcoded):

  • MAX_ACTIONS_PER_HOUR: 100 actions (limit posting/interactions)
  • MAX_FILE_UPLOADS_PER_HOUR: 50 uploads (limit bandwidth)
  • MAX_DB_QUERIES_PER_MINUTE: 1000 queries (limit database load)

CORS Configuration

CORS Layer Configuration:

1. Allow Origin: Mirror request (echo Origin header)
   - Allows any origin to make requests
   - Each response includes Access-Control-Allow-Origin: <requesting origin>

2. Allowed Methods: GET, POST, PATCH, DELETE
   - Other methods (PUT, HEAD) are rejected

3. Allowed Headers: Authorization, Content-Type
   - Custom headers must be explicitly allowed

4. Credentials: Enabled
   - Allows cookies and credentials in cross-origin requests

5. Max Age: 3600 seconds (1 hour)
   - Browsers cache CORS preflight responses for 1 hour

6. Apply to API routes
   - All routes inherit CORS configuration

Monitoring & Logging

Structured Logging

Tracing Library Format:

  • Each log entry includes message + contextual fields
  • Fields use structured format (field = value)
  • Levels: debug, info, warn, error

Request Logging Example:

  • method: HTTP verb
  • path: Request URI
  • remote_addr: Client IP
  • Message: “Handling request”

Error Logging Example:

  • error: Exception message
  • context: Category (e.g., “certificate_renewal”)
  • domain: Relevant domain

Security Events

Key security-related events logged with structured context:

Rate Limit Hit:

  • ip: Client IP address
  • reason: “rate_limit_exceeded”

Token Rejection:

  • token_issuer: Who signed the token
  • reason: “signature_verification_failed”

Authorization Failure:

  • ip: Client IP
  • path: Request URL
  • reason: “permission_denied”

Metrics

SecurityMetrics Structure:

  • failed_auth_attempts (AtomicU64) - Failed login attempts
  • rate_limit_hits (AtomicU64) - Rate limit violations
  • cert_renewals (AtomicU64) - Successful certificate renewals
  • signature_failures (AtomicU64) - Token signature verification failures
  • blocked_ips (AtomicUsize) - Currently rate-limited IP addresses

Deployment Considerations

Firewall Configuration

Required Ports:

  • 443 (HTTPS) - Primary server
  • 80 (HTTP) - ACME challenges only (optional redirect to HTTPS)

Firewall Rules:

# Allow HTTPS
sudo ufw allow 443/tcp

# Allow HTTP for ACME
sudo ufw allow 80/tcp

# Deny all other inbound
sudo ufw default deny incoming
sudo ufw default allow outgoing

Reverse Proxy

If using nginx or similar:

server {
    listen 443 ssl http2;
    server_name *.example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass https://localhost:8443;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # WebSocket support
    location /ws/ {
        proxy_pass https://localhost:8443;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Docker Deployment

FROM rust:1.75 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates
COPY --from=builder /app/target/release/basic-server /usr/local/bin/

EXPOSE 8443 80

CMD ["basic-server"]

Docker Compose:

version: '3.8'

services:
  cloudillo:
    build: .
    ports:
      - "443:8443"
      - "80:80"
    environment:
      MODE: standalone
      BASE_ID_TAG: example.com
      ACME_EMAIL: admin@example.com
      DB_DIR: /data/db
      DATA_DIR: /data/blobs
    volumes:
      - ./data:/data
    restart: unless-stopped

Security Checklist

Before Deployment

  • TLS certificates configured (ACME or manual)
  • Firewall rules in place
  • Rate limiting enabled
  • CORS properly configured
  • Logging enabled
  • Backups configured
  • Password policies enforced
  • Security headers set

Ongoing Maintenance

  • Monitor certificate expiration
  • Review security logs weekly
  • Update dependencies monthly
  • Backup data daily
  • Test disaster recovery quarterly
  • Audit access controls quarterly
  • Review rate limits as needed

See Also