Og
Developer Tools Azure

OpenAPI Generator on Ubuntu 24.04 on Azure User Guide

| Product: OpenAPI Generator on Ubuntu 24.04 LTS on Azure

Overview

OpenAPI Generator turns an OpenAPI specification into working code. Give it your API description and it returns a client SDK, a server stub or a documentation set, ready to unpack and use, across more than 150 language and framework targets. This image is the service form of it — the openapi-generator-online module: a REST API your build pipeline can call, and an interactive console in the browser for the times you just want a client for something right now.

Running it yourself matters because an internal API description is design documentation for your system. A private generator keeps it inside your own network while still giving every team the same generated clients, from the same generator version, in every pipeline.

What is included:

  • OpenAPI Generator Online 7.25.0, the unmodified upstream artefact from Maven Central at /opt/openapi-generator/, pinned by version and by the SHA-256 of the jar, run by systemd as the unprivileged oagen service account
  • nginx terminating TLS on port 443 and requiring an HTTP basic credential on every request. This is load-bearing rather than decorative: the upstream service has no authentication of any kind — it is published as an open demo — so the front door is the entire access-control story, and it has no exemptions
  • No default credential. A unique password is generated on the first boot of your VM, stored only as a salted SHA-512 hash, and written to a file only root can read
  • The generator bound to 127.0.0.1:8080, where nothing outside the instance can reach it. Off-box TCP is exactly 22 and 443; there is deliberately no port 80
  • Generated output written to /var/lib/openapi-generator/work, a directory the service account owns and the service's own systemd sandbox confines it to
  • A bounded cleanup on an hourly timer, so a long-running instance cannot fill its disk however heavily it is used
  • Confined specification fetching: generating from a URL keeps working, but the service cannot reach the Azure instance metadata service, the Azure wire server or anything on your private network while doing it
  • OpenJDK 21 (headless JRE) from Ubuntu main, so the JVM is patched by Ubuntu security like the rest of the image. No build toolchain ships: javac, mvn, gradle and jar are all absent
  • On-VM self tests that prove the whole posture, each one checked against a known-bad input first
  • The openapi-generator and nginx systemd services, enabled and active, plus the openapi-generator-reap.timer housekeeping timer

OpenAPI is a trademark of the Linux Foundation. All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them. This image packages the unmodified open source software, which is distributed under the Apache License 2.0.

The services, the SHA-256 pinned artefacts and the exact listening sockets, with the generator itself on loopback

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU, 4 GiB RAM) is the recommended size: one JVM with a 65% heap ceiling and nginx sit comfortably inside it, and generating a large SDK is a few seconds of CPU rather than a memory problem. Choose a larger size if many pipelines will generate concurrently.

Network security group inbound rules: 22/tcp from your management network, and 443/tcp from wherever your developers and build agents connect. There is deliberately no port 80 — the only credential this service has is an HTTP basic credential, and a plaintext port is a port that leaks it.

Step 1: Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for OpenAPI Generator 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). Then select Review + create and Create. Open 443 afterwards, as shown in the next step.

Step 2: Deploy from the Azure CLI

Accept the image terms once per subscription, then deploy:

az vm image terms accept --urn cloudimg:openapi-generator-ubuntu-24-04:default:latest

az group create --name openapi-generator-rg --location eastus

az vm create \
  --resource-group openapi-generator-rg \
  --name openapi-generator-vm \
  --image cloudimg:openapi-generator-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

az vm open-port --resource-group openapi-generator-rg --name openapi-generator-vm --port 443 --priority 1001

Restrict 443 to the networks your developers and build agents actually come from rather than leaving it open to the internet. The credential gate below is what protects the service, but a smaller exposed surface is still a smaller exposed surface.

Step 3: Retrieve this instance's credential

The image ships no usable credential. On the first boot of your VM a unique password is generated for that instance alone and written to /root/openapi-generator-credentials.txt, readable only by root:

sudo cat /root/openapi-generator-credentials.txt

The values you need:

Key What it is
OAG_URL the console address for this VM
OAG_HOST this VM's customer-facing address, resolved at first boot
OAG_AUTH_USER the front-door username (apiuser)
OAG_AUTH_PASSWORD the front-door password, unique to this VM
OAG_TLS_CERT the certificate this VM serves, so on-VM tooling can verify it

The password is never stored in clear anywhere else. nginx checks it against a salted SHA-512 hash in /etc/openapi-generator/htpasswd, which is 0640 root:www-data:

