Networking AWS

Wiredoor Ingress 1.8.0 on AWS User Guide

| Product: Wiredoor Ingress 1.8.0 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying Wiredoor Ingress 1.8.0 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other VPN, proxy and remote access servers in the listing are described on the listing's guide hub.

Wiredoor is a self-hosted "ingress as a service" platform. Machines that cannot accept inbound connections (a home server, an office NAS, a Kubernetes node behind NAT) run a small Wiredoor client that dials out to your EC2 instance over WireGuard. Wiredoor's built-in nginx then publishes their web (and, if you enable them, TCP) services on the instance's public address, with HTTPS from Let's Encrypt once you attach a domain. You manage nodes, services and domains from Wiredoor's web dashboard.

How this image differs from the upstream container:

  • Rebuilt with patched dependencies, with no source change. The upstream 1.8.0 container image ships body-parser 1.20.8, which lets a single unauthenticated request crash the Wiredoor API (CVE-2026-111181), plus PCRE2, expat and certifi advisories and an oauth2-proxy built on an older Go. This image is built at cloudimg from the unmodified v1.8.0 source and Dockerfile with only dependency raises inside upstream's own version ranges, the same base images upstream's release used (with today's Alpine packages), oauth2-proxy 7.15.5 compiled on Go 1.27, and the unused npm and pip package managers removed. The recipe and every version are in /usr/share/doc/cloudimg-wiredoor/.
  • Nothing pre-made. The admin password and the TLS certificate are generated on your instance at first boot, and Wiredoor generates its WireGuard server key and its session-signing secret on its first start against an empty data directory.
  • The dashboard answers only on your instance's own names. A request for any other host name on port 443 is refused, and a browser request from another site that tries to change something is refused.
  • WireGuard peers are fenced. A connected node can reach Wiredoor's published services and other nodes (and the internet, if you enable that for the node), but not the instance metadata service, your VPC, the instance itself, or Wiredoor's internal ports.
  • TCP services are off until you turn them on (Step 9), so only SSH, HTTP, HTTPS and WireGuard are open.
  • The image ships its container. The rebuilt image is stored on the AMI as a checksummed archive and loaded at first boot, so the instance needs no access to a container registry to start.

What is on the image:

  • Wiredoor 1.8.0 (wiredoor.service), running in Docker CE with exactly three published ports: TCP 80 (HTTP, redirected to HTTPS, and Let's Encrypt validation), TCP 443 (the dashboard and your published services) and UDP 51820 (WireGuard, which your nodes dial)
  • Docker CE and containerd from Docker's own signed repository, kept up to date by Ubuntu's unattended upgrades
  • cloudimg-wiredoor-guard.service: the firewall rule that keeps containers away from the instance metadata service
  • Helpers: wiredoor-credential-check (an end-to-end sign-in test), wiredoor-imds-guard (show the metadata guard) and wiredoor-render-nginx (refresh the dashboard's host names)

Default user: ubuntu · Architecture: x86_64 · Root volume: 12 GiB gp3

Security posture

  • Per-instance secrets. The admin password (28 random characters) is generated at first boot and written to /opt/wiredoor/.env and /root/wiredoor-credentials.txt, both readable by root alone; the container receives it from that file, never on a command line. Wiredoor refuses to start with its published sample password, and the sample sign-in admin@example.com does not exist on this image. The WireGuard server key, the session-signing (JWT) secret and the TLS key are unique to each instance.
  • End-entity certificate. Until you attach a domain with a Let's Encrypt certificate, HTTPS uses a self-signed certificate generated on your instance whose names are the instance's own addresses. It cannot sign other certificates. Do not install it in any trust store: compare its SHA-256 fingerprint instead (Step 5).
  • Fail-closed start. Wiredoor cannot start before first boot has generated its secrets and loaded the metadata guard: its unit is gated on a ready marker and a preflight check. If first boot fails, Wiredoor stays stopped.
  • Host pinning and cross-site protection. The dashboard is served only for this instance's addresses and names (and any you add, Step 8); any other Host or TLS server name on port 443 gets no answer. Browser requests that carry another site's Origin cannot change anything or open a WebSocket. The API also requires its bearer token on every call.
  • Real client addresses. nginx tells Wiredoor and your published services the address it actually received the connection from, never an X-Forwarded-For value a client made up, so the sign-in rate limit (5 attempts per 10 minutes per address) cannot be bypassed.
  • Metadata service protection. Wiredoor's nginx connects to your services from inside the container, so a service whose target is the metadata address would be a way to reach your instance's IAM role credentials. A Docker firewall rule (DOCKER-USER) drops everything containers send to 169.254.0.0/16, 168.63.129.16 and the IPv6 metadata addresses, and Docker, containerd and the Wiredoor unit are denied the metadata service by systemd IPAddressDeny.
  • Container hardening. The container runs on a private Docker bridge network with only its three ports published, with every Linux capability removed except the five it needs (network administration for WireGuard, binding ports 80 and 443 as a non-root user, and the user switch and file access its WireGuard helper uses), not privileged, without the Docker socket, and with capped logs. Docker listens on its local socket only.
  • Hardened SSH. Key-only login, no root login, no X11 forwarding.

Prerequisites

  • An AWS account subscribed to the listing, and an EC2 key pair in your Region.
  • A VPC subnet with internet access and a public IP (an Elastic IP is recommended, Step 10): your nodes dial UDP 51820 on it and your visitors use TCP 80 and 443.
  • On each machine you want to publish from: the Wiredoor client (wiredoor-cli, see https://www.wiredoor.net/docs) or any WireGuard client.
  • Optional: a DNS name you control, for Let's Encrypt certificates on your published services.

Connecting to your instance

Item Value
SSH user ubuntu
Dashboard https://<instance-address>/
Sign-in username admin@wiredoor.local
Sign-in password in /root/wiredoor-credentials.txt (root only)
WireGuard endpoint UDP 51820 on the instance's public address

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. Open the cloudimg Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  2. Once the subscription is active, choose Launch your software.
  3. Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose Wiredoor Ingress 1.8.0 on Ubuntu 24.04, then choose your Region.
  4. Pick an instance type (m5.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  5. Security group: the listing's default group allows SSH (TCP 22), HTTP (TCP 80), HTTPS (TCP 443) and WireGuard (UDP 51820) from anywhere. TCP 80 and 443 must stay open to your visitors and UDP 51820 to your nodes. Restrict TCP 22 to your own IP address.
  6. Choose a subnet with internet access and a public IP, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.

Option B: AWS CLI

Run this on your own workstation. It creates a security group, then launches the instance. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and address with your own:

SG=$(aws ec2 create-security-group --group-name wiredoor \
  --description "Wiredoor ingress" --vpc-id vpc-0123456789abcdef0 \
  --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol udp --port 51820 --cidr 0.0.0.0/0
aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type m5.large \
  --key-name my-key-pair \
  --security-group-ids "$SG" \
  --subnet-id subnet-0123456789abcdef0 \
  --associate-public-ip-address \
  --metadata-options HttpTokens=required,HttpPutResponseHopLimit=1 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=wiredoor}]'

HttpPutResponseHopLimit=1 is optional but recommended: it stops anything running inside a container on the instance from obtaining an IMDSv2 token at all, as an extra wall behind the firewall rule described above. The image itself already makes IMDSv2 required.

Step 2: Connect and read your credentials

Connect as ubuntu with the key pair you chose:

ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>

First boot finishes within a couple of minutes of launch. It generates the admin password and the TLS certificate, loads the Wiredoor container, applies the metadata guard and starts Wiredoor, which then creates its WireGuard server key. Your details are in /root/wiredoor-credentials.txt, readable only by root:

sudo cat /root/wiredoor-credentials.txt
# Wiredoor 1.8.0 - generated on first boot (cloudimg). Unique to this instance; keep it safe.
WIREDOOR_URL=https://203.0.113.25/
WIREDOOR_USERNAME=admin@wiredoor.local
WIREDOOR_PASSWORD=<28 random letters and digits>
WIREGUARD_ENDPOINT=203.0.113.25:51820/udp
TLS_CERT_SHA256=36:CD:EA:...(32 hexadecimal pairs)...:59:64:F0
...

Step 3: Confirm Wiredoor is running

Three services make up the appliance: Docker, the metadata guard and Wiredoor itself:

systemctl is-active docker.service cloudimg-wiredoor-guard.service wiredoor.service
active
active
active

Exactly these ports are open on the instance: SSH, and the three ports Docker publishes for the container (the local DNS resolver also listens, on loopback only):

sudo ss -tulnp | grep -E ':(22|80|443|51820) '
udp   UNCONN 0      0                 0.0.0.0:51820      0.0.0.0:*    users:(("docker-proxy",...))
udp   UNCONN 0      0                    [::]:51820         [::]:*    users:(("docker-proxy",...))
tcp   LISTEN 0      4096              0.0.0.0:80         0.0.0.0:*    users:(("docker-proxy",...))
tcp   LISTEN 0      4096              0.0.0.0:22         0.0.0.0:*    users:(("sshd",...),("systemd",...))
tcp   LISTEN 0      4096              0.0.0.0:443        0.0.0.0:*    users:(("docker-proxy",...))
tcp   LISTEN 0      4096                 [::]:80            [::]:*    users:(("docker-proxy",...))
tcp   LISTEN 0      4096                 [::]:22            [::]:*    users:(("sshd",...),("systemd",...))
tcp   LISTEN 0      4096                 [::]:443           [::]:*    users:(("docker-proxy",...))

Wiredoor's API (port 3000), its metrics endpoint and its log forwarder listen only inside the container. The WireGuard interface is up inside the container:

sudo docker exec -u root wiredoor wg show wg0 | head -3
interface: wg0
  public key: kQ4m...(44 characters)...=
  private key: (hidden)

Step 4: Run the credential check

wiredoor-credential-check signs in through Wiredoor's own nginx the way the dashboard does: it checks that unauthenticated and forged requests are refused, that a wrong password is refused, and that your password works. It uses two of the five sign-in attempts allowed per 10 minutes:

sudo wiredoor-credential-check
WIREDOOR_CREDCHECK_OK credential round trip through Wiredoor's nginx: 401 anonymous, 401 forged token, 401 wrong password, this instance's password mints a JWT that authenticates /api/auth/me

Step 5: Sign in to the dashboard

Before you type the password, check that the certificate your browser receives is your instance's. Print its fingerprint on the instance:

sudo openssl x509 -in /etc/wiredoor/tls/cert.pem -noout -sha256 -fingerprint
sha256 Fingerprint=36:CD:EA:5A:...:B6:59:64:F0

Open https://<public-ip>/ in your browser. It warns that the certificate is self-signed: open the certificate details, compare the SHA-256 fingerprint with the one above (and with TLS_CERT_SHA256 in the credentials file), and continue only if they match. Do not add the certificate to a trust store.

The Wiredoor sign-in page served over HTTPS

Sign in as admin@wiredoor.local with the password from the credentials file. The home page opens:

The Wiredoor home page after signing in

Client / Nodes lists the machines connected to this server. A new server has only Wiredoor's built-in local node, Wiredoor_Local (the server itself):

The Client / Nodes page with Wiredoor's built-in local node

Domains lists the DNS names you publish services on (none yet):

The Domains page on a new server

Step 6: Connect a node

On the machine whose services you want to publish (not on the EC2 instance), install the Wiredoor client (wiredoor-cli, https://github.com/wiredoor/wiredoor-cli) and connect it to this server. In the dashboard, choose Create Node on the home page (or Add Client on the Client / Nodes page), give it a name, and copy the command the node's page shows: wiredoor connect --url=https://<public-ip> --token=... (the token is a credential for that node: treat it like a password). Wiredoor's documentation covers each platform: https://www.wiredoor.net/docs.

If you prefer a plain WireGuard client, use Download Config on the node's page instead. The file contains the node's private key: keep it on that machine only.

Nodes dial the address in WIREGUARD_ENDPOINT. The node appears as online in the dashboard once its tunnel is up.

Step 7: Publish a service

On a connected node's page, choose Add Service for an HTTP service, give it a name, the port the service listens on (on the node), and either a path on this server (for example /grafana) or a domain (Step 8). Wiredoor writes the nginx configuration and reloads it; the service is then reachable at https://<public-ip>/<path> or https://<domain>/. Each service can be limited to client IP ranges, given an expiry, or protected with an OAuth2 sign-in (Wiredoor runs oauth2-proxy for it once you configure a provider in /opt/wiredoor/.env).

Step 8: Domains and Let's Encrypt

To publish a service on a domain:

  1. Point the domain's DNS A record at the instance's public IP (an Elastic IP, so it does not change).
  2. Set ADMIN_EMAIL in /opt/wiredoor/.env to your own address (it is the Let's Encrypt registration contact), then restart Wiredoor:
sudo nano /opt/wiredoor/.env
sudo systemctl restart wiredoor
  1. In the dashboard, choose Domains, Add Domain, and choose Let's Encrypt for its certificate. Validation uses port 80, which this image answers for every domain.

The dashboard itself answers only on the instance's own addresses and names. To reach it on a domain too, add the name to /etc/wiredoor/admin-hosts (one per line) and restart Wiredoor:

echo "wiredoor.example.com" | sudo tee -a /etc/wiredoor/admin-hosts
sudo systemctl restart wiredoor

The names the dashboard currently answers on:

sudo grep -o 'server_name [^_][^;]*;' /etc/wiredoor/nginx/default.conf
server_name 127.0.0.1 localhost 203.0.113.25 172.31.93.109 ec2-203-0-113-25.compute-1.amazonaws.com ip-172-31-93-109.ec2.internal;

Any other name on port 443 gets no answer:

curl -ks -o /dev/null -w '%{http_code}\n' -H 'Host: not-this-server.example' https://127.0.0.1/ || true
000

Step 9: TCP services (off by default)

Wiredoor can also publish raw TCP services on a range of public ports. That is off on this image, so only the four ports above are open. To turn it on:

  1. Add a port range (and/or a list of single ports) to /opt/wiredoor/.env, for example TCP_SERVICES_PORT_RANGE=32760-32767.
  2. List the same ports in /etc/wiredoor/tcp-ports (one port or range per line, for example 32760-32767) so the container publishes them.
  3. Open them in the instance's security group, then sudo systemctl restart wiredoor.

Step 10: Elastic IP or a DNS name

The WireGuard endpoint in each node's configuration is the address first boot found (VPN_HOST in /opt/wiredoor/.env). Attach an Elastic IP before you connect nodes, then point Wiredoor at it and refresh the dashboard's host names:

sudo sed -i 's/^VPN_HOST=.*/VPN_HOST=<elastic-ip-or-dns-name>/' /opt/wiredoor/.env
sudo wiredoor-render-nginx
sudo systemctl restart wiredoor

Step 11: What a WireGuard node can reach

Upstream Wiredoor forwards anything a node sends through the server. On this image a node's traffic is fenced by Wiredoor's own WireGuard rules, set at first boot: it can reach other nodes and, if you tick Send all internet traffic through the VPN for the node, the internet; it cannot open connections to the server itself over the tunnel (Wiredoor's API, metrics and log ports), to the metadata service, or to private address ranges (your VPC, the Docker network, this instance). Wiredoor's nginx still reaches every node to publish its services. The rules, inside the container:

sudo docker exec -u root wiredoor iptables -S FORWARD
-P FORWARD ACCEPT
-A FORWARD -i wg0 -o wg0 -j ACCEPT
-A FORWARD -d 10.0.0.0/8 -i wg0 -j DROP
-A FORWARD -d 172.16.0.0/12 -i wg0 -j DROP
-A FORWARD -d 192.168.0.0/16 -i wg0 -j DROP
-A FORWARD -d 100.64.0.0/10 -i wg0 -j DROP
-A FORWARD -d 169.254.0.0/16 -i wg0 -j DROP
-A FORWARD -d 168.63.129.16/32 -i wg0 -j DROP
-A FORWARD -i wg0 -j ACCEPT
-A FORWARD -o wg0 -j ACCEPT

Connections a node opens to the server itself over the tunnel are limited the same way (replies to Wiredoor's own connections and ping only):

sudo docker exec -u root wiredoor iptables -S INPUT
-P INPUT ACCEPT
-A INPUT -p udp -m udp --dport 51820 -j ACCEPT
-A INPUT -i wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i wg0 -p icmp -j ACCEPT
-A INPUT -i wg0 -j DROP

These rules are Wiredoor's own WG_POST_UP_SCRIPT / WG_POST_DOWN_SCRIPT settings in /opt/wiredoor/.env; Wiredoor stores them when it first starts. If you do want a node to reach something in your VPC, publish that VPC service through Wiredoor's local node instead (Step 7), so it is subject to the same access rules as every other service.

Step 12: Metadata access is blocked

wiredoor-imds-guard shows the firewall rules that drop everything containers send to the metadata addresses, with packet counters:

sudo wiredoor-imds-guard show
Chain DOCKER-USER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DROP       0    --  *      *       0.0.0.0/0            168.63.129.16        /* cloudimg-wiredoor-imds */
    0     0 DROP       0    --  *      *       0.0.0.0/0            169.254.0.0/16       /* cloudimg-wiredoor-imds */
...

Wiredoor cannot run without the guard: wiredoor.service requires cloudimg-wiredoor-guard.service, and when Docker restarts, the guard is applied again before Wiredoor. A service whose target is the metadata address is refused when you create it (Wiredoor cannot connect to it). Docker, containerd and the Wiredoor unit are also denied the metadata service directly:

systemctl show -p IPAddressDeny docker.service containerd.service wiredoor.service
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32

IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32

IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32

Step 13: How the fail-closed start works

wiredoor.service starts only after first boot has written /var/lib/cloudimg/wiredoor.bootstrap-ready, and its preflight (/usr/local/sbin/wiredoor-preflight) refuses to start the container unless the environment file is root-only with a real password, the end-entity certificate exists, the metadata guard is loaded and the pinned image is the one loaded. The pinned image identity is in /etc/wiredoor/wiredoor.conf:

sudo docker image ls --format '{{.Repository}}:{{.Tag}} {{.ID}}'
cloudimg/wiredoor:1.8.0 d70dfa311b7a

Step 14: Updates and logs

Ubuntu's unattended upgrades install security updates daily, from the Ubuntu archive and from Docker's repository (the image adds Docker to the allowed origins in /etc/apt/apt.conf.d/51cloudimg-docker-unattended). When Docker is upgraded, Wiredoor restarts with it and the metadata guard is applied again first. The Wiredoor software itself never changes automatically; when cloudimg publishes a newer Wiredoor version in this listing, move to it by launching the new version and reconnecting your nodes.

Wiredoor's own log goes to the journal and to Docker's capped log:

sudo journalctl -u wiredoor.service --no-pager -n 5 | cut -c1-120

First-boot progress is in sudo journalctl -u wiredoor-firstboot.service.

Step 15: Security recommendations

  • Restrict TCP 22 in the security group to your own address. Keep TCP 80 and 443 open to your visitors and UDP 51820 to your nodes.
  • Keep the admin password private and use a long, random one if you change it (ADMIN_PASSWORD in /opt/wiredoor/.env, then sudo systemctl restart wiredoor).
  • Treat node connection commands, access tokens and downloaded WireGuard configurations as secrets; remove nodes you no longer use.
  • Use domains with Let's Encrypt certificates for anything your users open in a browser.
  • Launch with HttpPutResponseHopLimit=1 (Step 1, Option B) for an extra metadata wall.
  • OAuth2 sign-in and go-jose advisory GHSA-mgcc-cww5-fp2q (CVE-2026-116587). The oauth2-proxy 7.15.5 on this image still links go-jose v3, which has no fix for this denial-of-service advisory: a crafted RSA key in a JSON Web Key Set (JWKS) can stall oauth2-proxy. On this image oauth2-proxy does not run and no identity provider, issuer or JWKS URL is configured until you set one up. If you enable OAuth2 for a service, point OAUTH2_PROXY_PROVIDER and any OIDC issuer or JWKS URL only at a trusted HTTPS endpoint you control or your own identity provider (never an address someone else can change), because only that provider's key set can trigger the advisory. cloudimg will publish a new version of this listing once oauth2-proxy moves off go-jose v3.
  • Keep unattended upgrades on, so Docker and the operating system stay patched.

Support and licensing

Wiredoor is licensed under the Apache License 2.0 by INFLADOOR LLC; its TERMS.md asks that it be used lawfully. oauth2-proxy is MIT-licensed, certbot is Apache-2.0, nginx is BSD-2-Clause, Node.js is MIT and Fluent Bit is Apache-2.0. The rebuild recipe (both Dockerfiles, the lock-file changes, oauth2-proxy's go.mod and go.sum) and the provenance of every component are in /usr/share/doc/cloudimg-wiredoor/, and the source is available from the upstream projects (https://github.com/wiredoor/wiredoor) and from cloudimg on request. cloudimg support covers deploying and operating this image; contact support@cloudimg.co.uk.

Wiredoor is a trademark of INFLADOOR LLC. WireGuard is a registered trademark of Jason A. Donenfeld. cloudimg is not affiliated with or endorsed by INFLADOOR LLC or the WireGuard project.