go-httpbin on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers deploying and using go-httpbin 2.26.0 on Ubuntu 24.04 LTS from cloudimg on Microsoft Azure.
go-httpbin is a Go implementation of httpbin: a server you point HTTP clients at when you need a predictable, controllable other end. It echoes back what you send (/get, /post, /anything, /headers), returns any status code you ask for (/status/418), and has endpoints for redirects, delays, slow drips, streams and server sent events, gzip, deflate and brotli responses, cookies, caching headers, images, auth challenges and a websocket echo. Use it to test HTTP clients and SDKs, retry and timeout handling, proxies, API gateways, load balancer health probes and CI pipelines, without depending on a shared public service.
The cloudimg image locks go-httpbin down:
- HTTPS from the first boot. nginx serves go-httpbin on port 443 with a certificate generated on your VM's first boot. Port 80 redirects to HTTPS.
- A password gate in front of everything. go-httpbin has no login of its own and reflects whatever it is sent, so the whole service sits behind an HTTP Basic Auth prompt whose password is generated on your VM's first boot. A fresh VM is never an open reflector on the internet.
- No default login. The image contains no password. First boot writes the gate's user name and a random 24 character password to
/var/lib/cloudimg/go-httpbin-credentials.txt(root only); nginx stores only a SHA-512 crypt hash. Common guesses are refused. - The gate password never reaches go-httpbin. nginx removes the
Authorizationheader before passing a request on, so/headers,/anythingand/dump/requestcan never echo your password back, and a web page on another site cannot read it through go-httpbin's CORS answers either. go-httpbin never logs it. - Real client address and host name. go-httpbin sees the address the connection really came from and the VM's configured name, whatever
X-Forwarded-For,HostorForwardedheaders a request carries. - Redirects stay on the VM.
/redirect-toonly sends a client to a path on this VM or to this VM's own name, so it cannot be used as an open redirector. Step 7 shows how to allow other names. - Bounded requests. Request and response bodies are capped at 1 MiB and every timed endpoint at 10 seconds; nginx also limits each client address to 64 connections and 50 requests a second.
- Other web sites cannot act with your login. A browser re-sends the gate password to any site that asks, so nginx refuses every state changing request (POST, PUT, DELETE and the like) and every WebSocket upgrade whose browser
Originis not this VM. - No route to the Azure metadata service. nginx, go-httpbin and the first boot service are blocked at the kernel level from the Azure Instance Metadata Service and the WireServer. The Azure VM agent keeps its access.
- A current, checked build. go-httpbin 2.26.0 is the release that fixes a websocket denial of service in 2.13 to 2.25. It is built on the image from the official source with Go 1.27.3, which also fixes a
crypto/x509advisory in the Go release the official binary was built with. The build is reproducible and its checksum is pinned. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: go-httpbin 2.26.0 (/usr/local/bin/go-httpbin, listening on 127.0.0.1:8080 only), nginx, Ubuntu 24.04 LTS, cloud-init and the Azure Linux agent.
Default user: the admin user you choose (azureuser in the examples) · Architecture: x64, Gen2 · Recommended size: Standard_B2s
Prerequisites
- An Azure subscription, an SSH public key, and a virtual network and subnet in your region.
- A network security group that allows TCP 22 (SSH), TCP 443 (go-httpbin) and TCP 80 (redirects to HTTPS). Open them to the networks your clients and test runners use; a test endpoint rarely needs to be reachable from the whole internet. go-httpbin needs no other port.
Step 1: Deploy from the Azure portal
Search the Azure Marketplace for go-httpbin, choose the cloudimg offer and select Create. Pick your subscription, resource group and region, size Standard_B2s, an SSH public key for the admin user, and inbound ports 22, 80 and 443. Review and create.
Step 2: Deploy from the Azure CLI
RG="go-httpbin-rg"; LOCATION="eastus"; VM_NAME="go-httpbin1"
IMAGE_URN="cloudimg1647283583153:go-httpbin-ubuntu-24-04:default:latest"
az group create --name "$RG" --location "$LOCATION"
az vm image terms accept --urn "$IMAGE_URN"
az vm create --resource-group "$RG" --name "$VM_NAME" --image "$IMAGE_URN" \
--size Standard_B2s --admin-username azureuser --ssh-key-values ~/.ssh/id_rsa.pub \
--public-ip-sku Standard
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 443 --priority 1001
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 80 --priority 1002
az vm create opens port 22 for you. Narrow the rules to your own networks with az network nsg rule update --source-address-prefixes <your-cidr> if you can.
Step 3: Connect and check the services
ssh azureuser@<public-ip>
First boot reads the VM's public IP from the Azure Instance Metadata Service, sets the served name and certificate and creates the gate password; then go-httpbin and nginx start:
systemctl is-active go-httpbin nginx go-httpbin-firstboot go-httpbin-address
sudo test -f /var/lib/cloudimg/go-httpbin-firstboot.done && echo FIRSTBOOT_DONE
All four units are active and the first boot marker is present:
active
active
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on the network. go-httpbin listens on the loopback address:
sudo ss -lntp | awk 'NR>1 {print $4, $6}' | grep -v '127.0.0.5' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:8080 users:(("go-httpbin"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))