sudo bash -c 'ls -l /etc/openapi-generator/htpasswd; grep -oE "^[^:]+:\\\$6\\\$" /etc/openapi-generator/htpasswd'

The per-VM credentials file, the salted hash nginx checks against, and the confined specification-fetch egress

Step 4: Open the console

Browse to https://<OAG_HOST>/ and sign in with OAG_AUTH_USER and OAG_AUTH_PASSWORD when the browser asks. The image ships a certificate generated for your VM's own address; because it is self-signed your browser will warn once, and Step 10 shows how to install your own.

The console lists every operation the service offers, grouped into clients and servers, with the API server set to your own instance:

The interactive console listing every generation operation, with the API server set to this instance's own address

Expand GET /api/gen/clients and press TRY to see the full catalogue of client and documentation generators this version supports:

The client generator catalogue returned live by the appliance

From the VM itself, the same call over the API:

sudo bash -c 'CF=/root/openapi-generator-credentials.txt; H=$(grep "^OAG_HOST=" $CF | cut -d= -f2-); U=$(grep "^OAG_AUTH_USER=" $CF | cut -d= -f2-); P=$(grep "^OAG_AUTH_PASSWORD=" $CF | cut -d= -f2-); curl -s --cacert /etc/ssl/openapi-generator/openapi-generator.crt --resolve "$H:443:127.0.0.1" -u "$U:$P" "https://$H/api/gen/clients" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d), \"client generators\"); print(\", \".join(d[:12]), \"...\")"'

The --resolve flag is there because an Azure VM cannot dial its own public IP: the Standard Load Balancer does not hairpin, so a request from the VM to its own public address times out. --resolve keeps your real address in the URL and in the TLS check while sending the connection to loopback. From your laptop you just use the URL.

Step 5: Generate a client

This is what the product is for. Post an OpenAPI specification and the target language; you get back a one-time download link.

In the console, expand POST /api/gen/clients/{language}, set language to python, paste your specification into the body as {"spec": { ... }} and press TRY:

A Python client generated from a submitted specification, with the one-time download link the appliance returned

From the API, end to end — generate, download, and look at what came back:

sudo bash -c 'set -e
CF=/root/openapi-generator-credentials.txt
CRT=/etc/ssl/openapi-generator/openapi-generator.crt
H=$(grep "^OAG_HOST=" $CF | cut -d= -f2-)
U=$(grep "^OAG_AUTH_USER=" $CF | cut -d= -f2-)
P=$(grep "^OAG_AUTH_PASSWORD=" $CF | cut -d= -f2-)
T=$(mktemp -d)
/usr/local/sbin/openapi-generator-write-spec.sh $T/spec.json
python3 -c "import json;json.dump({\"spec\":json.load(open(\"$T/spec.json\")),\"options\":{\"packageName\":\"acme_client\"}},open(\"$T/body.json\",\"w\"))"
CODE=$(curl -s -m 600 --cacert $CRT --resolve "$H:443:127.0.0.1" -u "$U:$P" -X POST -H "Content-Type: application/json" --data @$T/body.json "https://$H/api/gen/clients/python" | python3 -c "import json,sys;print(json.load(sys.stdin)[\"code\"])")
echo "download id: $CODE"
curl -s -m 600 --cacert $CRT --resolve "$H:443:127.0.0.1" -u "$U:$P" -o $T/client.zip -w "download: HTTP %{http_code}  %{size_download} bytes\n" "https://$H/api/gen/download/$CODE"
unzip -l $T/client.zip | tail -n +4 | head -8
rm -rf $T'

Three things about that download link are worth knowing:

  • It is one-time. The archive is deleted as it is read, so a second request for the same link fails. Generate again for another copy.
  • It is built from this instance's own address, taken from GENERATOR_HOST in /etc/openapi-generator/openapi-generator.env. It is deliberately not built from the incoming request: left to the request, any caller could choose the host their download link points at by setting a forwarded-host header.
  • It expires. Upstream holds generated archives for 24 hours; the housekeeping in Step 11 clears what is left after that.

An OpenAPI specification posted through the authenticated front door, a Python client generated, downloaded and unpacked to real source

To see what a particular generator can be tuned with, ask it. Every generator has its own option set — package names, library choices, serialisation, and so on — and the console shows them under GET /api/gen/clients/{language}:

The full option set for the Python generator, returned live by the appliance

