Apache Camel Karavan 4.22.1 on AWS User Guide
Overview
This guide covers launching and verifying Apache Camel Karavan 4.22.1 on Ubuntu 24.04 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.
Apache Camel Karavan is a low-code integration designer for Apache Camel. In a browser you draw routes from Camel's 300+ components and Enterprise Integration Patterns (timers, HTTP and REST, Kafka, JMS, files, databases, transformations, splitters, aggregators and more), edit REST APIs and OpenAPI definitions, keep projects in git with commit history, and run any integration with one click in developer mode, which starts it as its own container and streams its log back to the UI.
What is on the image:
- Apache Camel Karavan 4.22.1 (ASF release) on an Eclipse Temurin 25 runtime image built by cloudimg, matching upstream fix 39054f656 for issue #1653. The official
ghcr.io/apache/camel-karavan:4.22.1image does not start (it ships a Java 21 base for an application compiled for Java 25). The image here runs the unmodified ASFkaravan.jar(sha256b1bb9f35bf541f7c595ce6878122788f5d38d7f48ecd425295c3cab70b5d6784) oneclipse-temurin:25-jre, with jackson-core and jackson-databind updated to the 2.21.7 security releases. - The Karavan developer-mode runtime (
camel-karavan-devmode:4.22.1, Camel 4.22.1) with FreeMarker 2.3.35, Jolokia 2.6.2, Jackson 2.22.3 and the Red Hat UBIpcre2andopensslsecurity updates applied. Each integration you run starts from this image. - PostgreSQL 18.6 (
postgres:18.6-alpine, pinned by digest) for Karavan's projects and sessions, created empty on your instance's first boot, and a local git repository for your projects - A rootless Docker engine that runs as the unprivileged
karavanaccount. The root Docker daemon is disabled on this image. - nginx as the only network-facing service: HTTPS on port 443 with a self-signed certificate made on first boot, port 80 redirecting to HTTPS
- Per-instance passwords for both built-in Karavan accounts, generated on first boot into a root-only file
karavan-set-hostname, a helper that serves Karavan under your own DNS name and re-issues the certificate for it
The Dockerfiles of both cloudimg-built images ship on the instance in /usr/share/doc/cloudimg-karavan/.
Default user: ubuntu · Architecture: x86_64 · Root volume: 20 GiB gp3
Security posture
- No default login. Upstream Karavan seeds two accounts,
adminanddeveloper, with one published default password. On this image the admin password is generated on each instance's first boot, thedeveloperaccount gets a different password of its own, and the PostgreSQL password is a third random value. None is in the image. A guard refuses to start Karavan with a short, world-readable or published-default password. - Two locks on the door. nginx asks for a per-instance username and password (an HTTP access gate) before any request reaches Karavan, except a static
/healthz. Karavan then asks for its own sign-in. Karavan's API, its/publicand/platform/internalendpoints and path tricks such as;,%3Band%2Fall stop at the gate with401. - The Docker API Karavan holds is not root. Karavan needs a Docker API to run integrations. On this image that API belongs to a rootless Docker engine running as the unprivileged
karavanaccount, with no password, no login shell and no sudo rights. A Karavan sign-in, or any flaw in Karavan, gives at most that account's privileges. At build time even a privileged container with the whole root filesystem mounted and host networking could not read/etc/shadow, the credentials file, the TLS key or the gate passwords, could not write to/etcor/root, did not see the instance's network and could not reach the instance metadata service. - Integrations cannot read instance metadata. Integrations call URLs you configure, so every path out of the engine is blocked from 169.254.169.254, 168.63.129.16 and fd00:ec2::254 by two independent walls: systemd
IPAddressDenyon the engine's service (it holds the engine and every container process) and an iptables owner-match rule for thekaravanaccount. Each build proves it from inside a running integration, and removes each wall in turn to show that the other one still holds. - Integrations stay private. Integration containers join Karavan's own network (never the host network), get no Docker socket, and publish their ports on
127.0.0.1only. PostgreSQL sits on an internal network that integrations cannot reach and has no host port. - Only nginx faces the network. Karavan listens on
127.0.0.1:8080. nginx answers only for the configured hostname (any otherHostheader gets its connection closed), uses TLS 1.2 or 1.3, refuses cross-originPOSTrequests and WebSocket upgrades (403), strips CORS headers, rate-limits sign-in, keeps the session cookieSecure,HttpOnlyandSameSite=Lax, replaces any client-suppliedX-Forwarded-For,X-Real-IPorForwardedheader with the real peer address, and never passes the gate password on to Karavan. - The hostname comes from configuration, never from a request. First boot reads it from EC2 instance metadata (IMDSv2) and nginx hands Karavan that fixed name.
- Least-privilege containers. Karavan runs with a read-only root filesystem, every Linux capability dropped and
no-new-privileges. PostgreSQL runs as its own unprivileged user with every capability dropped. - No secret in a container environment. Karavan reads its passwords from a mounted file (
/opt/karavan/secrets/karavan-secrets.properties) and PostgreSQL fromPOSTGRES_PASSWORD_FILE, sodocker inspectnever shows them. - IMDSv2 required (the AMI is registered with
ImdsSupport v2.0), and per-instance identity: SSH host keys, the machine ID, the TLS certificate, the Docker engine ID and the git repository are created fresh on every instance.
Known library advisories. Each build scans the shipped images. These remain, have no fix in the same release line, and are not reachable on this image (the build asserts each condition): Netty's SNI routing bypass (CVE-2026-75595) needs Karavan to terminate TLS, and it does not (nginx does); Apache HttpComponents Core 5.3.6 (CVE-2026-54399, CVE-2026-54428) is used only by Karavan's Docker client on the local Unix socket; the Go runtime CVEs in PostgreSQL's gosu helper do not apply because PostgreSQL starts as its own user and gosu never runs. The developer-mode runtime is based on Red Hat UBI 9.8, which has open advisories with no released fix yet (expat, gnutls, python3, util-linux, X11 libraries, maven-resolver). Camel integrations do not use them, and they are updated when Red Hat ships fixes. Do not build FreeMarker templates from untrusted input.
Prerequisites
- An AWS account and permission to launch EC2 instances
- An EC2 key pair in the Region you launch in
- Outbound internet access from the instance: Camel downloads the components a route uses from Maven Central the first time it runs
Recommended instance type: m5.large. Karavan (a JVM capped at a 1 GiB heap), PostgreSQL and one or two running integrations (each its own JVM, about 1.5 GiB) fit in 8 GiB of memory. Choose m5.xlarge if you run several integrations at the same time.
Connecting to your instance
| Version | Operating system | SSH user |
|---|---|---|
| Apache Camel Karavan 4.22.1 on Ubuntu 24.04 | Ubuntu 24.04 LTS | ubuntu |
Step 1: Launch the instance
Option A: AWS Marketplace console
- Open the cloudimg Hardened Node.js & Developer and API Tools AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once the subscription is active, choose Launch your software.
- Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose Apache Camel Karavan 4.22.1 on Ubuntu 24.04, then choose your Region.
- Pick an instance type (
m5.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - Security group: the listing's default group opens SSH (22), HTTP (80) and HTTPS (443). Restrict all three to your own and your team's addresses.
- Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.
Option B: AWS CLI
Run this on your own workstation. It creates a security group that allows SSH from your management address and HTTPS from your team's network, then launches the instance. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDR ranges with your own:
SG=$(aws ec2 create-security-group --group-name karavan \
--description "Apache Camel Karavan" --vpc-id vpc-0123456789abcdef0 \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr 203.0.113.0/24
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 80 --cidr 203.0.113.0/24
aws ec2 run-instances \
--image-id ami-0123456789abcdef0 \
--instance-type m5.large \
--key-name my-key-pair \
--security-group-ids "$SG" \
--subnet-id subnet-0123456789abcdef0 \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=karavan}]'
You do not need --metadata-options: the AMI already makes IMDSv2 required. If you want the address to survive a stop and start, associate an Elastic IP.
Step 2: Connect and find your login
Connect as ubuntu with the key pair you chose:
ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>
First boot finishes within two or three minutes of launch. It sets the hostname Karavan is served as (the instance's public DNS name when it has one), creates the TLS certificate, generates the passwords, creates the local git repository, starts PostgreSQL and Karavan, proves both accounts sign in with their own passwords, loads a demo integration and writes /root/karavan-credentials.txt (mode 0600, root). Only then does it open nginx. These are the entries in that file (the passwords are not shown here):
sudo grep -E '^KARAVAN_(URL|USERNAME|DEVELOPER_USERNAME)=' /root/karavan-credentials.txt
sudo grep -oE '^[A-Z_]+=' /root/karavan-credentials.txt
sudo stat -c '%a %U:%G' /root/karavan-credentials.txt
KARAVAN_URL=https://ec2-3-95-166-62.compute-1.amazonaws.com/
KARAVAN_USERNAME=admin
KARAVAN_DEVELOPER_USERNAME=developer
KARAVAN_URL=
KARAVAN_USERNAME=
KARAVAN_PASSWORD=
KARAVAN_DEVELOPER_USERNAME=
KARAVAN_DEVELOPER_PASSWORD=
600 root:root
| Value | What it is for |
|---|---|
KARAVAN_URL |
The address of this instance's Karavan |
KARAVAN_USERNAME / KARAVAN_PASSWORD |
The admin account (all roles), generated on this instance. It opens both the access gate and Karavan's sign-in. |
KARAVAN_DEVELOPER_USERNAME / KARAVAN_DEVELOPER_PASSWORD |
The built-in developer account (developer role) with its own password, for both the gate and Karavan |
To read the values, run sudo cat /root/karavan-credentials.txt in your own session and store them in your password manager.
Step 3: Confirm Karavan is running
The metadata wall, the rootless engine, the first-boot units, Karavan and nginx are active. Only sshd and nginx listen beyond loopback: Karavan is on 127.0.0.1:8080, integrations you run publish on 127.0.0.1 too, and PostgreSQL's port exists only inside its internal container network. The engine belongs to the karavan account, so its containers are listed with sudo -u karavan:
systemctl is-active cloudimg-karavan-imds-deny.service karavan-docker.service karavan-firstboot.service karavan.service karavan-postboot.service nginx.service
kd() { (cd / && sudo -u karavan env HOME=/var/lib/karavan DOCKER_HOST=unix:///run/karavan-docker/docker.sock docker "$@"); }
kd ps --format '{{.Names}} {{.Image}} {{.Ports}}'
sudo ss -Htlnp | awk '{print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u
curl -sk https://127.0.0.1/healthz
active
active
active
active
active
active
karavan cloudimg/camel-karavan:4.22.1-temurin25 127.0.0.1:8080->8080/tcp
karavan-postgres postgres:18.6-alpine 5432/tcp
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:(("rootlesskit"))
127.0.0.53%lo:53 users:(("systemd-resolve"))
127.0.0.54:53 users:(("systemd-resolve"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
ok
/healthz is answered by nginx itself and reveals nothing, so it is safe to use as a load balancer health check.
Step 4: Prove the gate, the sign-in and the web defaults
Signed-out requests stop at the access gate, including the path variants that bypass path-based authorization in some web frameworks. The gate alone gives no access to Karavan's API: Karavan's own session is needed too. A cross-origin POST is refused, an unknown Host header gets no answer, and plain HTTP redirects to the configured HTTPS name. The last line signs in to Karavan with this instance's admin password and reads its version:
H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/karavan-host.conf)
P=$(sudo grep '^KARAVAN_PASSWORD=' /root/karavan-credentials.txt | cut -d= -f2-)
R=(--resolve "$H:443:127.0.0.1")
for path in / "/ui/project" "/ui/project;x" "/ui/project%3Bx" "/ui%2Fproject"; do
curl -sk "${R[@]}" --path-as-is -o /dev/null -w "anonymous $path -> %{http_code}\n" "https://$H$path"
done
curl -sk "${R[@]}" -o /dev/null -w 'gate, wrong password -> %{http_code}\n' -K <(printf 'user = "admin:not-the-password"\n') "https://$H/"
curl -sk "${R[@]}" -o /dev/null -w 'gate, your password -> %{http_code}\n' -K <(printf 'user = "admin:%s"\n' "$P") "https://$H/"
curl -sk "${R[@]}" -o /dev/null -w 'API without a Karavan session -> %{http_code}\n' -K <(printf 'user = "admin:%s"\n' "$P") "https://$H/ui/project"
curl -sk "${R[@]}" -o /dev/null -w 'cross-origin POST -> %{http_code}\n' -K <(printf 'user = "admin:%s"\n' "$P") -X POST \
-H 'Origin: https://evil.example' -H 'Content-Type: application/json' -d '{}' "https://$H/ui/auth/login"
curl -sk -o /dev/null -w 'unknown Host -> %{http_code}\n' -H 'Host: evil.example' https://127.0.0.1/ || true
curl -s -o /dev/null -w 'http -> %{http_code} %{redirect_url}\n' -H "Host: $H" http://127.0.0.1/
J=$(mktemp); curl -sk "${R[@]}" -o /dev/null -c "$J" -K <(printf 'user = "admin:%s"\n' "$P") -H "Origin: https://$H" \
-H 'Content-Type: application/json' --data-binary @<(printf '{"username":"admin","password":"%s"}' "$P") "https://$H/ui/auth/login"
curl -sk "${R[@]}" -b "$J" -K <(printf 'user = "admin:%s"\n' "$P") "https://$H/ui/configuration" | jq -c '{title, version, infrastructure}'
rm -f "$J"
anonymous / -> 401
anonymous /ui/project -> 401
anonymous /ui/project;x -> 401
anonymous /ui/project%3Bx -> 401
anonymous /ui%2Fproject -> 401
gate, wrong password -> 401
gate, your password -> 200
API without a Karavan session -> 401
cross-origin POST -> 403
unknown Host -> 000
http -> 301 https://ec2-3-95-166-62.compute-1.amazonaws.com/
{"title":"Apache Camel Karavan","version":"4.22.1","infrastructure":"docker"}
The password reaches curl through files on file descriptors (-K <(...) and --data-binary @<(...)), so it never appears on a command line or in your shell history.
Step 5: Sign in to Karavan
Browse to KARAVAN_URL (https://<public-dns-name>/). The certificate is self-signed, so your browser warns once; accept it for this host, or install your own certificate (Step 9). Your browser first asks for the access gate username and password: enter admin and KARAVAN_PASSWORD. Karavan's sign-in page then opens; enter the same username and password and choose Access Platform:

The Integrations page lists your projects. A new instance has one, Demo Integration (demo), with two routes, committed to the instance's local git repository:

Step 6: Design an integration
Open demo, choose Source and select heartbeat.camel.yaml. The Designer shows the route as a flow: a Timer that fires every 10 seconds, a Set Body step and a Log step. Select a step to edit its properties on the right; drag components and patterns from the palette to extend the route. Code shows the same route as Camel YAML and Diff shows your changes since the last commit.

To create your own project choose Create Integration on the Integrations page. Under Source, Commits shows the git history of the project and Push commits your changes to the local repository.
Step 7: Run an integration in developer mode
In the project, open Containers and choose Run. Karavan starts the integration as its own container from the developer-mode runtime image, on the rootless engine:

Log streams the container's output. The demo's two routes log a heartbeat every 10 seconds and a greeting every 30 seconds:

The same thing from the command line, through the access gate and Karavan's API:
H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/karavan-host.conf)
P=$(sudo grep '^KARAVAN_PASSWORD=' /root/karavan-credentials.txt | cut -d= -f2-)
R=(--resolve "$H:443:127.0.0.1"); J=$(mktemp)
G() { curl -sk "${R[@]}" -K <(printf 'user = "admin:%s"\n' "$P") -b "$J" -c "$J" -H "Origin: https://$H" "$@"; }
G -o /dev/null -H 'Content-Type: application/json' --data-binary @<(printf '{"username":"admin","password":"%s"}' "$P") "https://$H/ui/auth/login"
CSRF=$(awk '$6=="csrf"{print $7}' "$J")
G -o /dev/null -w 'run demo in developer mode -> %{http_code}\n' -H "X-CSRF-Token: $CSRF" "https://$H/ui/devmode/run/demo"
kd() { (cd / && sudo -u karavan env HOME=/var/lib/karavan DOCKER_HOST=unix:///run/karavan-docker/docker.sock docker "$@"); }
for i in $(seq 1 30); do L=$(kd logs demo 2>&1 || true); [[ "$L" == *"demo heartbeat"* ]] && break; sleep 5; done
kd ps --filter name=^demo$ --format '{{.Names}} {{.Image}} {{.Ports}}'
grep -m1 -o 'Body: cloudimg Karavan demo heartbeat' <<<"$L"
rm -f "$J"
run demo in developer mode -> 200
demo cloudimg/camel-karavan-devmode:4.22.1-cloudimg1 8443/tcp, 127.0.0.1:8081->8080/tcp
Body: cloudimg Karavan demo heartbeat
The first run of a route downloads the Camel components it uses from Maven Central, so it takes a little longer. An integration's HTTP port (here 8081) is bound to 127.0.0.1 only; to reach it from elsewhere, add an nginx location for it or use an SSH tunnel (ssh -L 8081:127.0.0.1:8081 ubuntu@<public-ip>). Docker reports developer-mode containers as unhealthy after a few minutes because Karavan 4.22.1's container health check probes an older management port; the integration keeps running and Karavan shows it as running.
Step 8: Check the security controls
The engine runs as karavan in rootless mode and the root Docker daemon is masked. From inside the running demo integration the internet is reachable but the instance metadata service is not, and both metadata walls are in place:
kd() { (cd / && sudo -u karavan env HOME=/var/lib/karavan DOCKER_HOST=unix:///run/karavan-docker/docker.sock docker "$@"); }
echo "engine runs as: $(ps -o user= -p "$(systemctl show -p MainPID --value karavan-docker.service)")"
kd info --format '{{json .SecurityOptions}}'
systemctl is-enabled docker.service docker.socket containerd.service
kd exec demo curl -s -m 10 -o /dev/null -w 'integration -> internet: %{http_code}\n' https://checkip.amazonaws.com/
kd exec demo curl -s -m 6 -o /dev/null -w 'integration -> instance metadata: %{http_code}\n' -X PUT \
-H 'X-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token || true
systemctl show -p IPAddressDeny karavan-docker.service
sudo iptables -S OUTPUT | grep -c 'uid-owner'
engine runs as: karavan
["name=seccomp,profile=builtin","name=rootless","name=cgroupns"]
masked
masked
masked
integration -> internet: 200
integration -> instance metadata: 000
IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32
4
000 means the connection never got an answer. If an integration needs AWS credentials, give it an access key through its own configuration (or a secrets service) rather than the instance role: the instance role is deliberately out of reach.
Step 9: Serve Karavan under your own DNS name
Point a DNS record at the instance (an Elastic IP keeps the address stable), then set the name. The helper re-issues the self-signed certificate for it, updates nginx and the Origin guard, and updates KARAVAN_URL:
sudo karavan-set-hostname <your-domain>
To use a certificate from a public CA, replace /etc/ssl/karavan/karavan.crt and /etc/ssl/karavan/karavan.key (key mode 0600, root) with your own and reload nginx (sudo systemctl reload nginx). Without a pinned name, the instance follows its public DNS name across a stop and start.
Step 10: Build images and deploy (optional)
Developer mode works out of the box. Karavan's Build and Deploy actions package an integration as a container image, push it to a registry and run it from there; they need a git remote and a container registry that you provide. Edit /opt/karavan/docker-compose.yml: set KARAVAN_GIT_REPOSITORY to your remote (with KARAVAN_GIT_USERNAME and KARAVAN_GIT_PASSWORD for an HTTPS remote) and add KARAVAN_CONTAINER_IMAGE_REGISTRY, KARAVAN_CONTAINER_IMAGE_REGISTRY_USERNAME and KARAVAN_CONTAINER_IMAGE_REGISTRY_PASSWORD. Then run sudo systemctl restart karavan to apply it. Keep these values out of shared files: the compose file is world-readable, so a production setup puts them in /opt/karavan/secrets/karavan-secrets.properties as karavan.git.password= and karavan.container-image.registry-password= lines instead.
Step 11: Change the passwords
In Karavan, open your profile from the account menu and use Change password for the signed-in account (as admin you can also set other accounts' passwords on the access management page). Then set the same new password for that account at the access gate (it asks for the password twice and stores only a bcrypt hash):
sudo htpasswd -B /etc/nginx/karavan.htpasswd admin # enter <new-password> twice
Update /root/karavan-credentials.txt to match. The value in /opt/karavan/secrets/karavan-secrets.properties only seeds the admin account on the first start of an empty database; changing it later does not change an existing password.
Maintenance
- Operating system: security updates install automatically (
unattended-upgrades). Docker CE comes from Docker's apt repository:sudo apt-get update && sudo apt-get install --only-upgrade docker-ce docker-ce-cli docker-ce-rootless-extras containerd.io docker-compose-plugin, thensudo systemctl restart karavan-docker karavan. - Karavan and its runtime: cloudimg publishes a new version in the listing when Karavan, its runtime or PostgreSQL needs an update. Back up first.
- Backups: your projects are in PostgreSQL (the
karavan-pgdatavolume under/var/lib/karavan/.local/share/docker/volumes/) and in the git repository at/var/lib/karavan/git/karavan.git. Snapshot the EBS volume, or stopkaravan.serviceand copy both.
Troubleshooting
- The browser keeps asking for a password: the access gate and Karavan's sign-in are separate; both take the same username and password from the credentials file.
- "Max failed attempts reached": Karavan locks an account after six wrong passwords in a row, and a correct password no longer opens it. As
adminyou can reset thedeveloperaccount's password on the access management page. To unlockadminitself, reset its counter in the database and restart Karavan:
text
kd exec -i -u 70 karavan-postgres psql -U karavan -d karavan -c \
"UPDATE access_state SET data = jsonb_set(data, '{failedAttempts}', '0') WHERE key = 'AccessPassword:dev:admin';"
sudo systemctl restart karavan
* A route fails to start on its first run: the instance must reach Maven Central (repo1.maven.org) to download Camel components. Check outbound HTTPS in your security group and network ACLs.
* Logs: journalctl -u karavan-firstboot -u karavan -u karavan-postboot -u karavan-docker, and kd logs karavan (with the kd function from Step 3).
Support
cloudimg provides support for this image by email at support@cloudimg.co.uk. Apache, Apache Camel, Camel and Karavan are trademarks of The Apache Software Foundation. cloudimg is not affiliated with, endorsed by or sponsored by The Apache Software Foundation. Apache Camel Karavan is licensed under the Apache License 2.0; Eclipse Temurin under the GNU General Public License, version 2, with the Classpath Exception. The licence texts and cloudimg's notes are on the instance in /usr/share/doc/cloudimg-karavan/.