Stoplight Prism 5.16.0 on AWS User Guide
Overview
This guide covers launching and using the Stoplight Prism 5.16.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg developer and API tools listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Stoplight Prism turns an API description into a working API before the real one exists. Point it at an OpenAPI (2.0 or 3.x) or Postman document and every operation answers with the examples from your spec, so front end, mobile and integration teams can build against it straight away. Every request is checked against the contract as well: a request that breaks the spec gets a 422 with a machine-readable explanation. In proxy mode Prism sits in front of your real API, forwards each request and validates both the request and the response, which shows whether a running service honours its published contract.
The cloudimg image runs Prism as a private service:
- HTTPS from the first boot. nginx serves Prism on port 443 with a certificate generated on your instance's first boot. Port 80 only redirects to HTTPS.
- A per-instance API key in front of everything. Prism has no access control of its own. Every request must carry this instance's key in an
X-Cloudimg-Keyheader; without it nginx answers401before Prism sees anything. The key is random, generated on first boot, and stored root-only. - Your spec's own security still works. The gate deliberately uses its own header instead of HTTP Basic Auth. Prism validates the security schemes your spec declares (bearer tokens, basic auth, API keys) against the
Authorizationheader, and a Basic Auth gate would have to take that header for itself. The image passesAuthorizationto Prism untouched, so a bearer-protected operation behaves exactly as your spec says. - No default login. The image contains no key, no certificate and no host name. First boot writes them to
/var/lib/cloudimg/prism-credentials.txt(root only). - Prism cannot reach inside your AWS account. Prism makes outbound requests on your behalf: for a spec URL, for remote
$refreferences in a spec, for OpenAPI callbacks to a URL named in a request, and to the upstream in proxy mode. Its service is blocked at the kernel level from the EC2 instance metadata service (IPv4 and IPv6), from every private address in your VPC unless you allow one proxy upstream, and from this instance's own loopback services. Public addresses keep working. - Prism runs locked in. It listens on
127.0.0.1:4010only, as its own unprivileged user with no capabilities, a read-only file system apart from its (empty) state directory and a 1 GB memory cap. Specs live in/etc/prism/spec/, which Prism can read but not change. CORS is off. - Pinned and locked. Node.js 24.22.1 LTS comes from the official release, checked against its published checksum. Prism and its 209 dependencies are installed from a locked dependency tree with every package checked against its recorded hash, and no package install script runs.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Stoplight Prism 5.16.0 (/opt/prism/), Node.js 24.22.1 (/opt/nodejs/), nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. Settings are in /etc/prism/; the build record is in /usr/share/doc/cloudimg-stoplight-prism/. cloudimg also masks the fwupd-refresh timer (EC2 has no firmware it can update).
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet.
- A security group that allows TCP 22 (SSH), TCP 443 (Prism) and TCP 80 (redirects to HTTPS). Open them to your own networks only.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Stoplight Prism 5.16.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair, a security group as above and a subnet.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=stoplight-prism}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Stoplight Prism 5.16.0 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot takes under a minute: it generates the certificate and the API key, then starts Prism and nginx. Check that everything is running:
systemctl is-active prism nginx prism-firstboot prism-egress-guard
sudo test -f /var/lib/cloudimg/prism-firstboot.done && echo FIRSTBOOT_DONE
active
active
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on the network; Prism listens on loopback (its Node.js process shows as MainThread):
sudo ss -lntp | awk 'NR>1 && $4 !~ /^127\.0\.0\.5[34]/ {print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u
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:4010 users:(("MainThread"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
Step 4: Get your URL and API key
Run sudo cat /var/lib/cloudimg/prism-credentials.txt to see your URL and API key. The file is readable by root only and holds these settings, all generated for this instance:
sudo sed -n 's/^\(PRISM_[A-Z0-9_]*\)=.*/\1/p' /var/lib/cloudimg/prism-credentials.txt
PRISM_URL
PRISM_HOST
PRISM_GATE_HEADER
PRISM_API_KEY
PRISM_TLS_CERT
PRISM_TLS_SHA256_FINGERPRINT
PRISM_URL is https://<public-ip>/. Send PRISM_API_KEY in the X-Cloudimg-Key header with every request. The instance's private IP address and its EC2 DNS names are served too, so clients inside your VPC can use them.
The certificate is self-signed and generated on your instance. When a client reports it, compare its SHA-256 fingerprint with PRISM_TLS_SHA256_FINGERPRINT before you trust the connection. Do not add the certificate to a trust store; to use a CA-issued certificate instead, see Step 11.
Step 5: Call the mock
The image serves a small sample spec out of the box, /etc/prism/spec/cloudimg-sample-api.yaml (a Widgets API). These commands run on the instance and read the key from the credentials file without printing it; --resolve sends the request to nginx on this instance while keeping the certificate's name:
H=$(sudo sed -n 's/^PRISM_HOST=//p' /var/lib/cloudimg/prism-credentials.txt)
key() { sudo sed -n 's/^PRISM_API_KEY=\(.*\)/header = "X-Cloudimg-Key: \1"/p' /var/lib/cloudimg/prism-credentials.txt; }
P=(curl -s --cacert /etc/ssl/prism/prism.crt --resolve "$H:443:127.0.0.1")
"${P[@]}" -K <(key) "https://$H/widgets" | python3 -m json.tool
[
{
"id": 1,
"name": "Acme Sprocket",
"color": "blue",
"inStock": true
},
{
"id": 2,
"name": "Acme Cog",
"color": "green",
"inStock": false
}
]
That is the example the spec gives for GET /widgets, served verbatim. You can check it against the spec file itself:
H=$(sudo sed -n 's/^PRISM_HOST=//p' /var/lib/cloudimg/prism-credentials.txt)
key() { sudo sed -n 's/^PRISM_API_KEY=\(.*\)/header = "X-Cloudimg-Key: \1"/p' /var/lib/cloudimg/prism-credentials.txt; }
P=(curl -s --cacert /etc/ssl/prism/prism.crt --resolve "$H:443:127.0.0.1")
cd "$(mktemp -d)"
"${P[@]}" -K <(key) "https://$H/widgets" > widgets.json
python3 - <<'PY'
import json, yaml
spec = yaml.safe_load(open("/etc/prism/spec/cloudimg-sample-api.yaml"))
want = spec["paths"]["/widgets"]["get"]["responses"]["200"]["content"]["application/json"]["example"]
got = json.load(open("widgets.json"))
print("GET /widgets returns the spec example:", got == want)
PY
GET /widgets returns the spec example: True
Step 6: See contract validation
Prism checks every request against the spec. NewWidget requires a name, so a create without one is refused with a 422 and an application/problem+json body that says exactly what is wrong:
H=$(sudo sed -n 's/^PRISM_HOST=//p' /var/lib/cloudimg/prism-credentials.txt)
key() { sudo sed -n 's/^PRISM_API_KEY=\(.*\)/header = "X-Cloudimg-Key: \1"/p' /var/lib/cloudimg/prism-credentials.txt; }
P=(curl -s --cacert /etc/ssl/prism/prism.crt --resolve "$H:443:127.0.0.1")
cd "$(mktemp -d)"
"${P[@]}" -K <(key) -o created.json -w 'valid widget: %{http_code}\n' -H 'Content-Type: application/json' -d '{"name":"Gear","color":"red"}' "https://$H/widgets"
"${P[@]}" -K <(key) -o problem.json -w 'missing name: %{http_code} %{content_type}\n' -H 'Content-Type: application/json' -d '{"color":"red"}' "https://$H/widgets"
python3 -c 'import json; p=json.load(open("problem.json")); print(p["title"]); [print("-", v["location"], v["code"], v["message"]) for v in p["validation"]]'
valid widget: 201
missing name: 422 application/problem+json
Invalid request
- ['body'] required Request body must have required property 'name'
Prism is stateless: the 201 returns the spec's example and nothing is stored, so GET /widgets keeps returning the same list.
Step 7: Your spec's own security schemes
DELETE /widgets/{id} in the sample spec declares HTTP bearer authentication. The cloudimg gate only looks at X-Cloudimg-Key and passes Authorization to Prism unchanged, so Prism enforces the spec's scheme itself:
H=$(sudo sed -n 's/^PRISM_HOST=//p' /var/lib/cloudimg/prism-credentials.txt)
key() { sudo sed -n 's/^PRISM_API_KEY=\(.*\)/header = "X-Cloudimg-Key: \1"/p' /var/lib/cloudimg/prism-credentials.txt; }
P=(curl -s --cacert /etc/ssl/prism/prism.crt --resolve "$H:443:127.0.0.1")
"${P[@]}" -K <(key) -o /dev/null -w 'DELETE without a bearer token: %{http_code}\n' -X DELETE "https://$H/widgets/1"
"${P[@]}" -K <(key) -o /dev/null -w 'DELETE with a bearer token: %{http_code}\n' -X DELETE -H 'Authorization: Bearer my-test-token' "https://$H/widgets/1"
DELETE without a bearer token: 401
DELETE with a bearer token: 204
The two 401s are easy to tell apart: the gate's own 401 carries a WWW-Authenticate: CloudimgKey challenge and the body {"error":"unauthorized",...}; a 401 from your spec's security scheme comes from Prism.
Step 8: Call it from your own machine
Copy the certificate to your machine once, check its fingerprint against PRISM_TLS_SHA256_FINGERPRINT, and pass it to curl. Keep the key out of your shell history by putting it in a curl config file:
scp -i <your-key.pem> ubuntu@<public-ip>:/etc/ssl/prism/prism.crt ./prism.crt
openssl x509 -in prism.crt -noout -fingerprint -sha256
printf 'header = "X-Cloudimg-Key: %s"\n' '<PRISM_API_KEY>' > prism-key.cfg && chmod 600 prism-key.cfg
curl --cacert prism.crt -K prism-key.cfg https://<public-ip>/widgets
Any HTTP client works the same way: send the X-Cloudimg-Key header and trust (or pin) this instance's certificate. For a browser front end under development, call Prism through your development server's proxy (for example the Vite or webpack dev-server proxy) and let the proxy add the header: the image does not send CORS headers, so a page on another origin cannot call it directly, and the key never has to live in browser code.
Step 9: Mock your own API
Copy your OpenAPI or Postman file into /etc/prism/spec/ and switch Prism to it. prism-set-mode checks the file, restarts Prism and fails if the spec does not load. This example mocks a small Todo API, then switches back to the sample:
sudo tee /etc/prism/spec/todo-api.yaml >/dev/null <<'YAML'
openapi: 3.0.3
info: {title: Todo API, version: 1.0.0}
paths:
/todos:
get:
responses:
'200':
description: The todo list
content:
application/json:
example: [{id: 1, title: Write the API spec, done: true}, {id: 2, title: Mock it with Prism, done: false}]
YAML
sudo prism-set-mode mock /etc/prism/spec/todo-api.yaml
H=$(sudo sed -n 's/^PRISM_HOST=//p' /var/lib/cloudimg/prism-credentials.txt)
key() { sudo sed -n 's/^PRISM_API_KEY=\(.*\)/header = "X-Cloudimg-Key: \1"/p' /var/lib/cloudimg/prism-credentials.txt; }
curl -s --cacert /etc/ssl/prism/prism.crt --resolve "$H:443:127.0.0.1" -K <(key) "https://$H/todos"; echo
sudo prism-set-mode mock /etc/prism/spec/cloudimg-sample-api.yaml
sudo rm /etc/prism/spec/todo-api.yaml
prism-set-mode: mode=mock spec=/etc/prism/spec/todo-api.yaml
[{"id":1,"title":"Write the API spec","done":true},{"id":2,"title":"Mock it with Prism","done":false}]
prism-set-mode: mode=mock spec=/etc/prism/spec/cloudimg-sample-api.yaml
A spec can also be an https:// URL (sudo prism-set-mode mock https://...). Specs and remote $ref references on private or loopback addresses do not load (see Step 12).
Prism's other options go in PRISM_FLAGS in /etc/prism/prism.env: --dynamic generates responses from the schemas instead of the examples, --errors turns contract violations into error responses, and --seed=<word> makes dynamic responses repeatable. Edit the file, then restart Prism:
sudo nano /etc/prism/prism.env
sudo systemctl restart prism
Only those flags are accepted: the listen address, the port and CORS are fixed so Prism stays behind the gate.
Step 10: Validate a real API with proxy mode
In proxy mode Prism forwards every request to your API and validates the request and the response against the spec; violations are logged (journalctl -u prism) and, with --errors, returned to the caller. Proxy mode is off until you switch it on:
sudo prism-set-mode proxy https://api.<your-domain> /etc/prism/spec/<your-spec>.yaml
journalctl -u prism -n 20 --no-pager
sudo prism-set-mode mock /etc/prism/spec/cloudimg-sample-api.yaml
The upstream must be a plain http:// or https:// address. If it resolves to a private address in your VPC, prism-set-mode allows exactly that address for Prism and nothing else. It refuses the instance metadata service, link-local and loopback addresses, so an upstream cannot be this instance's own services (its private IP included). Requests reach your API with the client's Authorization header unchanged, without X-Cloudimg-Key, and with X-Forwarded-For set to the real client address. Check the current settings with sudo prism-set-mode show.
Step 11: Use your own domain name or certificate
Point a DNS record at the instance, then set the name. The helper generates a new certificate for it (and the instance's addresses), pins nginx to those names (other host names get no answer) and prints the new fingerprint:
sudo prism-set-hostname <your-domain>
To use a CA-issued certificate, replace /etc/ssl/prism/prism.crt and /etc/ssl/prism/prism.key with your own (keep the key root only, mode 0600) and run sudo systemctl reload nginx. If the instance's public IP changes after a stop and start, run sudo prism-set-hostname with the new address, or use an Elastic IP.
Step 12: What the front door refuses
Without the key every path answers 401, and a wrong key gets the same plain 401 (there is no lockout to trip, and the key is 192 random bits). With the key, another web site still cannot post to the mock, compressed request bodies are refused, plain HTTP only redirects and unknown host names get no answer:
H=$(sudo sed -n 's/^PRISM_HOST=//p' /var/lib/cloudimg/prism-credentials.txt)
key() { sudo sed -n 's/^PRISM_API_KEY=\(.*\)/header = "X-Cloudimg-Key: \1"/p' /var/lib/cloudimg/prism-credentials.txt; }
P=(curl -s --cacert /etc/ssl/prism/prism.crt --resolve "$H:443:127.0.0.1")
"${P[@]}" -o /dev/null -w 'no key: %{http_code}\n' "https://$H/widgets"
"${P[@]}" -o /dev/null -w 'wrong key: %{http_code}\n' -H 'X-Cloudimg-Key: not-the-key' "https://$H/widgets"
"${P[@]}" -K <(key) -o /dev/null -w 'cross-site POST: %{http_code}\n' -H 'Origin: https://evil.example' -H 'Content-Type: application/json' -d '{"name":"x"}' "https://$H/widgets"
"${P[@]}" -K <(key) -o /dev/null -w 'compressed body: %{http_code}\n' -H 'Content-Encoding: gzip' -H 'Content-Type: application/json' --data-binary 'x' "https://$H/widgets"
curl -s -o /dev/null -w 'plain http: %{http_code} %header{location}\n' --resolve "$H:80:127.0.0.1" "http://$H/widgets"
curl -sk -o /dev/null -w 'unknown host name: %{http_code}\n' --resolve "other.example:443:127.0.0.1" https://other.example/widgets || true
no key: 401
wrong key: 401
cross-site POST: 403
compressed body: 415
plain http: 301 https://<public-ip>/widgets
unknown host name: 000
Because the key is a custom header, a browser never sends it on its own: a page on another site cannot make your browser call the mock with your credentials, which is the risk a password gate carries.
Prism's outbound fence is visible in its unit and in the nftables rule for its service account (the metadata addresses 169.254.169.254 and fd00:ec2::254 fall inside the listed ranges):
systemctl show -p IPAddressDeny --value prism | tr ' ' '\n' | sort
sudo nft list table inet cloudimg_prism_egress | grep -oE 'oifname "lo" .*(accept|reject)' | sed -E 's/counter packets [0-9]+ bytes [0-9]+ //'
10.0.0.0/8
168.63.129.16/32
169.254.0.0/16
172.16.0.0/12
192.168.0.0/16
fc00::/7
fe80::/10
oifname "lo" ip daddr 127.0.0.53 udp dport 53 accept
oifname "lo" ip daddr 127.0.0.53 tcp dport 53 accept
oifname "lo" ct state new reject
So a callback URL, a remote $ref or a proxy upstream that points at the metadata service, at a private address or at this instance's own ports never connects, while public addresses work.
Step 13: Run the self-test
The image ships a self-test that checks the listening ports, every refusal of the gate (including 25 wrong keys followed by the right one), the mock round trip against the spec example, the 422 contract check, the spec's bearer scheme through the gate, the request policy and the certificate:
sudo /usr/local/sbin/prism-selftest
PRISM_SELFTEST_OK
Step 14: Rotate the API key
The helper generates a new key, puts it in force at once (the old one stops working) and writes it to the credentials file. It never prints the key:
sudo prism-rotate-key
prism-rotate-key: a new API key is in force; read it with: sudo grep PRISM_API_KEY= /var/lib/cloudimg/prism-credentials.txt
Step 15: Security notes
The certificate generated at first boot is a server certificate only: it cannot sign other certificates.
openssl x509 -in /etc/ssl/prism/prism.crt -noout -ext basicConstraints,keyUsage,extendedKeyUsage
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication
Known advisories with no fix upstream. Prism reads Postman collections through the postman-collection library, which pins two older dependencies in every release, including the newest. Neither is reachable in this image: Faker 5.5.3's advisory concerns its fake() template function, which nothing in the image calls (Prism's own Faker is 10.6.0, which is fixed), and uuid 8.3.2's concerns its v3, v5 and v6 functions called with a buffer, while the only callers use v4(). The build checks both conditions and fails if either changes; the details are in the build record:
grep -oE '^ (@faker-js/faker|uuid) [0-9.]+ +GHSA-[a-z0-9-]+' /usr/share/doc/cloudimg-stoplight-prism/RESIDUALS
@faker-js/faker 5.5.3 GHSA-qxc2-j82w-r537
uuid 8.3.2 GHSA-w5hq-g745-h8pq
The image overrides lodash to 4.18.1 to close three lodash advisories in the same library. Prism serves no browser pages, so it bundles no DOMPurify. No package install script runs during the build; that also skips the install-time analytics ping of the @scarf/scarf dependency, and Prism makes no telemetry calls of its own.
Step 16: Keep it up to date
Ubuntu security updates install automatically. To apply everything now:
sudo apt-get update && sudo apt-get upgrade -y
New Prism releases arrive as new versions of the AWS Marketplace listing: launch the new version and copy your specs from /etc/prism/spec/ across.
Support
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
The other versions on this listing are on the developer and API tools guide.