sudo bash -c 'CF=/root/openapi-generator-credentials.txt; H=$(grep "^OAG_HOST=" $CF | cut -d= -f2-); U=$(grep "^OAG_AUTH_USER=" $CF | cut -d= -f2-); P=$(grep "^OAG_AUTH_PASSWORD=" $CF | cut -d= -f2-); curl -s --cacert /etc/ssl/openapi-generator/openapi-generator.crt --resolve "$H:443:127.0.0.1" -u "$U:$P" "https://$H/api/gen/clients/typescript-axios" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d), \"options:\"); print(\", \".join(sorted(d)[:10]), \"...\")"'

Pass any of them in the options object alongside spec.

Step 6: Generate a server stub

The same call, against /api/gen/servers/{framework}. Ask the instance which frameworks it supports:

sudo bash -c 'CF=/root/openapi-generator-credentials.txt; H=$(grep "^OAG_HOST=" $CF | cut -d= -f2-); U=$(grep "^OAG_AUTH_USER=" $CF | cut -d= -f2-); P=$(grep "^OAG_AUTH_PASSWORD=" $CF | cut -d= -f2-); curl -s --cacert /etc/ssl/openapi-generator/openapi-generator.crt --resolve "$H:443:127.0.0.1" -u "$U:$P" "https://$H/api/gen/servers" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d), \"server generators\"); print(\", \".join(d[:12]), \"...\")"'

Step 7: Generating from a specification URL, and what this image will fetch

Instead of spec, you can send openAPIUrl and the service will fetch the specification itself. That is genuinely useful — generating a client for a published API is a one-liner — but it means the service makes outbound requests to addresses a caller chooses.

This image therefore confines where those requests can go. The service's systemd unit carries an IP egress filter that denies the Azure instance metadata service (169.254.0.0/16), the Azure wire server (168.63.129.16) and every RFC1918 private range, while leaving public addresses and DNS alone. Public specification URLs keep working; a caller cannot turn the generator into a probe for your instance's identity tokens or your private network.

You can confirm it, with its own control:

sudo /usr/local/sbin/openapi-generator-egress-check.sh

That check reports what happens inside the filter and, as a control, what happens without it — because a metadata service that is unreachable for some unrelated reason would otherwise make the filter look like it was working when it was not.

If your specifications live on an internal host you do want the generator to reach, add that host's address to IPAddressAllow= in a drop-in rather than removing the deny list:

sudo bash -c 'systemctl cat openapi-generator.service | grep -E "^IPAddress"'

Step 8: Verify the security posture yourself

Everything this image claims about its front door is executable. The credential gate:

sudo /usr/local/sbin/openapi-generator-verify-auth.sh

It asserts that an unauthenticated request is refused 401 with a WWW-Authenticate challenge on the API, the console, the API document, the download endpoint and the static assets; that a wrong password, a blank password, a wrong username, well-known weak guesses and a malformed authorisation header are all refused; that this VM's own credential is accepted on a browser-shaped request and really returns a populated generator catalogue; that the generator's own port does not answer on the VM's external address and nothing answers on port 80; that TLS verification refuses an address the certificate does not name; and that a hostile forwarded-host header cannot move a download link.

Every one of those checks is itself tested against a known-bad input, because a gate that cannot fail proves nothing:

sudo /usr/local/sbin/openapi-generator-gate-selftest.sh

And the exact off-box listening set:

sudo /usr/local/sbin/openapi-generator-port-check.sh

The front door refusing everything except this instance's own credential, with every gate proven against a known-bad input first

The whole appliance in one command, including a real generate-and-download round trip whose archive is checked file by file:

sudo /usr/local/sbin/openapi-generator-selftest.sh

Step 9: Rotate the front-door password

Use the supplied helper. It reads the new password from standard input rather than from the command line, so it never lands in the process table, the journal or your shell history, and it refuses empty, short, all-digit, colon-bearing and well-known weak passwords. It also proves the rotation took before it returns: the new password must work and the old one must not.

printf '%s' '<new-password>' | sudo /usr/local/sbin/openapi-generator-set-password.sh

It rewrites both the hash nginx checks and the credentials file, so the two cannot drift apart. nginx re-reads the file per request — no restart, no dropped connections.

Step 10: Use your own certificate and hostname

Put your certificate and key at /etc/ssl/openapi-generator/openapi-generator.crt and .key (key 0600 root:root), then tell the instance the address it should advertise in download links, and restart:

sudo bash -c 'echo "GENERATOR_HOST=https://<your-domain>" > /etc/openapi-generator/openapi-generator.env; systemctl restart openapi-generator nginx'

GENERATOR_HOST is what every download link is built from, so change it in the same breath as the certificate. If you leave it pointing at the old address your clients will still generate but their download links will name the wrong host.

Step 11: Generated output and disk housekeeping

Generated archives live under /var/lib/openapi-generator/work and are removed as they are downloaded. Two things then keep the disk bounded:

  • Upstream expires an archive's download link after 24 hours and sweeps the expired ones hourly.
  • This image adds its own hourly reaper for what that sweep structurally cannot see: a generation that failed leaves a working directory behind with no link to expire. The reaper removes anything older than 25 hours — deliberately past upstream's own window, so it can never destroy a link that is still valid — and enforces a 2 GB ceiling, pruning oldest-first when it is exceeded.
sudo bash -c 'systemctl list-timers openapi-generator-reap.timer --no-pager | head -3; du -sh /var/lib/openapi-generator/work'

Run it by hand at any time, and check the instance is holding nothing:

sudo /usr/local/sbin/openapi-generator-reap.sh
sudo /usr/local/sbin/openapi-generator-workdir-empty.sh

To change the ceiling, edit ExecStart= in /etc/systemd/system/openapi-generator-reap.service and add --max-mb <N>.

Step 12: Upgrading

The generator is a single pinned jar, so an upgrade is a download, a checksum and a restart. Check what you are running:

sudo bash -c 'ls -1 /opt/openapi-generator/*.jar; unzip -p /opt/openapi-generator/openapi-generator-online-7.25.0.jar META-INF/MANIFEST.MF | grep -i "^implementation-version"'

New releases are published to Maven Central under org.openapitools:openapi-generator-online; https://repo1.maven.org/maven2/org/openapitools/openapi-generator-online/maven-metadata.xml is upstream's own statement of the current line. Note that the project's GitHub releases attach only the command-line jar, so the online service's artefact is on Maven Central and nowhere else.

To upgrade, download the new jar, verify its checksum against the .sha1 published beside it, place it in /opt/openapi-generator/, point ExecStart= at it in /etc/systemd/system/openapi-generator.service, then systemctl daemon-reload && systemctl restart openapi-generator. Re-run openapi-generator-selftest.sh afterwards.

One upgrade note specific to this product. The published openapi-generator-online jar bundles commons-lang3 3.12.0, because its build inherits Spring Boot 2.5's dependency management, while the generator core calls a class that only exists from 3.17.0. Without a correction, every Python-family generator fails with HTTP 500. This image launches the unmodified jar through Spring Boot's own PropertiesLauncher with -Dloader.path=/opt/openapi-generator/ext, which puts the correct commons-lang3 ahead of the bundled one. Keep that flag when you upgrade, and check whether the new release still needs it:

sudo bash -c 'unzip -l /opt/openapi-generator/openapi-generator-online-7.25.0.jar | grep -o "commons-lang3-[0-9.]*jar"; ls -1 /opt/openapi-generator/ext/'

Troubleshooting

The console loads but shows nothing. The console is driven entirely by the API document at /api-docs, which becomes available a few seconds after the generation API does. The service's own readiness gate waits for both before nginx is allowed to start, so this should not happen; if it does, check the service came up cleanly:

sudo bash -c 'systemctl is-active openapi-generator nginx; journalctl -u openapi-generator -n 20 --no-pager | tail -5'

A generation returns HTTP 400. The specification was not accepted. The most common cause is an anonymous inline schema where a generator expects a named one — move the schema into components/schemas and reference it. A missing spec and a missing openAPIUrl also give 400, as does an unknown generator name.

A download returns HTTP 500 on the second try. That is expected: archives are one-time. A link that was never issued returns 404.

Everything returns 401. Check the credential you are sending against /root/openapi-generator-credentials.txt. Remember there are no exempt paths — the console, the API document and the static assets are all behind the same credential.

The service will not start. It refuses to start if /etc/openapi-generator/openapi-generator.env is missing; that is deliberate, so it can never run with download links built from whatever a caller asks for. Restore the file with a GENERATOR_HOST=https://<address> line.

sudo bash -c 'systemctl is-enabled openapi-generator openapi-generator-firstboot nginx openapi-generator-reap.timer | tr "\n" " "; echo'

Support

This image is a repackaged open source product with 24/7 cloudimg support for deployment, upgrades, TLS certificates and custom domains, pipeline and CI integration, generator configuration and templates, and scaling. Contact support through the Azure Marketplace listing or at cloudimg.co.uk.