The built in self-test checks the whole path through the gate:
sudo go-httpbin-selftest
SELFTEST-OK https 301 + healthz + gate 401 + 6 wrong gate credentials refused + /get echo + no Authorization reaches go-httpbin + 418 + /json + local redirect 302 + off-site redirect refused + 403 cross-origin POST + 415 compressed body + 444 unknown host + preflight
Step 4: Get your URL and password
sudo cat /var/lib/cloudimg/go-httpbin-credentials.txt
The file is readable by root only and holds the URL and the gate's user name and password for this VM:
# go-httpbin 2.26.0 - generated on first boot by go-httpbin-firstboot.service
# These credentials are unique to this instance; none was baked into the image. Keep this file secret.
GO_HTTPBIN_URL=https://<public-ip>/
GO_HTTPBIN_USERNAME=httpbin
GO_HTTPBIN_PASSWORD=<GO_HTTPBIN_PASSWORD>
#
# Point any HTTP client at GO_HTTPBIN_URL with HTTP Basic Auth GO_HTTPBIN_USERNAME / GO_HTTPBIN_PASSWORD (self-signed
# certificate minted on this instance's first boot; port 80 redirects to https), e.g. GET /get, /status/418, /json.
# The gate's Authorization header is removed before go-httpbin, so its echo endpoints never show this password, and a
# web page on another site cannot read it back through go-httpbin's CORS answers.
# /redirect-to may only target this instance's name (add names to /etc/cloudimg/go-httpbin-redirect-domains, then
# sudo go-httpbin-set-hostname --refresh). Self-test: sudo go-httpbin-selftest
# Change the served name: sudo go-httpbin-set-hostname <your-domain>
# User guide: https://www.cloudimg.co.uk/guides/go-httpbin-on-ubuntu-24-04-azure/
The URL uses the VM's public IP. The first boot service cannot reach the metadata service itself, so a small service with no access to go-httpbin's files, go-httpbin-address, reads the address from the Azure Instance Metadata Service on every boot (including a Standard SKU public IP, which only the metadata service's load balancer view shows) and records it here:
cat /run/cloudimg-go-httpbin/address.env
GO_HTTPBIN_PUBLIC_IP=<public-ip>
GO_HTTPBIN_PRIVATE_IPS="<private-ip>"
Requests to the VM's private address are redirected to the public one. To serve a DNS name instead, see Step 9.
From your own machine or a CI job, pass the user name and password as HTTP Basic Auth, for example with a curl config file that holds user = "httpbin:<password>" and curl -k -K <file> https://<public-ip>/get. The certificate is self signed. Do not add it to a trust store: compare its fingerprint with the one this VM reports (sudo openssl x509 -in /etc/ssl/go-httpbin/go-httpbin.crt -noout -fingerprint -sha256) and pin that in your client, or replace it with a certificate from a certificate authority (Step 9). Opening the URL in a browser shows go-httpbin's endpoint index after the password prompt.

Step 5: Send your first requests
The commands below run on the VM itself. They read the password from the credentials file with sudo and hand it to curl as a config file (-K), so it never appears on a command line. /get echoes your request back as JSON:
H=$(sudo sed -n 's#^GO_HTTPBIN_URL=https://\([^/]*\)/$#\1#p' /var/lib/cloudimg/go-httpbin-credentials.txt)
gate() { sudo awk -F= '/^GO_HTTPBIN_USERNAME=/{u=$2} /^GO_HTTPBIN_PASSWORD=/{p=$2} END{printf "user = \"%s:%s\"\n", u, p}' /var/lib/cloudimg/go-httpbin-credentials.txt; }
curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" "https://$H/get?team=qa"
{
"args": {
"team": [
"qa"
]
},
"headers": {
"Accept": [
"*/*"
],
"Host": [
"<public-ip>"
],
"User-Agent": [
"curl/8.5.0"
],
"X-Forwarded-For": [
"127.0.0.1"
],
"X-Forwarded-Proto": [
"https"
],
"X-Real-Ip": [
"127.0.0.1"
]
},
"method": "GET",
"origin": "127.0.0.1",
"url": "https://<public-ip>/get?team=qa"
}
origin and X-Forwarded-For are the address the request really came from (here the VM itself); from your machine they show your address. There is no Authorization header: nginx checked the password and removed it.

Ask for a status code, post a JSON body, and list the headers go-httpbin receives:
H=$(sudo sed -n 's#^GO_HTTPBIN_URL=https://\([^/]*\)/$#\1#p' /var/lib/cloudimg/go-httpbin-credentials.txt)
gate() { sudo awk -F= '/^GO_HTTPBIN_USERNAME=/{u=$2} /^GO_HTTPBIN_PASSWORD=/{p=$2} END{printf "user = \"%s:%s\"\n", u, p}' /var/lib/cloudimg/go-httpbin-credentials.txt; }
curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -o /dev/null -w '%{http_code}\n' "https://$H/status/418"
curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' --data '{"order": 42}' "https://$H/post" | python3 -c 'import json,sys; o=json.load(sys.stdin); print(o["json"], o["headers"]["Content-Type"])'
curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" "https://$H/headers" | python3 -c 'import json,sys; print(sorted(json.load(sys.stdin)["headers"]))'
418
{'order': 42} ['application/json']
['Accept', 'Host', 'User-Agent', 'X-Forwarded-For', 'X-Forwarded-Proto', 'X-Real-Ip']
The endpoint index at the root URL lists everything go-httpbin answers; the go-httpbin README describes each one.
Step 6: Delays, streams and limits
Timed endpoints wait as long as you ask, up to 10 seconds, and sized endpoints return up to 1 MiB. Beyond that go-httpbin answers 400 and says why:
H=$(sudo sed -n 's#^GO_HTTPBIN_URL=https://\([^/]*\)/$#\1#p' /var/lib/cloudimg/go-httpbin-credentials.txt)
gate() { sudo awk -F= '/^GO_HTTPBIN_USERNAME=/{u=$2} /^GO_HTTPBIN_PASSWORD=/{p=$2} END{printf "user = \"%s:%s\"\n", u, p}' /var/lib/cloudimg/go-httpbin-credentials.txt; }
curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -o /dev/null -w '%{http_code}\n' "https://$H/delay/2"
curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" "https://$H/delay/11"; echo
curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" "https://$H/bytes/2000000"; echo
curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" "https://$H/drip?duration=2&numbytes=5&code=200"; echo
200
{
"status_code": 400,
"error": "Bad Request",
"detail": "invalid duration: duration 11s longer than 10s"
}
{
"status_code": 400,
"error": "Bad Request",
"detail": "invalid byte count: 2000000 not in range [1, 1048576]"
}
*****
nginx passes responses through as go-httpbin produces them, so /drip, /stream/<n> and /sse arrive gradually, as a real slow server's would. A request body larger than 1 MiB gets a 413 from nginx, and a client that opens more than 64 connections or sends more than 50 requests a second (after a burst of 200) gets 429.
The limits are go-httpbin's -max-body-size and -max-duration options in /etc/systemd/system/go-httpbin.service. To raise them, run sudo systemctl edit --full go-httpbin, change the two values, and change client_max_body_size in /etc/nginx/sites-available/cloudimg-go-httpbin to match; then restart go-httpbin and reload nginx.
Step 7: Redirects
/redirect/<n>, /relative-redirect/<n> and /absolute-redirect/<n> redirect within the VM. /redirect-to?url= redirects wherever the url says, which on an open server makes it an open redirector, so here it only accepts a path on this VM or this VM's own name:
H=$(sudo sed -n 's#^GO_HTTPBIN_URL=https://\([^/]*\)/$#\1#p' /var/lib/cloudimg/go-httpbin-credentials.txt)
gate() { sudo awk -F= '/^GO_HTTPBIN_USERNAME=/{u=$2} /^GO_HTTPBIN_PASSWORD=/{p=$2} END{printf "user = \"%s:%s\"\n", u, p}' /var/lib/cloudimg/go-httpbin-credentials.txt; }
curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -o /dev/null -w '%{http_code} %header{location}\n' "https://$H/redirect-to?url=/get"
curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -o /dev/null -w '%{http_code} %header{location}\n' "https://$H/redirect/2"
curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -o /dev/null -w '%{http_code}\n' "https://$H/redirect-to?url=https://example.com/"
302 /get
302 /relative-redirect/1
400
To let /redirect-to send clients to other names (for example the test system your client should end up at), list them one per line in /etc/cloudimg/go-httpbin-redirect-domains and re-apply the configuration. Both nginx and go-httpbin then accept them:
echo '<your-domain>' | sudo tee -a /etc/cloudimg/go-httpbin-redirect-domains
sudo go-httpbin-set-hostname --refresh
Only names are accepted, one per line, without a scheme, path or wildcard.
Step 8: Auth challenge endpoints
/basic-auth/<user>/<password>, /hidden-basic-auth/..., /digest-auth/... and /bearer test how a client answers an authentication challenge, which uses the same Authorization header as the gate. Through the gate they always answer 401, because nginx removes that header. Run them on the VM (or through an SSH tunnel to 127.0.0.1:8080), where go-httpbin answers directly:
curl -s -u alice:s3cret http://127.0.0.1:8080/basic-auth/alice/s3cret
curl -s -o /dev/null -w '%{http_code}\n' -u alice:wrong http://127.0.0.1:8080/basic-auth/alice/s3cret
{
"authenticated": true,
"user": "alice",
"authorized": true
}
401
From your machine, ssh -L 8080:127.0.0.1:8080 azureuser@<public-ip> makes http://127.0.0.1:8080/ on your machine reach go-httpbin directly while the tunnel is open.
Step 9: Use your own domain
Point a DNS record for your domain at the VM's public IP (or give the public IP a DNS label in Azure), then give go-httpbin the name. The helper mints a new certificate for it, pins nginx, the cross-site guard and the redirect allow list to it, and redirects the VM's IP addresses there:
sudo go-httpbin-set-hostname <your-domain>
For a certificate your clients trust, replace /etc/ssl/go-httpbin/go-httpbin.crt and /etc/ssl/go-httpbin/go-httpbin.key with your own (for example from Let's Encrypt or your certificate authority) and reload nginx.
Step 10: Security notes
nginx, go-httpbin and the first boot service are blocked from the Azure Instance Metadata Service and the WireServer (169.254.169.254, 168.63.129.16 and their IPv6 forms). Only go-httpbin-address, which runs no go-httpbin code, and the Azure agent keep that access:
for u in nginx go-httpbin go-httpbin-firstboot; do printf '%s ' "$u"; systemctl show -p IPAddressDeny --value "$u" | tr ' ' '\n' | sort | tr '\n' ' '; echo; done
nginx 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128
go-httpbin 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128
go-httpbin-firstboot 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128
The gate answers before go-httpbin does, nginx refuses a state changing request that another web site's page sends, a request body with a Content-Encoding is refused, and a request for a host name the VM does not serve gets no answer at all:
H=$(sudo sed -n 's#^GO_HTTPBIN_URL=https://\([^/]*\)/$#\1#p' /var/lib/cloudimg/go-httpbin-credentials.txt)
gate() { sudo awk -F= '/^GO_HTTPBIN_USERNAME=/{u=$2} /^GO_HTTPBIN_PASSWORD=/{p=$2} END{printf "user = \"%s:%s\"\n", u, p}' /var/lib/cloudimg/go-httpbin-credentials.txt; }
curl -sk -o /dev/null -w '%{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/get"
curl -sk -K <(gate) -o /dev/null -w '%{http_code}\n' --resolve "$H:443:127.0.0.1" -X POST -H 'Origin: https://evil.example' --data 'x=1' "https://$H/post"
printf 'x' | curl -sk -K <(gate) -o /dev/null -w '%{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Content-Encoding: gzip' --data-binary @- "https://$H/post"
curl -sk -o /dev/null -w '%{http_code}\n' --resolve evil.example:443:127.0.0.1 https://evil.example/ || true
401
403
415
000
go-httpbin answers CORS requests from any origin, because that is part of what it tests. A page on another site that your browser opens while it remembers the gate password can therefore read go-httpbin's GET responses, but those responses only reflect that page's own request: the gate's Authorization header is gone before go-httpbin sees it. A browser shaped cross site request shows it:
H=$(sudo sed -n 's#^GO_HTTPBIN_URL=https://\([^/]*\)/$#\1#p' /var/lib/cloudimg/go-httpbin-credentials.txt)
gate() { sudo awk -F= '/^GO_HTTPBIN_USERNAME=/{u=$2} /^GO_HTTPBIN_PASSWORD=/{p=$2} END{printf "user = \"%s:%s\"\n", u, p}' /var/lib/cloudimg/go-httpbin-credentials.txt; }
curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -D - -o /dev/null -H 'Origin: https://evil.example' "https://$H/headers" | grep -i '^access-control-allow' | tr -d '\r'
curl -sk -K <(gate) --resolve "$H:443:127.0.0.1" -H 'Origin: https://evil.example' "https://$H/headers" | python3 -c 'import json,sys; print(sorted(json.load(sys.stdin)["headers"]))'
access-control-allow-credentials: true
access-control-allow-origin: https://evil.example
['Accept', 'Host', 'Origin', 'User-Agent', 'X-Forwarded-For', 'X-Forwarded-Proto', 'X-Real-Ip']

How go-httpbin itself is started:
systemctl show -p ExecStart --value go-httpbin | grep -o 'argv\[\]=[^;]*' | sed 's/argv\[\]=//'
/usr/local/bin/go-httpbin -host 127.0.0.1 -port 8080 -max-body-size 1048576 -max-duration 10s -log-level OFF -allowed-redirect-domains ${GO_HTTPBIN_REDIRECT_DOMAINS}
Other notes:
- Cross site requests. Requests without a browser
Originheader (curl, SDKs, test runners) are unaffected by the cross site guard. A page on another site cannot send POST, PUT, PATCH, DELETE or preflight (OPTIONS) requests, or open the websocket, with your login. - Compressed request bodies. nginx answers 415 to any request that carries a
Content-Encoding, before go-httpbin sees it. Send bodies uncompressed; go-httpbin's compressed responses (/gzip,/deflate,/brotli) are unaffected. - WebSocket echo.
/websocket/echoworks through nginx over HTTP/1.1 (wss://<your host>/websocket/echowith the Basic Auth header), with messages up to 1 MiB and a 10 second session. - Logs. go-httpbin's own request log is off, so the URLs and headers your tests send are not kept in the system journal. nginx records each request (client address, method, path, status, user agent) in
/var/log/nginx/access.log. To turn go-httpbin's log on, change-log-level OFFto-log-level INFOwithsudo systemctl edit --full go-httpbin. - What go-httpbin exposes.
/envis empty,/hostnamereturns a placeholder and/versiononly the service name, so nothing about the VM is published. go-httpbin makes no outbound requests of its own. - SSH. Root login, empty passwords and X11 forwarding are off. Password sign in follows the choice you made when you created the VM.
- The build.
/usr/share/doc/cloudimg-go-httpbin/BUILD-INFO.txtrecords the source archive, the Go toolchain and the checksums of the shipped files.
Step 11: Keep it up to date
Ubuntu security updates install automatically. To apply all updates now:
sudo apt-get update && sudo apt-get upgrade -y
New go-httpbin releases arrive as new versions of this offer, built and checked the same way. go-httpbin keeps no data, so moving to one is a matter of deploying the new version and pointing your clients at it. The release notes list what changed.
Support and licensing
go-httpbin is open source under the MIT licence (Will McCutchen and the go-httpbin contributors); the source code is at github.com/mccutchen/go-httpbin. It has no dependencies beyond the Go standard library, whose BSD style licence ships with it. The licence texts are in /usr/share/doc/cloudimg-go-httpbin/licences/. There is no per user licence fee. The image price covers cloudimg's build, hardening, first boot setup and support.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA