Networking Azure

Wiredoor Ingress as a Service on Ubuntu 24.04 on Azure User Guide

| Product: Wiredoor Ingress as a Service on Ubuntu 24.04 LTS on Azure

Overview

Wiredoor is a self-hosted ingress-as-a-service platform. It solves the problem of reaching a service that has no public address: an application on a home lab, a machine behind carrier-grade NAT, a Kubernetes cluster with no load balancer, an IoT gateway on a customer site. Instead of forwarding ports or opening a firewall inwards, you install the Wiredoor CLI on the private machine and it dials outbound to this server over a WireGuard tunnel. Wiredoor then publishes the service through its own nginx reverse proxy on a public domain or path, with TLS, access control and monitoring, and traffic travels back down the encrypted tunnel. Nothing inbound is ever opened on the private network.

The cloudimg image installs Docker CE from the official Docker repository and runs Wiredoor 1.7.3 as a container pinned by digest. The container bundles nginx, the control-plane API, WireGuard, oauth2-proxy and certbot under supervisord, so there is no separate host web server to configure. A unique admin password and the public WireGuard endpoint are generated on the first boot of every VM, and the image itself ships with no credentials and no key material of any kind. Backed by 24/7 cloudimg support.

What is included:

  • Wiredoor 1.7.3 (ghcr.io/wiredoor/wiredoor:v1.7.3, Apache-2.0) running as a container managed by systemd + Docker
  • Docker CE preinstalled from the official Docker apt repository
  • A WireGuard tunnel on UDP 51820, the public endpoint auto-detected on first boot
  • HTTP 80 (redirecting to HTTPS) and HTTPS 443 ingress, plus the optional TCP service range 32760-32767
  • A web admin dashboard for nodes, domains, services and access logs
  • A unique admin password generated on first boot and recorded in a root-only file
  • Automatic Let's Encrypt certificates for public domains, and a self-signed certificate for IP-based access
  • Persistent IP forwarding and WireGuard module loading, so the tunnel survives a reboot
  • docker.service + wiredoor.service as systemd units, enabled and active
  • 24/7 cloudimg support

Security posture of this image

Because Wiredoor's whole purpose is to publish private services to the internet, the appliance itself ships closed:

  • No baked key material. Wiredoor generates the WireGuard server private key, the JWT signing secret, the self-signed TLS keypair and the QUIC host key lazily on first container start, inside the data volume. The image ships that volume empty, so every key is unique to your VM.
  • No default administrator. The admin credential lives only in /opt/wiredoor/.env, which does not exist in the image. Wiredoor 1.7.3 refuses to start if the administrator password is missing or is still the upstream sample value.
  • Nothing serves before lockdown. wiredoor.service is gated on a bootstrap marker that first boot creates only after your per-VM secret has been written, so the dashboard and the ingress cannot answer a request until the VM has its own credentials.
  • No pre-seeded peers. There are no WireGuard peers or client configurations in the image; you register your own nodes.

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting point. Because Wiredoor is the entry point for all inbound traffic, the VM needs a public IP address (or a DNS name pointing at it) that your private nodes and your users can reach.

NSG inbound rules: allow 22/tcp from your management network, 80/tcp and 443/tcp for ingress and for Let's Encrypt validation, and 51820/udp for the WireGuard tunnel - without the UDP rule the dashboard works while no node can connect. Add 32760-32767/tcp only if you intend to expose raw TCP services.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Wiredoor by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22), HTTP (80) and HTTPS (443). The WireGuard UDP 51820 rule is added after creation in Step 2. Then Review + create -> Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name wiredoor \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard

# Open ingress (TCP 80/443) and the WireGuard tunnel (UDP 51820)
az vm open-port --resource-group <your-rg> --name wiredoor --port 80    --priority 1010
az vm open-port --resource-group <your-rg> --name wiredoor --port 443   --priority 1020
az vm open-port --resource-group <your-rg> --name wiredoor --port 51820 --priority 1030

WireGuard is a UDP protocol - make sure the 51820/udp rule actually reaches the VM, or the dashboard will load while no node can establish a tunnel.

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4 - Confirm the services and the tunnel are up

systemctl is-active docker.service wiredoor.service
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
docker exec -u root wiredoor wg show
ss -tuln | grep -E ':(80|443|51820)\b' | awk '{print $1, $5}' | sort -u

Both units report active, the container runs the digest-pinned image, and wg show reports the wg0 interface listening on port 51820 with a public key generated on this VM:

active
active
NAMES      IMAGE                              STATUS
wiredoor   ghcr.io/wiredoor/wiredoor:v1.7.3   Up 4 minutes
interface: wg0
  public key: CHCDuCmLi5HWqSGkjN70RfpqYz8f7RSAn3aWjtEUiy8=
  private key: (hidden)
  listening port: 51820
