Deployment guide

Wiredoor

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 in a home lab, a machine behind carrier-grade NAT, a Kubernetes cluster with no load balancer, or 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. The CLI 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.

This SecureAnyCloud 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. The image itself ships with no credentials and no key material of any kind.

What you get:

  • Wiredoor 1.7.3 (ghcr.io/wiredoor/wiredoor:v1.7.3, Apache-2.0) running as a container managed by systemd and Docker
  • Docker CE preinstalled from the official Docker apt repository
  • A WireGuard tunnel on UDP 51820, with 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 and the public WireGuard endpoint, generated on the first boot of every VM 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 and wiredoor.service as enabled, active systemd units
  • 24/7 SecureAnyCloud support

Security posture of this image

Because Wiredoor's 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. 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

You need an active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region.

Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting size. Wiredoor is the entry point for all inbound traffic, so the VM needs a public IP address (or a DNS name pointing at it) that your private nodes and your users can reach.

Configure the network security group (NSG) to allow these inbound rules: 22/tcp from your management network, and 80/tcp and 443/tcp for ingress and for Let's Encrypt validation. Also allow 51820/udp for the WireGuard tunnel. Without the UDP rule, the dashboard works but 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 SecureAnyCloud, and select Create.

On the Basics tab, 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).

Select Review + create, then Create. You add the WireGuard UDP 51820 rule after creation in Step 2.

Step 2 - Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:wiredoor-ubuntu-24-04:default:latest
az vm create \
  --resource-group <your-rg> \
  --name wiredoor \
  --image sac1647283583153:wiredoor-ubuntu-24-04:default:latest \
  --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 reaches the VM. Otherwise the dashboard loads, but no node can establish a tunnel.

Step 3 - Connect to your VM

Connect to the VM over SSH.

ssh azureuser@<vm-public-ip>

Step 4 - Confirm the services and the tunnel are up

Check the services and the container:

systemctl is-active docker.service wiredoor.service
sudo docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
sudo 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 and the container runs the digest-pinned image. Next, confirm the tunnel. 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

Run wg show as root inside the container. The container's own unprivileged user cannot read the interface.

Step 5 - Retrieve your admin password and tunnel endpoint

Your VM generates a unique admin password on first boot and writes it to a root-only file. Show the password line:

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

The password is masked in this guide. 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=<set-at-first-boot> 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 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.

Step 6 - Verify the credentials and the ingress

The image ships a checker. It proves that 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 also 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 instead of retrying.

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 warns you once. This is expected for IP-based access. Step 11 replaces it with a real Let's Encrypt certificate.

After you sign in, the dashboard summarises your nodes, the services you have published and recent activity.

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, such as your home server, a VM in a private subnet or an on-site gateway.

That machine needs no public IP and no inbound firewall rule. It only needs to reach this server outbound on UDP 51820. Install the CLI:

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

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

Step 9 - Register the node and publish a service

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

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

The CLI prompts for the admin credentials from Step 5 and asks for a name for this node. It registers the node and brings up the WireGuard tunnel automatically.

Next, publish a local service. This example publishes 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>. It 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

In the dashboard, open Nodes to see 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. A working dashboard is therefore already proof that the reverse-proxy path is healthy.

Open a node to see the services published through it, with their domain or path, backend target and status. Check this view when a published service returns an error. It shows whether Wiredoor believes the service is up and where it is forwarding.

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 sample sign-in name that is not a deliverable address. 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 also changes the sign-in username, 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. Manage it with systemd:

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

Where things live. List the stack directory:

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. Then edit the image: line to the new tag, and 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 (for example, behind a NAT gateway, or if you prefer that 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. When you publish a service, Wiredoor can allow or block by public CIDR, and can put an OAuth2 provider in front of it. 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 that 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. The cause is almost always the UDP rule. Confirm that the host is listening, then confirm that 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. On the private machine, check that the service is listening on the port you published. In the Nodes view, check that the node's tunnel is up.

Inspect the container's 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. This 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 appears only on the very first start.

Support

SecureAnyCloud publishes and maintains this image and provides 24/7 support. Wiredoor itself is open source under the Apache-2.0 licence. See the Wiredoor documentation and GitHub repository for upstream detail.

Deploy on Azure

Launch Wiredoor Ingress as a Service 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

Deploy on Azure