Configuration

Caddy Reverse Proxy

Overview

ASD uses Caddy as the local reverse proxy between your services and the outside world. Every request from a tunnel or local hostname passes through Caddy before reaching your application.

Internet --> ASD Cloud --> SSH Tunnel --> Caddy (local) --> Your Service
                                           |
                                      Routing, TLS,
                                      auth, headers

Caddy is configured declaratively through your asd.yaml. You never write a Caddyfile by hand. ASD generates the Caddy JSON configuration from your service definitions and applies it via the Caddy Admin API.

Commands

asd caddy start      # Start the Caddy reverse proxy
asd caddy stop       # Stop Caddy
asd caddy restart    # Restart with updated configuration
asd caddy config     # Show the current generated Caddy configuration

Caddy starts automatically when you run asd net apply --caddy or set auto_start_caddy: true in your features block. By default, Caddy listens on port 80 (HTTP) and port 443 (HTTPS when TLS is enabled). The admin API listens on localhost:2019.

How Caddy Fits In

ASD provides three access patterns for every service. Caddy controls two of them:

Access PatternGoes Through CaddyDescription
Local DirectNohttp://localhost:3000 — raw connection
Caddy RouteYeshttp://myapp.localhost — local routing
Tunnel RemoteYeshttps://myapp-abc123.eu1.tn.asd.engineer — public access

When Caddy is not running, only local direct access works. Tunnel traffic always flows through Caddy because the SSH tunnel terminates at the Caddy listener.

Routing Modes

Caddy supports two routing strategies. You can use both simultaneously for the same service.

Host-Based Routing

Each service gets its own hostname:

network:
  services:
    frontend:
      dial: "127.0.0.1:5173"
      host: "app.localhost"

Access: http://app.localhost/

Path-Based Routing

Multiple services share a single hostname, distinguished by URL path:

network:
  services:
    api:
      dial: "127.0.0.1:8080"
      paths: ["/api"]
      stripPrefix: true

Access: http://asd.localhost/api/

With stripPrefix: true, Caddy removes /api before forwarding to the upstream. Your API receives requests at / instead of /api/.

Route Priority

When multiple services have overlapping paths, use priority to control matching order. Higher values match first:

network:
  services:
    api-v2:
      dial: "127.0.0.1:8081"
      paths: ["/api/v2"]
      priority: 60
    api:
      dial: "127.0.0.1:8080"
      paths: ["/api"]
      priority: 50

Without priority, /api/v2/users might match the broader /api rule first.

Publish Preference

The publishPreferred property controls which routing strategy is used for tunnel URLs:

network:
  services:
    myapp:
      dial: "127.0.0.1:3000"
      host: "app.localhost"
      paths: ["/"]
      publishPreferred: "host"    # "host", "path", or "both"

TLS Configuration

Caddy handles TLS certificates for local HTTPS:

network:
  caddy:
    tls:
      enabled: true
      auto: true           # Self-signed certificates for *.localhost

For custom certificates:

network:
  caddy:
    tls:
      enabled: true
      cert_file: "/path/to/cert.pem"
      key_file: "/path/to/key.pem"

For automatic certificates from a public certificate authority (ACME protocol, used by Let’s Encrypt) with DNS challenge:

network:
  caddy:
    tls:
      enabled: true
      acme:
        email: "admin@example.com"
        dns_provider: "cloudflare"

Tunnel traffic already uses HTTPS via the ASD cloud. Local TLS is optional and primarily useful for testing HTTPS-only features like Secure cookies or HSTS.

Basic Authentication

HTTP Basic Auth adds a browser login prompt to services accessed via Caddy or tunnel.

Project-Wide

Protect all services:

network:
  caddy:
    basic_auth:
      enabled: true
      realm: "My Project"
      routes: ["host", "path", "tunnel"]

Credentials in .env:

ASD_BASIC_AUTH_USERNAME=admin
ASD_BASIC_AUTH_PASSWORD=your-secure-password

ASD hashes passwords with bcrypt before sending them to Caddy. Plaintext credentials never leave your machine.

Per-Service Override

Disable auth for a public API while keeping everything else protected:

network:
  caddy:
    basic_auth:
      enabled: true

  services:
    public-api:
      dial: "127.0.0.1:3000"
      basic_auth:
        enabled: false

    admin-panel:
      dial: "127.0.0.1:8080"
      basic_auth:
        enabled: true
        realm: "Admin Only"
        routes: ["host"]

Route Type Filtering

The routes property controls which access patterns require authentication:

Route TypeMeaning
hostHost-based routes (http://api.localhost)
pathPath-based routes (http://asd.localhost/api)
tunnelTunnel routes (https://myapp-xxx.eu1.tn.asd.engineer)

When routes is not specified, authentication applies to all types.

Security-Sensitive Services

Built-in services that provide shell or code access are flagged as security-sensitive:

ServiceWhyBehavior
ttyd (Web Terminal)Full shell accessAlways requires TTYD_USERNAME and TTYD_PASSWORD
code-server (VS Code)Code editing + terminalRecommends ASD_CODESERVER_AUTH=password

You can mark your own services as security-sensitive:

network:
  services:
    admin-tool:
      dial: "127.0.0.1:9000"
      securitySensitive: true

Security Headers

Add HTTP security headers per service:

network:
  services:
    production-api:
      dial: "127.0.0.1:3000"
      securityHeaders:
        enableHsts: true
        hstsMaxAge: 31536000
        frameOptions: "DENY"
        enableCompression: true
PropertyTypeDescription
enableHstsbooleanAdd Strict-Transport-Security header
hstsMaxAgenumberHSTS max-age in seconds (default: 1 year)
frameOptions"DENY" or "SAMEORIGIN"X-Frame-Options header value
enableCompressionbooleanEnable gzip/zstd response compression

Iframe Embedding

Allow a service to be embedded in an iframe from a specific origin:

network:
  services:
    dashboard:
      dial: "127.0.0.1:3000"
      iframeOrigin: "https://dashboard.asd.engineer"
      deleteResponseHeaders:
        - "Content-Security-Policy"
        - "X-Frame-Options"

deleteResponseHeaders strips headers from the upstream response before Caddy adds its own. This is needed when the upstream sets restrictive headers that conflict with your embedding requirements.

Set iframeOrigin: null to explicitly block iframe embedding.

Ingress Tagging

Tag routes with identifiers for monitoring:

network:
  services:
    api:
      dial: "127.0.0.1:8080"
      ingressTag: "production-api"

Caddy adds the tag as an X-ASD-Ingress response header. The default value comes from the ASD_INGRESS_TAG environment variable, or "local-caddy" if unset. Set ingressTag: null to disable the header.

Macro-Driven Configuration

Service definitions support ${{ }} template expressions that resolve at configuration time.

Environment Variables

network:
  services:
    app:
      dial: "127.0.0.1:${{ env.APP_PORT }}"
      host: "${{ env.APP_HOST }}"

Tunnel Credential Macros

These macros read from the credential registry and work across all authentication types (SSH keys, tokens, ephemeral):

MacroReturnsExample
${{ macro.tunnelHost("app") }}Full tunnel hostnameapp-fkmc.eu1.tn.asd.engineer
${{ macro.tunnelClientId() }}Short client IDfkmc
${{ macro.tunnelEndpoint() }}Server FQDNeu1.tn.asd.engineer
${{ macro.exposedOrigin("app") }}Full HTTPS origin URLhttps://app-fkmc.eu1.tn.asd.engineer
${{ macro.exposedOriginWithAuth("app") }}Origin with embedded credentialshttps://user:pass@app-fkmc.eu1.tn.asd.engineer

When no tunnel credentials exist (fresh install), these macros resolve to empty strings. Empty hosts are filtered out, so routes fall back to localhost only.

Utility Macros

MacroDescription
${{ macro.getRandomPort() }}Allocate a free port
${{ macro.getRandomString(length=32) }}Random alphanumeric string
${{ macro.bcrypt(password="...", cost=12) }}Generate a bcrypt hash
${{ core.isDockerAvailable() }}Returns true if Docker is running

Bcrypt in Auth Config

Use the bcrypt macro to hash passwords in your asd.yaml:

network:
  caddy:
    basic_auth:
      enabled: true
      password: "${{ macro.bcrypt(password='my-secret', cost=12) }}"

Built-in Service Paths

ASD’s built-in services use the /asde/ prefix to avoid collisions with your routes:

ServiceCaddy Path
Web Terminal (ttyd)/asde/ttyd/
VS Code Server/asde/codeserver/
Database UI (DbGate)/asde/dbgate/
Network Inspector/asde/mitmproxy/

These paths are registered automatically when you start the respective service.

Health Checks

ASD runs a multi-level health check cascade for each service. The asd net TUI shows green/red indicators based on these results. Checks run in order and stop at the first failure:

  1. Tunnel check — sends an HTTP request to the public tunnel URL. The X-Asd-Tunnel response header distinguishes “server alive, no client” (not-found) from “request forwarded” (forwarded).
  2. HTTP check — sends an HTTP request to the Caddy route (e.g., http://myapp.localhost). A 200 or 401 response means the service is reachable.
  3. TCP check — attempts a TCP connection to the service port. Confirms the process is listening.
  4. Process check — verifies the background process is still running (PID alive).

Press Ctrl+R in the asd net TUI to re-run all health checks, or use asd net refresh from the command line.

Related Guides