tcp 0.0.0.0:443
tcp 0.0.0.0:80
tcp [::]:443
tcp [::]:80
udp 0.0.0.0:51820
udp [::]:51820

wg show must be run as root inside the container - the container's own unprivileged user cannot read the interface.

docker.service and wiredoor.service both active, the Wiredoor container running the digest-pinned v1.7.3 image, the WireGuard wg0 interface listening on port 51820 with a per-VM public key, and TCP 80/443 plus UDP 51820 published on the host

Step 5 - Retrieve your admin password and tunnel endpoint

The admin password is generated uniquely on the first boot of your VM and written to a root-only file:

sudo sed -E 's/^(WIREDOOR_PASSWORD=).*/\1********/' /root/wiredoor-credentials.txt | head -8

The password is masked here only so it stays out of this page; on your VM the real value is on that line.

# Wiredoor - generated on first boot by wiredoor-firstboot.service
# These credentials are unique to this VM. Store them somewhere safe.

WIREDOOR_URL=https://20.169.218.14/
WIREDOOR_USERNAME=admin@wiredoor.local
WIREDOOR_PASSWORD=********
WIREGUARD_ENDPOINT=20.169.218.14:51820/udp

Read the whole file (including the notes at the bottom) with sudo cat /root/wiredoor-credentials.txt.

WIREDOOR_USERNAME is the sign-in name. WIREGUARD_ENDPOINT is the address your private nodes will dial, auto-detected on first boot; if this VM sits behind a NAT gateway or you use a DNS name, correct VPN_HOST in /opt/wiredoor/.env and restart the service.

The Docker version, the digest-pinned Wiredoor image reference, and the per-VM credentials file showing the generated admin password, the dashboard URL and the auto-detected public WireGuard endpoint

Step 6 - Verify the credentials and the ingress

The image ships a checker that proves the per-VM credential works end to end and that anonymous and forged requests are rejected:

sudo /usr/local/sbin/wiredoor-credential-check.sh
OK wiredoor credential round-trip verified (401 anon, 401 forged, wrong-pw rejected, per-VM password mints a working JWT)

You can confirm the ingress separately - port 80 redirects to HTTPS, the dashboard answers on 443, and the API rejects an unauthenticated call:

printf 'http  -> %s\n'        "$(curl -s  -o /dev/null -w '%{http_code}' -m 10 http://127.0.0.1/)"
printf 'https -> %s\n'        "$(curl -ks -o /dev/null -w '%{http_code}' -m 10 https://127.0.0.1/)"
printf 'api (no token) -> %s\n' "$(curl -ks -o /dev/null -w '%{http_code}' -m 10 https://127.0.0.1/api/auth/me)"
http  -> 301
https -> 200
api (no token) -> 401

Sign-in is rate limited to 5 attempts per 10 minutes per source IP. The checker above uses two of them. If you see Too many attempts, try again later, wait for the window to clear rather than retrying.

The appliance shipping closed, the credential round-trip check reporting OK for anonymous 401, forged-token 401, wrong-password rejection and a working per-VM JWT, and the ingress returning 301 on HTTP, 200 on HTTPS and 401 on the unauthenticated API

Step 7 - Sign in to the dashboard

Open https://<vm-public-ip>/ in a browser and sign in with the WIREDOOR_USERNAME and WIREDOOR_PASSWORD from Step 5.

Until you attach a public domain, the dashboard is served with a self-signed certificate, so your browser will warn once. That is expected for IP-based access; Step 11 replaces it with a real Let's Encrypt certificate.

The Wiredoor sign-in page served over HTTPS, asking for the username and password generated on this VM's first boot

Once signed in, the dashboard summarises your nodes, the services you have published and recent activity.

The Wiredoor admin dashboard after signing in with the per-VM credentials, showing the node and service summary

Step 8 - Install the Wiredoor CLI on your private machine

Everything so far ran on the Wiredoor server. The remaining registration steps run on the private machine whose service you want to publish - your home server, a VM in a private subnet, an on-site gateway. It needs no public IP and no inbound firewall rule; it only needs to be able to reach this server outbound on UDP 51820.

curl -s https://www.wiredoor.net/install-wiredoor-cli.sh | sh

Packages are also published on the wiredoor-cli releases page.

Step 9 - Register the node and publish a service

Still on the private machine, log in to your Wiredoor server and register this machine as a node:

wiredoor login --url=https://<vm-public-ip>

The CLI prompts for the admin credentials from Step 5, asks for a name for this node, registers it, and brings up the WireGuard tunnel automatically. Then publish a local service - here an application listening on port 3000:

wiredoor http myapp --domain app.<your-domain> --port 3000

