Expose
Overview
Expose is an open-source reverse tunnel server written in pure PHP, and a self-hosted alternative to hosted tunneling services such as ngrok. A small Expose client on a developer's machine opens a persistent outbound connection to the server. The server accepts inbound public HTTP requests and routes them back down that connection to whatever is running on the developer's laptop. A work-in-progress site, an API under development or a webhook receiver becomes reachable at a real public URL, without deploying anything and without opening a firewall port.
Running the tunnel server yourself keeps development traffic, webhook payloads and pre-release demos on infrastructure you control, under your own domain. It also removes the per-seat subscriptions, session time limits and rotating subdomains that hosted services impose.
This SecureAnyCloud image installs the official Expose server at /opt/expose/app. It runs as the expose-server service, bound to loopback 127.0.0.1:8080, behind nginx, which terminates TLS on port 443. Neither the dashboard password nor the tunnel token ever crosses the wire in plaintext.
Upstream Expose publishes a default dashboard login in its own configuration file, and by default it accepts a tunnel from anyone who can reach the server. This image does neither, and it contains no administrator credential at all. On each VM's first boot, expose-firstboot.service generates a unique dashboard password and a unique tunnel authentication token. It writes them to a root-only file, regenerates a per-VM TLS certificate, renders the nginx site and then hands over to the server.
Token validation is switched on, so only a client holding your token can open a tunnel. A start-time guard re-checks this on every service start and refuses to run if a published default credential is ever in effect. The appliance therefore fails closed rather than serving a known login.
What you get:
- Expose server (verified at 3.2.0) from the official upstream release
- The built-in Expose admin dashboard, served over TLS
expose-server.servicebound to loopback127.0.0.1:8080- nginx terminating TLS on
443, with80redirecting to HTTPS and serving an unauthenticated/healthzprobe expose-firstboot.servicefor the first-boot credentials, per-VM TLS certificate and nginx siteexpose-token-seed.service, which registers your per-VM tunnel token with the server- A unique per-VM dashboard password and tunnel token, in a root-only
0600file - Token validation enabled, so the server is never an open relay
- An Ubuntu 24.04 LTS base, patched at build time
- 24/7 SecureAnyCloud support, 24h response SLA
Prerequisites
You need an active Azure subscription, an SSH key pair, and a VNet with a subnet.
Expose proxies traffic rather than running your application, so it is light on resources. Standard_B2s (2 vCPU, 4 GB RAM) is sufficient for most teams.
To publish tunnels under your own hostnames, you also need a domain you control and the ability to add DNS records for it (Step 6). The server boots and the dashboard works without any DNS at all, so you can deploy first and add your domain later.
Deploy from the Azure Portal
Search the Marketplace for Expose on Ubuntu 24.04 and choose your VM size.
Attach an NSG that allows TCP 22 (SSH) from your management network and TCP 443 (dashboard and tunnel traffic) from the internet. Tunnel traffic must reach the VM from wherever your public URLs are consumed, so 443 generally needs to be open broadly. Port 80 only issues an HTTPS redirect and serves the health probe.
Deploy from the Azure CLI
Run the following on your own workstation, where the Azure CLI is installed:
az vm image terms accept --urn sac1647283583153:expose-ubuntu-24-04:default:latest
az vm create \
--resource-group my-resource-group \
--name my-expose-vm \
--image sac1647283583153:expose-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
# Open the ports the appliance uses
az vm open-port --resource-group my-resource-group --name my-expose-vm --port 443 --priority 1001
az vm open-port --resource-group my-resource-group --name my-expose-vm --port 80 --priority 1002Connect via SSH
ssh azureuser@<vm-ip>Verify the services are running
First boot generates your credentials and renders the nginx site before the tunnel server starts, so give the VM a few seconds after it boots. Check the services:
systemctl is-active expose-server nginx expose-firstbootExpected output:
active active activeFor a fuller view of the tunnel server, run:
systemctl status expose-server --no-pager | head -12The health probe answers without credentials, so a load balancer can watch it:
curl -s http://127.0.0.1/healthzExpected output:
okRead your per-VM credentials
Every VM generates its own dashboard password and tunnel token on first boot and writes them to a root-only file:
sudo cat /root/expose-info.txtEXPOSE_ADMIN_PASSWORD signs you in to the dashboard. EXPOSE_AUTH_TOKEN is what an Expose client presents to open a tunnel. Treat both as secrets. No two VMs share them, and neither is baked into the image.
Confirm that the tunnel server answers through the TLS front door:
curl -k -s -o /dev/null -w "%{http_code}\n" https://127.0.0.1/api/healthExpected output:
200Point your own domain at the server
Expose routes purely on the hostname of the incoming request. A tunnel published as demo is served at demo.your-domain.com. To use your own domain, add two DNS records that point at the VM's public IP:
your-domain.com A <your-vm-ip> *.your-domain.com A <your-vm-ip>The wildcard record lets every new tunnel get its own hostname without adding DNS each time. Then set the domain on the VM and re-render the certificate and nginx site:
sudo sed -i 's|^EXPOSE_DOMAIN=.*|EXPOSE_DOMAIN="your-domain.com"|' /etc/expose/expose.env
sudo /opt/expose/render-tls.sh
sudo /opt/expose/render-nginx.sh
sudo systemctl reload nginx
sudo systemctl restart expose-serverUntil you do this, the tunnel domain defaults to the VM's own address. That is enough to reach the dashboard but not to serve tunnel subdomains. /etc/expose/README.txt on the VM repeats these steps.
Sign in to the dashboard
Browse to https://<your-vm-ip>/ and sign in with the user admin and the EXPOSE_ADMIN_PASSWORD from Step 5. The dashboard is reachable by IP even before you configure DNS, because the appliance's nginx front door routes the VM's own address to the admin interface.
The Users page is where tunnel access tokens live. Your per-VM token is already registered there as sac-default.
The Settings page shows the security posture this image ships with. Require authentication tokens is enabled, so the server accepts a tunnel only from a client presenting a valid token.
Your browser trusts only the per-VM self-signed certificate at this point, so you will see a certificate warning until you complete Step 10.
Publish a local site through the tunnel
On your own workstation, not the VM, install the Expose client and point it at your server. The client is a single file:
curl -Ls https://github.com/exposedev/expose/releases/latest/download/expose -o expose chmod +x expose ./expose token <auth-token> ./expose share http://localhost:3000 --server-host=your-domain.com --server-port=443 --subdomain=demoUse the EXPOSE_AUTH_TOKEN from Step 5 as <auth-token>. The client prints the public URL it has been given.
Back in the dashboard, the Sites page now lists the live tunnel. It shows which local address the tunnel points at and which token opened it.
Anyone with the public URL now reaches the site running on your workstation. The page is served from a laptop, over a tunnel, through a server you own.
Confirm authentication is enforced
The admin API rejects anything without your per-VM credential. First, call it without credentials:
curl -k -s -o /dev/null -w "%{http_code}\n" https://127.0.0.1/api/usersExpected output:
401Then call it with the per-VM password from Step 5:
curl -k -s -o /dev/null -w "%{http_code}\n" -u "admin:<admin-password>" https://127.0.0.1/api/usersExpected output:
200The upstream project's published default login is rejected with 401. That credential does not exist on this image, and the expose-server service refuses to start if it is ever put back in place.
Enable a real TLS certificate
The image generates a self-signed certificate per VM, so the dashboard and tunnels are encrypted immediately. Once your DNS from Step 6 resolves, replace it with a publicly trusted wildcard certificate. A wildcard is required because every tunnel gets its own subdomain, and it must be issued with a DNS-01 challenge:
sudo snap install --classic certbot
sudo certbot certonly --manual --preferred-challenges dns \
-d your-domain.com -d '*.your-domain.com'
sudo cp /etc/letsencrypt/live/your-domain.com/fullchain.pem /etc/expose/tls/expose.crt
sudo cp /etc/letsencrypt/live/your-domain.com/privkey.pem /etc/expose/tls/expose.key
sudo chown root:expose /etc/expose/tls/expose.crt /etc/expose/tls/expose.key
sudo chmod 0644 /etc/expose/tls/expose.crt
sudo chmod 0640 /etc/expose/tls/expose.key
sudo systemctl reload nginxPersistence, first boot and updates
Registered tunnel tokens, connected site history and request logs live in a SQLite database at /var/lib/expose/expose.db, which is created on first boot.
Your credentials live in /etc/expose/expose.env, which the service reads, and in /root/expose-info.txt, which is your copy. Configuration that is not secret lives in /etc/expose/expose-server.php. That file deliberately contains no credential and reads the dashboard login from expose.env at runtime.
To add another tunnel token, select Add user on the dashboard's Users page, or call POST /api/users. To rotate the dashboard password, edit EXPOSE_ADMIN_PASSWORD in /etc/expose/expose.env and restart expose-server. The start-time guard rejects short or published default values, so choose a long random password.
The base image has unattended security upgrades enabled. Expose itself is pinned to the release verified for this image. To upgrade it, replace /opt/expose/app from a newer upstream release.
Support
Every SecureAnyCloud image includes 24/7 support with a 24h response SLA. Contact sales@secureanycloud.com, quoting the image name and your Azure VM ID.
Deploy on Azure
Launch Expose on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com