Tunnels

How to Tunnel Securely with Caddy and Basic Authentication

Published:
Kelvin Wuite
By Kelvin Wuite • 6 min read
Share
How to Tunnel Securely with Caddy and Basic Authentication

When you expose a local service with asd expose, it gets a public HTTPS URL. By default, anyone with that URL can access your service. ASD's built-in Caddy integration lets you add HTTP Basic Authentication with a few lines in asd.yaml — no manual Caddy configuration, no bcrypt hashing, no separate reverse proxy setup.

How ASD handles Caddy

ASD manages a local Caddy reverse proxy automatically. When you define services in asd.yaml, ASD generates Caddy routes, handles routing, and applies authentication. You never touch a Caddyfile directly.

Traffic flows like this:

Internet → ASD Cloud → SSH Tunnel → Caddy (local) → Your Service
                                       ↑
                                  Security layer

Prerequisites

  • ASD CLI installed (asd init to set up your workspace)
  • Authenticated with ASD (asd login)
  • A local service running (we use port 3000 in this guide)

Step 1: Configure your service with basic auth

Add your service to asd.yaml with a basic_auth block. You can enable it globally or per-service:

version: 1
project:
  name: my-project
  domain: localhost

network:
  caddy:
    enable: true
    basic_auth:
      enabled: true
      realm: "My Project"

  services:
    my-app:
      dial: "127.0.0.1:3000"
      host: "myapp.localhost"

The basic_auth block under caddy protects all services. ASD reads credentials from your .env file:

ASD_BASIC_AUTH_USERNAME=admin
ASD_BASIC_AUTH_PASSWORD=your-secure-password

These are set automatically when you run asd init. The password is auto-generated with random characters by default.

Step 2: Apply the network configuration

Apply the configuration and start Caddy:

asd net apply --caddy

This generates Caddy routes with the authentication handler, bcrypt-hashes the password automatically, and applies everything to the running Caddy instance.

Step 3: Verify with the network TUI

Open the ASD network dashboard to verify your service is running and protected:

asd net

You should see your service with a green checkmark. Use these keyboard shortcuts:

KeyAction
TabCycle tabs (Services, Projects, Logs)
EnterActions menu for selected service
Ctrl+RRefresh health status
Ctrl+QQuit

Step 4: Expose via tunnel

Expose the service through an ASD tunnel. Because Caddy sits in front of your service, the tunnel endpoint is already protected:

asd expose 3000 myapp

The second argument is the subdomain prefix for your tunnel URL.

Output:

Local:  http://localhost:3000
Caddy:  http://myapp.localhost
Tunnel: https://myapp-abc123.cicd.eu1.asd.engineer

Anyone accessing the tunnel URL will see a Basic Auth prompt before reaching your service.

Per-service credentials with macros

Each service can have its own credentials, auto-generated via macros. First, define the variables in your tpl.env template:

# tpl.env — macros expanded during asd init / asd env-init
ASD_BASIC_AUTH_USERNAME=admin
ASD_BASIC_AUTH_PASSWORD=getRandomString(length=32,charset=safe)

# Custom credentials for admin panel
ADMIN_PANEL_USERNAME=admin
ADMIN_PANEL_PASSWORD=getRandomString(length=32,charset=safe)

After asd init, your .env has real values:

# .env (generated)
ASD_BASIC_AUTH_USERNAME=admin
ASD_BASIC_AUTH_PASSWORD=k7mx9pqr2st8vw1yb4cf6dh3jn5q8z2a

ADMIN_PANEL_USERNAME=admin
ADMIN_PANEL_PASSWORD=p3rk8tn2wq5xj7mv9cf4yd6ah1bs0e3g

Then reference those credentials in asd.yaml per service. This lets you mix public and protected services, each with separate credentials:

network:
  caddy:
    enable: true
    basic_auth:
      enabled: true
      realm: "My Project"

  services:
    public-api:
      dial: "127.0.0.1:3000"
      host: "api.localhost"
      basic_auth:
        enabled: false              # No auth for this service

    admin-panel:
      dial: "127.0.0.1:8080"
      host: "admin.localhost"
      basic_auth:
        enabled: true
        realm: "Admin Only"
        username: "${ADMIN_PANEL_USERNAME}"
        password: "${ADMIN_PANEL_PASSWORD}"

The global ASD_BASIC_AUTH_* credentials protect all other services. The admin panel uses its own separate credentials from .env. To rotate credentials, run asd env-init --override --yes and all macros regenerate.

Selective route protection

By default, basic auth applies to all route types. You can restrict it to specific route types:

network:
  caddy:
    basic_auth:
      enabled: true
      routes:
        - host                  # Only protect host-based routes

Macros: auto-generated credentials and ports

ASD uses macros in tpl.env templates to auto-generate secure values during asd init. You never need to generate passwords manually — ASD does it for you.

# tpl.env (template, expanded during asd init)
ASD_BASIC_AUTH_USERNAME=admin
ASD_BASIC_AUTH_PASSWORD=getRandomString(length=32,charset=safe)

After running asd init, your .env contains a real random value:

# .env (generated)
ASD_BASIC_AUTH_USERNAME=admin
ASD_BASIC_AUTH_PASSWORD=k7mx9pqr2st8vw1yb4cf6dh3jn5q8z2a

The same macro system handles port allocation to prevent conflicts when running multiple projects or parallel CI:

# tpl.env — ports auto-allocated, no conflicts
ASD_CADDY_PORT_HTTP=getRandomPort(min=30000,max=65000)
ASD_CADDY_PORT_HTTPS=getRandomPort(min=30000,max=65000)
ASD_CADDY_ADMIN_PORT=getRandomPort(min=30000,max=65000)

Available macros:

MacroDescription
getRandomString(length,charset) Generate a secure random string (charsets: safe, alnum, hex, alpha)
getRandomPort(min,max) Allocate an unused port (OS-level check, no conflicts)
getRandomPorts(n,sep) Allocate multiple ports at once
getPortRange(size,min,max) Reserve a contiguous range of ports

To regenerate all macro values (e.g., rotate passwords):

asd env-init --override --yes

If you prefer to set your own password manually, generate one with:

openssl rand -base64 24

Then set it in your .env file directly. ASD will use whatever value is in .env, whether macro-generated or manually set.

Summary

ASD's Caddy integration handles authentication as part of your service configuration. Define basic_auth in asd.yaml, apply with asd net apply --caddy, and expose with asd expose. No manual Caddyfiles, no bcrypt hashing by hand, no separate reverse proxy setup. The entire flow — from local routing to public HTTPS with authentication — is managed through the ASD CLI.

Kelvin Wuite
Written by

Kelvin Wuite

Kelvin Wuite is the founder of Accelerated Software Development B.V. With over eighteen years of development experience, he has witnessed the same patterns repeat across every software team: endless documentation, manual preparation, environment mismatches, and fragmented collaboration. His drive is to remove these barriers, enabling engineers to work together in unified environments with shorter feedback loops and hands-on collaboration. Since 2015 he has been refining these ideas, leading to ASD — a platform designed to create a faster, more integrated way for development teams to collaborate in an age where AI is thriving.

Related Articles