Artificial Intelligence (AI) AWS

ContextForge MCP Gateway 1.0.11 on AWS User Guide

| Product: ContextForge MCP Gateway 1.0.11 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying ContextForge MCP Gateway 1.0.11 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened Crawl4AI & AI Agent Builders AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other AI agent products in the listing are described on the listing's guide hub.

ContextForge (IBM's open-source MCP gateway, Apache-2.0) puts one authenticated endpoint in front of many Model Context Protocol (MCP) servers, REST APIs and agent-to-agent (A2A) services. You register upstream MCP servers ("gateways"), it discovers their tools, prompts and resources into one registry, and you group tools into virtual servers that MCP clients such as Claude Desktop, Cursor or your own agents connect to over Streamable HTTP or SSE. An admin UI, a REST API, API tokens, teams and role-based access control come with it.

Version 1.0.11 is the newest 1.0 release. It carries every fix for the advisories published against earlier 1.0 releases, among them GHSA-5424-f25v-29r8 (a default JWT signing key accepted), GHSA-9qxh-fpx5-vxvw (server-side request forgery bypass) and GHSA-qmfj-jjw4-8qrq (cross-team agent takeover). No advisory is open against 1.0.11.

What is on the image:

  • ContextForge MCP Gateway 1.0.11, installed from the official PyPI release (checksum-pinned) into a Python 3.12 virtual environment at /opt/mcpgateway/venv, owned by root so the service cannot change its own code
  • A bundled reference MCP server, the official time server (mcp-server-time, MIT), in its own environment on 127.0.0.1:8002, already registered with the gateway as localtime, so a real tool call works the moment the instance boots
  • nginx in front of the gateway: HTTPS on port 443 with a self-signed certificate created on first boot
  • A per-instance admin password, JWT signing key and credential-encryption key, generated on first boot into root-only files
  • The SQLite database at /var/lib/mcpgateway/mcp.db, created on your instance's first boot (the image ships none)
  • mcpgateway-api-token and mcpgateway-set-hostname, helpers for an API token and for your own DNS name
  • The package list and the build-time dependency audit (pip-audit, 0 known vulnerabilities) in /usr/share/doc/cloudimg/mcp-context-forge/

Default user: ubuntu · Architecture: x86_64 · Root volume: 10 GiB gp3

Security posture

  • No default login. The admin password, the JWT signing key, the key that encrypts stored upstream credentials, the default password for new users and the basic-auth password are generated on each instance's first boot. None is in the image, so no two instances share one. The gateway runs in production mode, which also refuses upstream placeholder values such as changeme, and a start-up guard refuses to start it if any secret is missing, short or a known default.
  • No database in the image. The gateway creates its database, schema and admin user on first boot, after the per-instance secrets exist. The admin password is stored only as an Argon2id hash.
  • Everything needs a login. The admin UI needs the admin email and password; the REST and JSON-RPC API and the MCP endpoints need a bearer token. Public self-registration and HTTP basic auth are off. The admin UI's sign-in form carries its own CSRF token, and its session cookie is Secure and HttpOnly and is not accepted for API calls.
  • Only nginx faces the network. The gateway listens on 127.0.0.1:4444 and the time server on 127.0.0.1:8002. nginx answers only for the instance's configured hostname (any other Host header gets its connection closed), uses TLS 1.2 or 1.3, refuses cross-origin form posts and WebSocket upgrades, and replaces any client-supplied X-Forwarded-For, X-Real-IP, Forwarded or X-Forwarded-Host header, so the gateway logs the real client address and builds its URLs from the configured name. Port 80 is not used.
  • Server-side request forgery protection. A gateway fetches URLs that its users register, so the gateway's own URL checks stay on: the instance metadata service, link-local, carrier-grade NAT, the Azure wire server, private (RFC 1918) networks and unresolvable names are refused when you register an MCP server. Loopback is allowed so the bundled time server can be federated.
  • The services cannot read instance metadata. On top of that, the gateway, the time server, nginx and the first-boot units are blocked from the instance metadata service (169.254.169.254, fd00:ec2::254 and the IPv4-mapped form) by systemd IPAddressDeny. This was proved at build time from inside the gateway's own process group with its own HTTP client, while the same client outside the group could reach it.
  • Least privilege. The gateway runs as mcpgateway and the time server as mcptime, with no Linux capabilities, a read-only system and only /var/lib/mcpgateway writable.
  • IMDSv2 required. The AMI is registered with ImdsSupport v2.0, so instances refuse token-less (IMDSv1) metadata requests.
  • Per-instance identity. SSH host keys, the machine ID, the TLS certificate and the database are created fresh on every instance.

Prerequisites

  1. An AWS account and permission to launch EC2 instances
  2. An EC2 key pair in the Region you launch in

Recommended instance type: m5.large. The gateway and the time server use about 700 MiB of memory at idle, so a t3.medium (4 GiB) is enough for a small team.

Connecting to your instance

Version Operating system SSH user
ContextForge MCP Gateway 1.0.11 on Ubuntu 24.04 Ubuntu 24.04 LTS ubuntu

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. Open the cloudimg Hardened Crawl4AI & AI Agent Builders AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  2. Once the subscription is active, choose Launch your software.
  3. Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose ContextForge MCP Gateway 1.0.11 on Ubuntu 24.04, then choose your Region.
  4. Pick an instance type (m5.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  5. Security group: the listing's default group opens SSH (22) and HTTPS (443). Restrict SSH to your own IP address, and HTTPS to the networks your administrators and MCP clients connect from.
  6. Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.
Port Why it is open
22 SSH for you, as ubuntu
443 The admin UI, the REST / JSON-RPC API and the MCP endpoints your clients connect to

Option B: AWS CLI

Run this on your own workstation. It creates a security group for 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 contextforge \
  --description "ContextForge MCP Gateway" --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 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=contextforge}]'

You do not need --metadata-options: the AMI already makes IMDSv2 required. If you will use your own DNS name, associate an Elastic IP so the address survives a stop and start.

Step 2: Connect and find your admin login

Connect as ubuntu with the key pair you chose:

ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>

First boot finishes within a minute or two of launch. It sets the hostname the gateway is served as (the instance's public DNS name when it has one), creates the TLS certificate for it, generates every secret and writes /root/mcp-context-forge-credentials.txt (mode 0600, root). The gateway then creates its database and admin user, and a second first-boot step registers the bundled time server. These are the entries in the credentials file (the password is not shown here):

sudo grep -E '^MCPGATEWAY_(URL|ADMIN_UI|ADMIN_EMAIL)=' /root/mcp-context-forge-credentials.txt
sudo grep -oE '^[A-Z0-9_]+=' /root/mcp-context-forge-credentials.txt
sudo stat -c '%a %U:%G' /root/mcp-context-forge-credentials.txt
MCPGATEWAY_URL=https://ec2-3-94-54-86.compute-1.amazonaws.com/
MCPGATEWAY_ADMIN_UI=https://ec2-3-94-54-86.compute-1.amazonaws.com/admin/
MCPGATEWAY_ADMIN_EMAIL=admin@example.com
MCPGATEWAY_URL=
MCPGATEWAY_ADMIN_UI=
MCPGATEWAY_ADMIN_EMAIL=
MCPGATEWAY_ADMIN_PASSWORD=
MCPGATEWAY_CERT_SHA256=
600 root:root
Value What it is for
MCPGATEWAY_URL The base URL of this gateway (the MCP endpoints live under it)
MCPGATEWAY_ADMIN_UI The admin UI
MCPGATEWAY_ADMIN_EMAIL The platform administrator's sign-in email
MCPGATEWAY_ADMIN_PASSWORD The administrator's password, generated on this instance
MCPGATEWAY_CERT_SHA256 The fingerprint of this instance's TLS certificate, to check the browser warning against

To read the password, run sudo cat /root/mcp-context-forge-credentials.txt in your own session and store it in a password manager.

Step 3: Confirm the gateway is running

The first-boot units, the time server, the gateway and nginx are active. Only sshd and nginx (443) listen beyond loopback: the gateway is on 127.0.0.1:4444, the time server on 127.0.0.1:8002 (port 53 is Ubuntu's local DNS resolver):

systemctl is-active mcp-context-forge-firstboot mcp-context-forge-federate mcp-timeserver mcp-context-forge nginx
sudo ss -Htln | awk '{print $4}' | sort -u
active
active
active
active
active
0.0.0.0:22
0.0.0.0:443
127.0.0.1:4444
127.0.0.1:8002
127.0.0.53%lo:53
127.0.0.54:53
[::]:22
[::]:443

The version, the health endpoint (verified against the instance's own certificate) and the build-time dependency audit, which found no known vulnerability in either Python environment:

/opt/mcpgateway/venv/bin/python -c 'import mcpgateway; print("ContextForge", mcpgateway.__version__)'
curl -s --cacert /etc/nginx/tls/mcpgateway.crt https://localhost/health | jq -r .status
grep -c 'No known vulnerabilities found' /usr/share/doc/cloudimg/mcp-context-forge/pip-audit.txt
ContextForge 1.0.11
healthy
2

Step 4: Sign in to the admin UI

Open MCPGATEWAY_ADMIN_UI in your browser. The certificate is self-signed, so the browser warns once; compare the certificate's SHA-256 fingerprint with MCPGATEWAY_CERT_SHA256 before you accept it (or install your own certificate, Step 8). Sign in with MCPGATEWAY_ADMIN_EMAIL and MCPGATEWAY_ADMIN_PASSWORD. A wrong password is refused and no session is created:

The ContextForge sign-in page showing Invalid email or password after a wrong password was entered for admin@example.com

With the right password you land on the overview, which shows the bundled MCP server, its two tools and the calls made so far:

The ContextForge System Overview with 1 of 1 MCP servers, 2 of 2 tools, 1 virtual server, the Python MCP runtime and a 100 percent success rate

MCP Servers lists the upstream servers the gateway federates. localtime is the bundled time server, active and reachable on loopback; you add your own servers on the same page:

The ContextForge MCP Servers page listing the localtime gateway at http://localhost:8002/sse with status Active, and the Add New MCP Server or Gateway form

Tools is the registry of every tool the gateway discovered, here the time server's two tools with their read-only and idempotent annotations:

The ContextForge MCP Tools page listing localtime-convert-time and localtime-get-current-time from the localtime source over SSE

Step 5: Use the API and call a tool through the gateway

The REST and JSON-RPC API needs a bearer token. sudo mcpgateway-api-token signs in as the administrator on loopback (the password goes to curl on standard input, never on a command line) and prints the token the gateway issues; it is valid for 20 minutes. The examples pass the token to curl through a file descriptor, so it is not visible in the process list either. The registered upstream servers and their tools:

TOKEN=$(sudo mcpgateway-api-token)
auth() { printf 'header = "Authorization: Bearer %s"\n' "$TOKEN"; }
curl -s --cacert /etc/nginx/tls/mcpgateway.crt -K <(auth) https://localhost/gateways \
  | jq -r '.[] | [.name, .url, .transport, "reachable=\(.reachable)"] | @tsv'
curl -s --cacert /etc/nginx/tls/mcpgateway.crt -K <(auth) https://localhost/tools \
  | jq -r '.[] | [.name, .description] | @tsv'
localtime   http://localhost:8002/sse   SSE reachable=true
localtime-convert-time  Convert time between timezones
localtime-get-current-time  Get current time in a specific timezone

Call a tool through the gateway with JSON-RPC. The gateway forwards the call to the upstream server and returns its answer:

TOKEN=$(sudo mcpgateway-api-token)
auth() { printf 'header = "Authorization: Bearer %s"\n' "$TOKEN"; }
curl -s --cacert /etc/nginx/tls/mcpgateway.crt -K <(auth) -X POST https://localhost/rpc \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"localtime-get-current-time","arguments":{"timezone":"Europe/London"}}}' \
  | jq -r '.result.content[0].text'
{
  "timezone": "Europe/London",
  "datetime": "2026-10-20T11:00:25+01:00",
  "day_of_week": "Tuesday",
  "is_dst": true
}

For long-lived tokens for your applications, use API Tokens in the admin UI (each token can be limited to a team, a server, permissions, source IP ranges and an expiry date).

Step 6: Create a virtual server for your MCP clients

MCP clients connect to a virtual server: a named set of tools, prompts and resources with its own endpoint. Create one with both time tools (the admin UI's Virtual Servers page does the same):

TOKEN=$(sudo mcpgateway-api-token)
auth() { printf 'header = "Authorization: Bearer %s"\n' "$TOKEN"; }
TOOLS=$(curl -s --cacert /etc/nginx/tls/mcpgateway.crt -K <(auth) https://localhost/tools | jq -c '[.[].id]')
curl -s --cacert /etc/nginx/tls/mcpgateway.crt -K <(auth) -X POST https://localhost/servers \
  -H 'Content-Type: application/json' \
  -d "{\"server\":{\"name\":\"time-tools\",\"description\":\"Time tools for MCP clients\",\"associated_tools\":${TOOLS}}}" \
  | jq '{id, name, associatedTools, url}'
{
  "id": "0ca5ab32a7c5417db99ee7cef9b6712b",
  "name": "time-tools",
  "associatedTools": [
    "localtime-get-current-time",
    "localtime-convert-time"
  ],
  "url": "https://ec2-3-94-54-86.compute-1.amazonaws.com/servers/0ca5ab32a7c5417db99ee7cef9b6712b/mcp"
}

The url is the virtual server's MCP endpoint (Streamable HTTP), built from the instance's configured hostname. An MCP client lists its tools like this:

TOKEN=$(sudo mcpgateway-api-token)
auth() { printf 'header = "Authorization: Bearer %s"\n' "$TOKEN"; }
SERVER=$(curl -s --cacert /etc/nginx/tls/mcpgateway.crt -K <(auth) https://localhost/servers | jq -r '.[] | select(.name=="time-tools") | .id')
curl -s --cacert /etc/nginx/tls/mcpgateway.crt -K <(auth) -X POST "https://localhost/servers/${SERVER}/mcp" \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq -r '.result.tools[].name'
localtime-get-current-time
localtime-convert-time

To connect a client such as Claude Desktop, Cursor or VS Code, give it the virtual server's URL and an API token as a bearer header. A client that speaks Streamable HTTP takes the URL directly, for example:

{
  "mcpServers": {
    "contextforge-time": {
      "type": "http",
      "url": "https://<your-dns-name>/servers/<server-id>/mcp",
      "headers": { "Authorization": "Bearer <api-token>" }
    }
  }
}

A client that only speaks stdio can reach it through FastMCP's bridge (uvx fastmcp-remote). Install your own certificate (Step 8) before connecting clients that verify TLS, or point them at this instance's certificate.

Step 7: Check the security controls

Without a token the API answers 401; a request for any other hostname has its connection closed (000); nothing listens on port 80; and a cross-origin form post is refused by nginx (403):

curl -s -o /dev/null -w 'no token -> %{http_code}\n' --cacert /etc/nginx/tls/mcpgateway.crt https://localhost/tools
curl -s -o /dev/null -w 'unknown Host -> %{http_code}\n' -k -H 'Host: example.com' https://localhost/health || true
curl -s -o /dev/null -w 'port 80 -> %{http_code}\n' http://localhost/ || true
curl -s -o /dev/null -w 'cross-origin POST -> %{http_code}\n' --cacert /etc/nginx/tls/mcpgateway.crt -X POST -H 'Origin: https://evil.example' https://localhost/auth/email/login
no token -> 401
unknown Host -> 000
port 80 -> 000
cross-origin POST -> 403

The services are denied the instance metadata service, and the gateway refuses to register an MCP server whose URL points at it. The API answers 422 without detail; the gateway's log names the rejected field:

systemctl show -p IPAddressDeny mcp-context-forge mcp-timeserver nginx
TOKEN=$(sudo mcpgateway-api-token)
auth() { printf 'header = "Authorization: Bearer %s"\n' "$TOKEN"; }
curl -s --cacert /etc/nginx/tls/mcpgateway.crt -K <(auth) -X POST https://localhost/gateways \
  -H 'Content-Type: application/json' -w '\nHTTP %{http_code}\n' \
  -d '{"name":"metadata-test","url":"http://169.254.169.254/latest/meta-data/"}'
sudo journalctl -u mcp-context-forge --since '-1min' -o cat | grep -o "Request validation error on /gateways[^\"]*" | tail -1
IPAddressDeny=::ffff:169.254.169.254/128 fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32

IPAddressDeny=::ffff:169.254.169.254/128 fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32

IPAddressDeny=::ffff:169.254.169.254/128 fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32
{"detail":"An error occurred, please try again."}
HTTP 422
Request validation error on /gateways: 1 error(s): [loc=('body', 'url') type=value_error]

The gateway's settings that this image sets, and its database, created on this instance's first boot:

sudo grep -E '^(ENVIRONMENT|AUTH_REQUIRED|PUBLIC_REGISTRATION_ENABLED|API_ALLOW_BASIC_AUTH|SSRF_[A-Z_]+|APP_DOMAIN)=' /etc/mcpgateway/mcpgateway.env
sudo ls -l /var/lib/mcpgateway/
ENVIRONMENT=production
APP_DOMAIN=https://ec2-3-94-54-86.compute-1.amazonaws.com
AUTH_REQUIRED=true
PUBLIC_REGISTRATION_ENABLED=false
API_ALLOW_BASIC_AUTH=false
SSRF_PROTECTION_ENABLED=true
SSRF_ALLOW_LOCALHOST=true
SSRF_ALLOW_PRIVATE_NETWORKS=false
SSRF_DNS_FAIL_CLOSED=true
SSRF_BLOCKED_NETWORKS=["169.254.169.254/32","169.254.169.123/32","fd00::1/128","169.254.0.0/16","fe80::/10","100.64.0.0/10","168.63.129.16/32","fd00:ec2::/32","0.0.0.0/8"]
total 3100
-rw-r----- 1 mcpgateway mcpgateway 2080768 Oct 20 09:50 mcp.db
-rw-r----- 1 mcpgateway mcpgateway   32768 Oct 20 10:00 mcp.db-shm
-rw-r----- 1 mcpgateway mcpgateway 1054752 Oct 20 10:00 mcp.db-wal

Registering MCP servers on your private network. Private (RFC 1918) addresses are refused by default. To federate MCP servers inside your VPC, allow only their ranges: add SSRF_ALLOWED_NETWORKS=["10.20.0.0/24"] (your range) to /etc/mcpgateway/mcpgateway.env and run sudo systemctl restart mcp-context-forge. Never add 169.254.0.0/16, and never set SSRF_PROTECTION_ENABLED=false.

Step 8: Use your own DNS name and certificate

Point a DNS name at the instance (an Elastic IP keeps the address stable), then set it as the name the gateway is served as. The helper creates a new self-signed certificate for the name, sets the gateway's APP_DOMAIN and allowed origin, reloads nginx and restarts the gateway; requests for any other name are then refused:

sudo mcpgateway-set-hostname mcp.example.com

To use a certificate from your own CA or from Let's Encrypt, replace /etc/nginx/tls/mcpgateway.crt and /etc/nginx/tls/mcpgateway.key (mode 0600) with your pair for the same name and run sudo systemctl reload nginx.

Maintenance

  • Logs: sudo journalctl -u mcp-context-forge (the gateway, JSON), sudo journalctl -u mcp-timeserver, /var/log/nginx/ for HTTPS access, sudo journalctl -u mcp-context-forge-firstboot -u mcp-context-forge-federate for first boot, and System Logs in the admin UI.
  • Restart: sudo systemctl restart mcp-context-forge (or mcp-timeserver, nginx).
  • Configuration: /etc/mcpgateway/mcpgateway.env (root only, mode 0600). Keep JWT_SECRET_KEY and AUTH_ENCRYPTION_SECRET safe: changing the first signs everyone out, and changing the second makes stored upstream credentials unreadable.
  • Backups: stop the gateway briefly (sudo systemctl stop mcp-context-forge), copy /var/lib/mcpgateway/ and /etc/mcpgateway/mcpgateway.env somewhere safe, then start it again. Also take EBS snapshots of the volume.
  • Operating system updates: Ubuntu's unattended security upgrades stay on, so nginx, Python and the rest of the OS keep receiving patches. Apply the rest with sudo apt update && sudo apt upgrade in a maintenance window.
  • Upgrading ContextForge: read the release notes, back up as above, then sudo /opt/mcpgateway/venv/bin/pip install --only-binary :all: 'mcp-contextforge-gateway==<version>' and sudo systemctl restart mcp-context-forge. The gateway migrates its database on start.

Troubleshooting

The sign-in page says Invalid email or password. Use MCPGATEWAY_ADMIN_EMAIL and MCPGATEWAY_ADMIN_PASSWORD from /root/mcp-context-forge-credentials.txt exactly. To change the password, open Users in the admin UI and require a password change for your account: the next sign-in asks for a new one. The credentials file keeps the first-boot value.

Signing in answers 403. The sign-in form is protected against cross-site requests and only accepts sign-ins for the configured name: open the admin UI at the exact URL in MCPGATEWAY_ADMIN_UI (or the name you set with mcpgateway-set-hostname), not by IP address.

The connection is closed. nginx serves only the configured name, the instance's public and private IPv4 addresses and localhost, and closes the connection for any other name. After a stop and start without an Elastic IP the public name and address change: run sudo mcpgateway-set-hostname <new-name>.

Registering an MCP server answers 422. The URL is refused by the server-side request forgery checks (metadata, link-local, private or unresolvable addresses) or is malformed; sudo journalctl -u mcp-context-forge -o cat | grep 'Request validation error' shows it. See "Registering MCP servers on your private network" in Step 7.

The gateway does not start. sudo journalctl -u mcp-context-forge --no-pager | tail -20 shows the reason, for example mcpgateway-guard refusing a missing or weak secret in /etc/mcpgateway/mcpgateway.env.

Support

cloudimg provides 24/7 support for this image by email and live chat: launching and connecting, registering MCP servers, virtual servers and API tokens, connecting MCP clients, TLS and hostnames, and upgrades.

ContextForge is developed by IBM and contributors and is released under the Apache License 2.0; the licence text ships on the instance in /usr/share/doc/cloudimg/mcp-context-forge/LICENSE. The bundled mcp-server-time is part of the Model Context Protocol reference servers and is released under the MIT licence. Ubuntu is a registered trademark of Canonical Ltd. All other product and company names are trademarks or registered trademarks of their respective holders. cloudimg is not affiliated with or endorsed by them.