The domain must have a DNS A record pointing at your Wiredoor server's public IP. Wiredoor requests a Let's Encrypt certificate for it, adds an nginx location, and forwards matching requests down the tunnel to 127.0.0.1:3000 on the private machine. To expose a raw TCP service instead, use wiredoor tcp <name> --port <port>, which allocates a port from the 32760-32767 range.

Make the tunnel survive a reboot of the private machine:

systemctl enable --now wiredoor

Step 10 - Confirm the node and its published services

Back in the dashboard, Nodes lists every registered machine and its tunnel state. The image arrives with one built-in local node, which is how the dashboard itself is published through Wiredoor's own ingress - so a working dashboard is already proof the reverse-proxy path is healthy.

The Wiredoor Nodes view listing the registered nodes with their tunnel addresses and connection state

Opening a node shows the services published through it, with their domain or path, backend target and status. This is the view to check when a published service returns an error - it shows whether Wiredoor believes the service is up and where it is forwarding.

A Wiredoor node detail view showing the HTTP services published through that node, including the backend target and status of each

Step 11 - Set your contact email before enabling public certificates

Wiredoor reuses the administrator email as the Let's Encrypt registration contact. The image sets a placeholder sign-in name, which is not a deliverable address, so set your own address before you request a certificate for a public domain - otherwise expiry warnings go nowhere.

sudo sed -i "s/^ADMIN_EMAIL=.*/ADMIN_EMAIL=<your-email>/" /opt/wiredoor/.env
sudo systemctl restart wiredoor.service

Changing ADMIN_EMAIL changes the sign-in username too, so use the new address the next time you sign in. The password is unchanged.

Maintenance

Restart or stop the stack. The service is a thin systemd wrapper around Docker Compose:

sudo systemctl restart wiredoor.service
sudo systemctl status wiredoor.service --no-pager | head -12

Where things live.

sudo ls -l /opt/wiredoor/

docker-compose.yml is the stack definition, .env holds this VM's configuration and admin secret (0600 root:root), wiredoor-data/ is the SQLite database, WireGuard configuration and generated keys, and wiredoor-certbot/ holds Let's Encrypt certificates.

Back up. Stop the stack, archive the data and certificate directories, then start it again:

sudo systemctl stop wiredoor.service
sudo tar czf /root/wiredoor-backup-$(date +%F).tar.gz -C /opt/wiredoor wiredoor-data wiredoor-certbot .env
sudo systemctl start wiredoor.service

Treat that archive as a secret: it contains the WireGuard server private key, the JWT signing secret and your admin password.

Upgrade. Wiredoor is pinned by tag in /opt/wiredoor/docker-compose.yml. To move to a newer release, take a backup first, edit the image: line to the new tag, then pull and recreate:

sudo sed -i 's|wiredoor:v1.7.3|wiredoor:v<new-version>|' /opt/wiredoor/docker-compose.yml
sudo docker compose -f /opt/wiredoor/docker-compose.yml pull
sudo systemctl restart wiredoor.service

Read the upstream release notes before upgrading - Wiredoor 1.5.0 changed the container to run as a non-root user, which needed a one-time ownership fix.

Change the tunnel endpoint. If the auto-detected address is wrong (a NAT gateway, or you would rather nodes dial a DNS name), edit VPN_HOST in /opt/wiredoor/.env and restart. Existing node configurations are regenerated with the new endpoint.

Restrict access to a published service. Wiredoor can allow or block by public CIDR when you publish a service, and can put an OAuth2 provider in front of it. Note that IP restrictions must use public ranges - the service is reachable from the internet, so filtering on private ranges has no effect.

Troubleshooting

The dashboard does not load. Check that first boot finished and the container is up:

systemctl is-active wiredoor-firstboot.service wiredoor.service
sudo journalctl -u wiredoor-firstboot.service --no-pager | tail -20

A node cannot connect. This is almost always the UDP rule. Confirm the host is listening, then confirm your NSG allows 51820/udp from the node's network:

ss -uln | grep 51820

Sign-in returns "Too many attempts". Sign-in allows 5 attempts per 10 minutes per source IP. Wait for the window to clear.

A published service returns 502. Wiredoor reached the tunnel but not the backend. Check that the service is listening on the port you published, on the private machine, and that the node's tunnel is up in the Nodes view.

Inspect the container's own logs. All the bundled processes log through the container:

sudo docker logs wiredoor 2>&1 | tail -30

On a completely fresh data volume you may see one UNIQUE constraint failed: wg_interfaces.config line followed by Failed to bootstrap application in the first few seconds. That is an upstream start-up race between two of the bundled processes; supervisord restarts the loser immediately and the stack comes up clean. It is harmless, and only ever appears on the very first start.

Support

This image is published and maintained by cloudimg with 24/7 support. Wiredoor itself is open source under the Apache-2.0 licence; see the Wiredoor documentation and GitHub repository for upstream detail.