Deployment guide

go-httpbin

Overview

This guide covers deploying and using go-httpbin 2.26.0 on Ubuntu 24.04 LTS from SecureAnyCloud 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) and returns any status code you ask for (/status/418). It also has endpoints for redirects, delays, slow drips, streams, 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. This SecureAnyCloud image locks go-httpbin down so it is safe to run on a network.

What you get:

  • 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. The whole service therefore sits behind an HTTP Basic Auth prompt, with a password 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/sac/data/go-httpbin-credentials.txt (root only). nginx stores only a SHA-512 crypt hash, and common guesses are refused.
  • The gate password never reaches go-httpbin. nginx removes the Authorization header before passing a request on. /headers, /anything and /dump/request can 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, Host or Forwarded headers a request carries.
  • Redirects stay on the VM. /redirect-to only 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. nginx therefore refuses every state-changing request (POST, PUT, DELETE and the like) and every WebSocket upgrade whose browser Origin is 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/x509 advisory 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 with a subnet in your region.
  • A network security group that allows TCP 22 (SSH), TCP 443 (go-httpbin) and TCP 80 (redirects to HTTPS). Open these ports to the networks your clients and test runners use, because a test endpoint rarely needs to be reachable from the whole internet. go-httpbin needs no other port.
1

Deploy from the Azure portal

Search the Azure Marketplace for go-httpbin, choose the SecureAnyCloud offer and select Create.

Pick your subscription, resource group and region. Choose size Standard_B2s, add an SSH public key for the admin user, and allow inbound ports 22, 80 and 443. Then review and create the VM.

2

Deploy from the Azure CLI

RG="go-httpbin-rg"; LOCATION="eastus"; VM_NAME="go-httpbin1" IMAGE_URN="sac1647283583153: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. Where possible, narrow the rules to your own networks with az network nsg rule update --source-address-prefixes <your-cidr>.

3

Connect and check the services

SSH to the VM as azureuser:

ssh azureuser@<public-ip>

On first boot, the VM reads its 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. Confirm that the units are active:

systemctl is-active go-httpbin nginx go-httpbin-firstboot go-httpbin-address
sudo test -f /var/lib/sac/data/go-httpbin-firstboot.done && echo FIRSTBOOT_DONE

The output shows all four units active and the first-boot marker present:

active active active active FIRSTBOOT_DONE

List the listening sockets. Only SSH and nginx listen on the network, and 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"))

Run the built-in self-test, which 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
4

Get your URL and password

Read the credentials file:

sudo cat /var/lib/sac/data/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/sac/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://secureanycloud.com/stacks/go-httpbin/

The URL uses the VM's public IP. The first-boot service cannot reach the metadata service itself. Instead, a small service named go-httpbin-address, which has no access to go-httpbin's files, reads the address from the Azure Instance Metadata Service on every boot and records it here. This also works for a Standard SKU public IP, which only the metadata service's load balancer view shows:

cat /run/sac-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, use a curl config file that holds user = "httpbin:<password>" and run curl -k -K <file> https://<public-ip>/get.

The certificate is self-signed. Do not add it to a trust store. Instead, 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. Alternatively, 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.

5

Send your first requests

Run the commands below 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/sac/data/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/sac/data/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 show the address the request really came from. Here that is the VM itself. From your machine, they show your address. There is no Authorization header, because 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/sac/data/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/sac/data/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 endpoint.

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 those limits, go-httpbin answers 400 and says why:

H=$(sudo sed -n 's#^GO_HTTPBIN_URL=https://\([^/]*\)/$#\1#p' /var/lib/sac/data/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/sac/data/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. 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 and change the two values. Then change client_max_body_size in /etc/nginx/sites-available/sac-go-httpbin to match, restart go-httpbin and reload nginx.

7

Redirects

/redirect/<n>, /relative-redirect/<n> and /absolute-redirect/<n> redirect within the VM. /redirect-to?url= redirects wherever the url says. On an open server, that makes it an open redirector, so here it accepts only a path on this VM or this VM's own name:

H=$(sudo sed -n 's#^GO_HTTPBIN_URL=https://\([^/]*\)/$#\1#p' /var/lib/sac/data/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/sac/data/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, such as the test system your client should end up at, list them one per line in /etc/sac/go-httpbin-redirect-domains and re-apply the configuration. Both nginx and go-httpbin then accept them:

echo '<your-domain>' | sudo tee -a /etc/sac/go-httpbin-redirect-domains
sudo go-httpbin-set-hostname --refresh

Only names are accepted, one per line, without a scheme, path or wildcard.

8

Auth challenge endpoints

/basic-auth/<user>/<password>, /hidden-basic-auth/..., /digest-auth/... and /bearer test how a client answers an authentication challenge. They use the same Authorization header as the gate, so 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

To reach go-httpbin directly from your machine, run ssh -L 8080:127.0.0.1:8080 azureuser@<public-ip>. While the tunnel is open, http://127.0.0.1:8080/ on your machine reaches go-httpbin.

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 the name and pins nginx, the cross-site guard and the redirect allow list to it. It also redirects the VM's IP addresses to the name:

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. Then reload nginx.

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, and it refuses a request body with a Content-Encoding. 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/sac/data/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/sac/data/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. Those responses only reflect that page's own request, because the gate's Authorization header is gone before go-httpbin sees it. A browser-shaped cross-site request shows this:

H=$(sudo sed -n 's#^GO_HTTPBIN_URL=https://\([^/]*\)/$#\1#p' /var/lib/sac/data/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/sac/data/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']

Show 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 Origin header (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/echo works through nginx over HTTP/1.1 (wss://<your host>/websocket/echo with 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 OFF to -log-level INFO with sudo systemctl edit --full go-httpbin.
  • What go-httpbin exposes. /env is empty, /hostname returns a fixed generic value, and /version returns only the service name. Nothing about the VM is published, and 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/sac-go-httpbin/BUILD-INFO.txt records the source archive, the Go toolchain and the checksums of the shipped files.
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 a new version means deploying it and pointing your clients at it. The release notes list what changed.

Support and licensing

go-httpbin is open source under the MIT license (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 license ships with it. The license texts are in /usr/share/doc/sac-go-httpbin/licences/.

There is no per-user license fee. The image price covers SecureAnyCloud's build, hardening, first-boot setup and support.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com
  • Support: 24/7, with a 24 hour response SLA

Deploy on Azure

Launch go-httpbin 2.26.0 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