Apache Iggy 0.9.0 on AWS User Guide
Overview
This guide covers launching and using the Apache Iggy 0.9.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg streaming and messaging listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Apache Iggy is a persistent message streaming platform written in Rust. Producers append messages to topics, which are grouped into streams and split into partitions; consumers read them back by offset, timestamp or consumer group. Iggy keeps every message on disk in an append-only log and serves it with very low latency over its binary protocol, with client SDKs for Rust, Python, Go, Java, C#, Node.js and C++, plus an HTTP REST API and a web console.
What the cloudimg image adds:
- No default login. The
iggyroot password is generated on your instance at first boot and written to/root/apache-iggy-credentials.txt(root only). The upstream development loginiggy/iggyis never set, and the server will not start at all until your instance's own password exists. - TLS everywhere. The web console and the REST API are served by nginx on port 443 with a certificate generated on your instance; port 80 only redirects to HTTPS. SDK clients connect to port 8090, Iggy's own TLS listener, with the same certificate. Plaintext TCP, QUIC and the WebSocket transport are switched off.
- Fail closed. Before every start a check refuses to run the server without your instance's root password, with a short or well-known password, with plaintext TCP, QUIC, the WebSocket transport or open CORS switched on, or with a certificate that is not a server certificate. nginx opens 80 and 443 only after the first boot has proven that your password works and
iggy/iggyis refused. - A locked-down container. Iggy runs as the official
apache/iggy:0.9.0image, pinned by its digest, under systemd: as an unprivileged user, without Linux capabilities, with a read-only root filesystem, and under Docker's default seccomp profile plus only the three io_uring system calls the server needs. Its HTTP port is published on the loopback address only. - Hardened defaults. The container and every host service except the one that reads the instance's name are blocked from the instance metadata service, the AMI requires IMDSv2, nginx hands Iggy the configured host name and the real client address (never values a client sends), requests carrying another site's
Originare refused, logins are rate limited per client address, and the console's session cookie isSecureandSameSite=Strict. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Apache Iggy 0.9.0 (the official Apache image apache/iggy:0.9.0, with the server, its embedded web console and the iggy command line client), Docker Engine (Docker CE), nginx 1.24, cloud-init and the AWS Systems Manager agent.
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 with a route to the internet (or a private route you use for management).
- A security group that allows TCP 22 (SSH) from your admin network, TCP 443 (the web console and REST API) from your admin network and the hosts that call the API, and TCP 8090 (the SDK transport) from the networks your applications run in, plus TCP 80 if you want plain HTTP requests redirected to HTTPS. Never open 3000: the HTTP API listens on the loopback address only.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Apache Iggy 0.9.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above. Iggy keeps its message log on the root volume: give the instance a root volume sized for the messages you expect to retain (Step 13).
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 \
--block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=50,VolumeType=gp3}' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=iggy}]'
First boot takes about a minute after the instance is running: the boot services name the instance, create its certificate and root password, start Iggy, prove the login, and then open nginx.
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Apache Iggy 0.9.0 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-dns-name>
Step 3: Check the services
systemctl is-active apache-iggy-hostname apache-iggy-firstboot iggy-server apache-iggy-postboot nginx
sudo test -f /var/lib/cloudimg/apache-iggy-firstboot.done && echo FIRSTBOOT_DONE
All five units are active and the first boot marker is present:
active
active
active
active
active
FIRSTBOOT_DONE
Only SSH, nginx and the TLS SDK port listen on network interfaces; the HTTP API 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"))
0.0.0.0:8090 users:(("docker-proxy"))
127.0.0.1:3000 users:(("docker-proxy"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
[::]:8090 users:(("docker-proxy"))
Step 4: Get your URLs, password and certificate fingerprint
sudo cat /root/apache-iggy-credentials.txt
The file is readable by root only:
# Apache Iggy 0.9.0 - generated on this instance's first boot (unique to this instance).
# Keep this file safe. The root user can do everything; create least-privilege users for your applications.
IGGY_WEB_CONSOLE_URL=https://<public-dns-name>/ui/
IGGY_HTTP_API_URL=https://<public-dns-name>/
IGGY_TCP_TLS_ADDRESS=<public-dns-name>:8090
IGGY_TLS_DOMAIN=<public-dns-name>
IGGY_TLS_CERT_SHA256=<the certificate's SHA-256 fingerprint>
IGGY_ROOT_USERNAME=iggy
IGGY_ROOT_PASSWORD=<IGGY_ROOT_PASSWORD>
#
# The certificate is this instance's own self-signed END-ENTITY certificate (/etc/iggy/tls/iggy.crt). Compare its
# SHA-256 fingerprint above with what your browser or client shows, or replace it with a CA-issued certificate
# (see the guide). SDK clients connect to IGGY_TCP_TLS_ADDRESS with TLS on, IGGY_TLS_DOMAIN as the TLS domain and a
# copy of /etc/iggy/tls/iggy.crt as the CA file. Plaintext TCP, QUIC and WebSocket transports are off.
# On-instance CLI (prompts for the password): sudo iggy -u iggy stream list
The name is the instance's public DNS name from the EC2 instance metadata (its public IP address, or the private address, when it has none). The certificate also names the public and private IP addresses. If you stop and start the instance without an Elastic IP, the boot service picks up the new name and issues a new certificate for it. To serve Iggy under your own DNS name or with your own certificate, see Step 11.
The certificate is a self-signed server certificate created on your instance. It cannot sign other certificates, so do not add it to your operating system's or browser's trust store; instead compare its fingerprint with the one in the credentials file:
openssl x509 -in /etc/iggy/tls/iggy.crt -noout -fingerprint -sha256
Step 5: The web console
Open https://<public-dns-name>/ui/ in a browser. The certificate is self signed, so the browser warns the first time: open the certificate details, check that its SHA-256 fingerprint matches IGGY_TLS_CERT_SHA256, and proceed (or install a certificate from your CA first, Step 11). Sign in as iggy with the password from the credentials file.

The overview shows the server's resource use and totals: streams, topics, partitions, segments, messages, connected clients and consumer groups:

The streams view lists every stream. Select one to see its topics with their message counts, partitions and message expiry, create topics, and open a topic to see its partitions:

Open a partition to browse its messages. The console shows each payload base64 encoded, with its offset, timestamp and checksum:

Step 6: Streams, topics and messages from the instance
The iggy command runs the command line client from the same pinned Iggy image, as a throwaway unprivileged container next to the server, and talks to the local HTTP API. Without -p it reads the password from the terminal (or, as here, from standard input), so the password never appears on a command line. Create a stream, a topic with one partition whose messages expire after seven days, send three messages and read them back:
P() { sudo sed -n 's/^IGGY_ROOT_PASSWORD=//p' /root/apache-iggy-credentials.txt; }
P | sudo iggy -u iggy stream create orders 2>&1 | grep -v keyring
P | sudo iggy -u iggy topic create orders events 1 none 7days 2>&1 | grep -v keyring
P | sudo iggy -u iggy message send orders events "order-1001 created" "order-1002 created" "order-1003 shipped" 2>&1 | grep -v keyring
P | sudo iggy -u iggy message poll --first --message-count 10 orders events 0 2>&1 | grep -v keyring
Executing create stream with name: orders and ID auto incremented
Stream with name: orders and ID auto incremented created
Executing create topic with name: events, message expiry: 7days, compression algorithm: none, max topic size: server_default in stream with ID: orders
Topic with name: events, partitions count: 1, compression algorithm: none, message expiry: 7days, max topic size: server_default created in stream with ID: orders
Executing send messages to topic with ID: events and stream with ID: orders
Sent messages to topic with ID: events and stream with ID: orders
Executing poll messages from topic ID: events and stream with ID: orders
Polled messages from topic with ID: events and stream with ID: orders (from partition with ID: 0)
Polled 3 messages of total size 246 B, it took <time>
+--------+----------------------------+-----------------------------------------+--------+--------------------+
| Offset | Timestamp | ID | Length | Payload |
+=============================================================================================================+
| 0 | <timestamp> | <message id> | 18 | order-1001 created |
|--------+----------------------------+-----------------------------------------+--------+--------------------|
| 1 | <timestamp> | <message id> | 18 | order-1002 created |
|--------+----------------------------+-----------------------------------------+--------+--------------------|
| 2 | <timestamp> | <message id> | 18 | order-1003 shipped |
+--------+----------------------------+-----------------------------------------+--------+--------------------+
The client prints two keyring backend unavailable warnings on every run (it has no desktop keyring to keep a session in); they are harmless, and the grep -v keyring above hides them. In an interactive shell you can simply run sudo iggy -u iggy stream list and type the password at the prompt. sudo iggy --help lists every command (streams, topics, partitions, consumer groups, users, personal access tokens and more).
Step 7: The HTTP REST API
Every console action goes through Iggy's REST API, served at https://<public-dns-name>/. Log in with POST /users/login to get a bearer token (valid for one hour), then send it in the Authorization header. The API never reads credentials from cookies. Keep the password and the token in files readable only by you, never on the command line. On the instance (the --connect-to option sends the request to nginx on this host while the certificate and host name are still checked):
H=$(sudo sed -n 's/^IGGY_TLS_DOMAIN=//p' /root/apache-iggy-credentials.txt)
umask 077; W=$(mktemp -d)
sudo sed -n 's/^IGGY_ROOT_PASSWORD=//p' /root/apache-iggy-credentials.txt | { read -r P; printf '{"username":"iggy","password":"%s"}' "$P" > "$W/login.json"; }
curl -s --cacert /etc/iggy/tls/iggy.crt --connect-to "$H:443:127.0.0.1:443" -H 'Content-Type: application/json' \
--data @"$W/login.json" "https://$H/users/login" | jq -r .access_token.token | { read -r T; printf 'header = "Authorization: Bearer %s"\n' "$T" > "$W/auth"; }
curl -s --cacert /etc/iggy/tls/iggy.crt --connect-to "$H:443:127.0.0.1:443" -K "$W/auth" "https://$H/stats" | jq -c '{iggy_server_version, streams_count, messages_count}'
curl -s --cacert /etc/iggy/tls/iggy.crt --connect-to "$H:443:127.0.0.1:443" -K "$W/auth" "https://$H/streams" | jq -c '.[] | {id, name, topics_count, messages_count}'
rm -rf "$W"
{"iggy_server_version":"0.9.0","streams_count":1,"messages_count":3}
{"id":0,"name":"orders","topics_count":1,"messages_count":3}
From another machine, copy the certificate first (Step 8) and use curl --cacert iggy.crt https://<public-dns-name>/.... A wrong password gets HTTP 400 with "code":"invalid_credentials", a request without a token HTTP 401 with "code":"access_token_missing", and a browser request from another site HTTP 403. nginx allows 10 logins per minute per client address (bursts of 5), so a script cannot be used to guess passwords.
Step 8: Connect your applications over TLS
Applications use an Iggy SDK over the TCP transport on port 8090, which accepts TLS only. The client checks the server certificate, so give it a copy of the instance's certificate as its CA file (this trusts this one certificate for this one connection; do not install it in a system trust store). Copy it to the client machine and compare the fingerprint with IGGY_TLS_CERT_SHA256 from the credentials file:
scp -i <your-key.pem> ubuntu@<public-dns-name>:/etc/iggy/tls/iggy.crt iggy.crt
openssl x509 -in iggy.crt -noout -fingerprint -sha256
Connection settings for any SDK: server address <public-dns-name>:8090, TLS on, TLS domain <public-dns-name> (the IGGY_TLS_DOMAIN value, which the certificate names), CA file iggy.crt, and a user and password (the root user, or better a user you create in Step 9). A Python client with the official SDK (pip install apache-iggy==0.9.0):
import asyncio, os
from apache_iggy import IggyClient, TcpConfig, SendMessage, PollingStrategy, Consumer
HOST = "<public-dns-name>"
async def main():
client = IggyClient(TcpConfig(server_address=f"{HOST}:8090", tls_enabled=True,
tls_domain=HOST, tls_ca_file="iggy.crt"))
await client.connect()
await client.login_user("orders-app", os.environ["IGGY_PASSWORD"])
await client.send_messages("orders", "events", 0, [SendMessage("order-1004 created")])
msgs = await client.poll_messages("orders", "events", consumer=Consumer.Single("billing"),
polling_strategy=PollingStrategy.Next(), count=10, auto_commit=True)
for m in msgs:
print(m.offset(), bytes(m.payload()).decode())
asyncio.run(main())
The Rust, Go, Java, C# and Node.js SDKs have the same three TLS settings (TLS enabled, TLS domain, CA file) in their TCP client configuration. A client without the CA file is refused by its own certificate check, and a plaintext client is disconnected by the server. Clients inside the VPC can connect to the private IP address (the certificate names it too); set the TLS domain to whatever address you connect to, or set your own DNS name first (Step 11).
Step 9: Users, permissions and tokens
Give each application its own user with only the permissions it needs, instead of sharing the root password. Users are created through the API with a JSON body. The block below generates a random password for an orders-app user into ~/orders-app.password (readable only by you), and creates the user with permission to read the orders stream (stream id 0) and to send and poll its messages, and nothing else:
H=$(sudo sed -n 's/^IGGY_TLS_DOMAIN=//p' /root/apache-iggy-credentials.txt)
C=(curl -s --cacert /etc/iggy/tls/iggy.crt --connect-to "$H:443:127.0.0.1:443")
umask 077; W=$(mktemp -d)
sudo sed -n 's/^IGGY_ROOT_PASSWORD=//p' /root/apache-iggy-credentials.txt | { read -r P; printf '{"username":"iggy","password":"%s"}' "$P" > "$W/login.json"; }
"${C[@]}" -H 'Content-Type: application/json' --data @"$W/login.json" "https://$H/users/login" | jq -r .access_token.token | { read -r T; printf 'header = "Authorization: Bearer %s"\n' "$T" > "$W/auth"; }
openssl rand -base64 32 | tr -dc 'A-Za-z0-9' > ~/orders-app.password
jq -n --rawfile p ~/orders-app.password '{username: "orders-app", password: ($p | rtrimstr("\n")), status: "active", permissions: {
global: {manage_servers: false, read_servers: false, manage_users: false, read_users: false, manage_streams: false,
read_streams: false, manage_topics: false, read_topics: false, poll_messages: false, send_messages: false},
streams: {"0": {manage_stream: false, read_stream: true, manage_topics: false, read_topics: true, poll_messages: true, send_messages: true}}}}' > "$W/user.json"
"${C[@]}" -K "$W/auth" -H 'Content-Type: application/json' --data @"$W/user.json" -o /dev/null -w 'create user: HTTP %{http_code}\n' "https://$H/users"
"${C[@]}" -K "$W/auth" "https://$H/users" | jq -c '.[] | {id, username, status}'
rm -rf "$W"
create user: HTTP 200
{"id":0,"username":"iggy","status":"active"}
{"id":1,"username":"orders-app","status":"active"}
The orders-app user can now read the orders stream and send and poll its messages, but gets HTTP 403 ("code":"unauthorized") if it tries to create a stream. Copy ~/orders-app.password to the application's secret store and delete it from the instance. Users can also create personal access tokens (POST /personal-access-tokens with a name and an expiry) for services that should not hold a password; a token is shown once and can be revoked at any time. See the Iggy documentation for the full permission model.
Step 10: Changing the root password
Change the root password with the helper below. It sets a new random password through the API, then updates /etc/iggy/secrets/root.env (which the server reads at startup) and the credentials file to match:
sudo apache-iggy-rotate-root-password
apache-iggy-rotate-root-password: done (new password in /root/apache-iggy-credentials.txt)
If you change the root password in another way (an SDK or the API), the server keeps the new one, but the credentials file and root.env still hold the old value; running the helper afterwards needs the current password in the credentials file.
Step 11: Your own DNS name and certificate
Point a DNS record at the instance, then tell Iggy to serve that name. This issues a new self-signed certificate that names it (used by both nginx and the SDK port), updates nginx and the allowed browser origin, sets the address Iggy advertises to clients, updates the credentials file and restarts the server:
sudo apache-iggy-set-hostname iggy.example.com
The name is saved in /etc/cloudimg/apache-iggy-hostname and survives reboots and address changes.
To use a certificate from your own CA or from Let's Encrypt for both the console and the SDK port, copy the certificate chain (the server certificate first, then any intermediates) and its private key to the instance and run:
sudo apache-iggy-install-cert fullchain.pem privkey.pem iggy.example.com
The helper checks that the certificate is a server certificate for that name, matches the key and is not about to expire, installs both for nginx and Iggy, and restarts them. The boot-time renewal leaves an installed certificate alone; clients then verify it with their normal CA certificates and need no CA file. Running apache-iggy-set-hostname again goes back to a self-signed certificate. Renew a CA certificate by running apache-iggy-install-cert again with the new files.
Step 12: Security checks
apache-iggy-selfcheck re-proves the appliance with this instance's credentials. Every request goes through nginx over HTTPS and is verified against the instance's certificate, as a client would see it:
sudo apache-iggy-selfcheck
SELFCHECK OK root login over verified HTTPS, iggy/iggy + wrong password + anonymous + cross-origin refused, TCP 8090 is TLS for <public-dns-name>
The metadata block is a firewall rule for the container (DOCKER-USER chain) plus a systemd IPAddressDeny rule on each host service; you can see both:
sudo iptables -S DOCKER-USER
for u in iggy-server nginx apache-iggy-firstboot apache-iggy-postboot docker; do printf '%s: ' $u; systemctl show -p IPAddressDeny --value $u.service; done
-N DOCKER-USER
-A DOCKER-USER -d 168.63.129.16/32 -j DROP
-A DOCKER-USER -d 169.254.169.254/32 -j DROP
iggy-server: 168.63.129.16/32 169.254.169.254/32
nginx: 168.63.129.16/32 169.254.169.254/32
apache-iggy-firstboot: 168.63.129.16/32 169.254.169.254/32
apache-iggy-postboot: 168.63.129.16/32 169.254.169.254/32
docker: 168.63.129.16/32 169.254.169.254/32
Only apache-iggy-hostname, which reads the instance's name and addresses at boot, can reach the metadata service. Because of the block, Iggy (and connectors you run in containers on this host) cannot pick up the instance's IAM role credentials.
Step 13: Versions, provenance and file locations
sed -n '1,3p' /usr/share/doc/apache-iggy/BUILD-PROVENANCE.txt
sudo docker ps --format '{{.Names}} {{.Image}} {{.Status}}'
Apache Iggy 0.9.0 (cloudimg AWS image)
Server image: apache/iggy@sha256:b35fc2841964f2962a5f164d33b3ae3a9de99a66920defd3cd8a1c039470602d (Docker Hub apache/iggy:0.9.0, published 2026-09-18 with the server-0.9.0 release)
Licence: Apache-2.0. LICENSE sha256 f4b9e8594f90b3730c8c856887c43469eae52e397de0c66871211c6ef577c10d = LICENSE in iggy-0.9.0-src.tar.gz
iggy-server b35fc2841964 Up <time>
| Path | What it is |
|---|---|
/var/lib/iggy |
Iggy's data: streams, topics, partitions, segments, the metadata journal (users, permissions) |
/etc/iggy/server.env |
the server settings (Iggy environment variables); /etc/iggy/node.env holds the advertised address |
/etc/iggy/secrets/root.env |
the root user name and password the server reads at startup (root and the server only) |
/etc/iggy/tls/iggy.crt, iggy.key |
the certificate (public) and its private key, used by nginx and by Iggy on 8090 |
/etc/iggy/seccomp-iggy.json |
the container's seccomp profile (Docker's default plus io_uring) |
/root/apache-iggy-credentials.txt |
URLs, root password and certificate fingerprint (root only) |
/etc/nginx/sites-available/cloudimg-iggy |
the HTTPS front door for the console and the REST API |
/usr/share/doc/apache-iggy/ |
LICENSE, NOTICE, LICENSE-binary and BUILD-PROVENANCE.txt |
Server logs go to the journal: journalctl -u iggy-server. To change a setting, edit /etc/iggy/server.env and run sudo systemctl restart iggy-server; the start check refuses settings that would switch off TLS on 8090 or turn on QUIC, the WebSocket transport or CORS.
Step 14: Backup and maintenance
- Back up
/var/lib/iggywith the server stopped (sudo systemctl stop iggy-server), or take EBS snapshots of the root volume. Also keep/etc/iggy,/etc/cloudimgand the credentials file. - Retention is set per topic: give topics a message expiry or a maximum size (as in Step 6) so the log does not fill the root volume. Grow the volume later with an EBS volume modification and
sudo growpart /dev/nvme0n1 1 && sudo resize2fs /dev/nvme0n1p1. - Iggy runs one worker per vCPU. An m5.large is a good start; choose more vCPUs and memory for higher throughput.
- Ubuntu and Docker security updates install automatically (unattended-upgrades). Iggy itself is the pinned image; to upgrade, launch the newer version from the listing and move your data across.
Step 15: Security recommendations
- Limit TCP 8090 in the security group to the networks your applications run in, and TCP 443 and 22 to your admin network. The console and the root user can delete streams and messages; treat them as administrative access.
- Give each application its own user with only the permissions it needs (Step 9), and rotate the root password if you have shared the credentials file (Step 10).
- Serve Iggy under your own DNS name with a certificate from a trusted CA (Step 11), and keep TLS verification on in every client.
Support and licensing
cloudimg provides 24/7 technical support for this image by email and live chat: support@cloudimg.co.uk. Apache Iggy is licensed under the Apache License 2.0 (its LICENSE, NOTICE and the licences of the components in the image are on the instance in /usr/share/doc/apache-iggy); Docker Engine and nginx are open source software under the Apache License 2.0 and the BSD 2-Clause licence. Apache Iggy, Iggy, Apache and the Apache feather logo are trademarks of the Apache Software Foundation; cloudimg is not affiliated with or endorsed by the Apache Software Foundation.