Zope 6.3 on AWS User Guide
Overview
This guide covers launching and using the Zope 6.3 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg CMS, Blogs and Wikis listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Zope is an open source web application server written in Python. Everything it serves lives as objects in ZODB, its built-in object database: folders, pages, DTML methods, page templates, files and images. You build and manage a site through the Zope Management Interface (ZMI) at /manage, and Zope publishes each object at its own URL. The image runs Zope 6.3 (released 22 October 2026, the newest release of the current Zope 6 line) on Python 3.12.
The cloudimg image adds:
- No shared database, no default administrator. The image ships no ZODB (
Data.fs), no user and noinituseror emergencyaccessfile. Your instance's first boot creates its own ZODB and anadminuser with a password unique to your instance, through Zope's own user folder, before the web server starts. The password is stored only as a salted hash in your ZODB and in a credentials file only root can read. - Secrets never on a command line. The password reaches Zope on standard input, never as a command argument that lands in logs, and nothing from the image build is reused.
- An audited, pinned package set. Zope and its 77 companion Python distributions are installed from Zope's published 6.3 version list with one SHA-256 checksum per package, into a virtual environment that belongs to root. The exact set was checked for known vulnerabilities before the build (
pip-auditand the OSV database report none). Paste is deliberately left out: Zope does not need it, and the pinned release has a known flaw in its session module. - HTTPS from the first boot. nginx serves the ZMI on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS, and the instance's other addresses (its IP addresses) redirect to the one host name Zope is served as.
- Zope on a private socket. Zope's WSGI server (waitress) listens on a Unix socket that only nginx can reach, so there is no Zope port on the network at all, not even on localhost.
- Fail closed. Zope and nginx do not start until first boot has finished; if first boot cannot complete, the site stays offline rather than open.
- Rate-limited sign in. Every request that carries a password (HTTP Basic authentication, on any path and in any form) counts towards a limit of 60 a minute from one address after a burst of 60; nginx then answers 429. The limit uses the address of the connection, never a header a client sends.
- Requests from other sites cannot act for you. Browsers remember Basic authentication, so a page on another site could otherwise make your browser change your Zope. Every POST, every link with arguments (Zope runs methods named in a query string on any object) and every link to a
manage_method,@@view or++traversal needs proof that it comes from your site itself; only the plain ZMI entry pages (/manage,manage_mainand friends) stay linkable and bookmarkable. - No WebDAV, no XML-RPC, no client virtual hosting. nginx answers only GET, HEAD and POST (WebDAV methods such as PROPFIND, PUT, MKCOL and LOCK get 405), XML-RPC is off, and a client cannot send Zope's
VirtualHostBase,VirtualHostRootor_vh_path elements: nginx alone adds them, with the host name you configure, so every URL Zope generates names that host. - The real host and client address. nginx hands Zope the configured host name and the address of the connection, and removes
X-Forwarded-Host,Forwarded,Client-IPand the other forwarding headers (andCGI-Authorization, which Zope would otherwise treat as a sign-in). - Hardened defaults. Zope runs as its own
zopeuser in a systemd sandbox; the only place it can write is its database directory/var/lib/zope, and it cannot change its own code or configuration. Debug mode is off. Zope, nginx and the first boot service are blocked from the instance metadata service, the AMI requires IMDSv2, and requests for a host name the instance does not serve are dropped. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
Know what the administrator can do. The ZMI is a programming environment: a user with the Manager role can create DTML methods and page templates that run (sandboxed) Python expressions, change security settings and delete anything. Treat the admin password like a root password, and give other people their own users with only the roles they need.
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), TCP 443 (Zope) and TCP 80 (redirects to HTTPS) from the networks that should reach them. Keep 22, and the ZMI if you can, to your own networks.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Zope 6.3 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (or a burstable t3.medium for light use), your key pair and a security group as above.
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=zope}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Zope 6.3 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot takes well under a minute (it creates the certificate, the ZODB and the administrator). Then nginx, Zope and the first boot unit are active:
systemctl is-active nginx zope zope-firstboot
active
active
active
nginx serves ports 80 and 443 and Zope listens only on its Unix socket, so the only open ports are 22, 80 and 443:
sudo ss -tlnp | awk 'NR==1 || /nginx|sshd|python|runwsgi/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
ls -l /run/zope/zope.sock | awk '{print $1, $3, $4, $NF}'
State Local Peer
LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN 0.0.0.0:22 users:(("sshd"),("systemd"))
LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:22 users:(("sshd"),("systemd"))
LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))
srw-rw---- zope zope /run/zope/zope.sock
The Zope release and the versions of its main parts:
cat /usr/share/doc/zope/RELEASE
/opt/zope/venv/bin/python -c 'import importlib.metadata as m; print("Zope", m.version("Zope"), "| waitress", m.version("waitress"), "| ZODB", m.version("ZODB"), "| RestrictedPython", m.version("RestrictedPython"), "| AccessControl", m.version("AccessControl"))'
/opt/zope/venv/bin/python --version
nginx -v 2>&1
Zope 6.3 (ZPL-2.1), Python 3.12 venv /opt/zope/venv
requirements-sha256 ff318d61f351c72606433394a169d002382173fce16ea213bc0bee1c8a5d55eb
Paste: not installed (GHSA-vf89-fq68-57cx); Chameleon 4.6.0 under the Repoze licence (BSD-style)
Zope 6.3 | waitress 3.0.2 | ZODB 6.4 | RestrictedPython 8.5 | AccessControl 7.5
Python 3.12.3
nginx version: nginx/1.24.0 (Ubuntu)
requirements-sha256 is the checksum of the installed package list (/opt/zope/venv/bin/pip list --format=freeze, names normalised), the list that was checked for known vulnerabilities before the build.
Step 4: Get your sign-in details
The credentials file holds the address Zope is served as (your instance's public DNS name by default), the ZMI address, the administrator's user name and password, all generated on this instance, and the SHA-256 fingerprint of the instance's certificate:
sudo cat /root/zope-credentials.txt
# Zope 6.3 on Ubuntu 24.04 - this instance's own credentials (cloudimg). Keep this file private (0600 root).
ZOPE_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
ZOPE_MANAGE_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/manage
ZOPE_ADMIN_USER=admin
ZOPE_ADMIN_PASSWORD=<48 hexadecimal characters>
ZOPE_TLS_SHA256=<the certificate's SHA-256 fingerprint>
# Change the password: sudo zope-set-password | Change the served name: sudo zope-set-hostname <name>
Step 5: Open Zope and sign in to the ZMI
Open ZOPE_URL in your browser. The certificate is self signed (generated on your instance), so your browser warns once: compare the certificate's SHA-256 fingerprint with ZOPE_TLS_SHA256 and continue, or install a certificate from your own certificate authority (Step 9). Anyone can see Zope's welcome page:

Now open ZOPE_MANAGE_URL (/manage). Your browser asks for a user name and password: enter ZOPE_ADMIN_USER and ZOPE_ADMIN_PASSWORD. The ZMI opens on the root folder, which holds the user folder acl_users, the welcome page index_html and the virtual host monster nginx uses:

Bookmark /manage. Links that change something only work from inside the ZMI (Step 8), so open the ZMI and click through to the page you want rather than pasting a long ZMI address into the browser.
Step 6: Build with the ZMI
Choose Select type to add at the top right to add objects to the folder you are in. For example, choose Folder, enter an Id and a Title and click Add:

The new folder appears in the root folder, and Zope publishes it at https://<your host>/<id>/:

The same menu adds DTML documents and methods, page templates, files and images. Each object's Security tab sets which roles may view or change it.
Users live in a user folder. Open acl_users to see them, and use Add there to create users for the people who work on the site, with only the roles they need (Manager is full control):

The Control Panel (top right) shows the Zope and Python versions and the database, and is where you pack the database (Step 7):

Step 7: Your data, packing and backups
Everything you build is in the ZODB file /var/lib/zope/Data.fs (with its index). Only the zope user can read it:
sudo ls -l /var/lib/zope | awk 'NR>1{print $1, $3, $4, $NF}'
-rw------- zope zope Data.fs
-rw------- zope zope Data.fs.index
-rw------- zope zope Data.fs.lock
-rw------- zope zope Data.fs.tmp
-rw------- zope zope Z4.pid
The ZODB keeps every old version of every object, which is what makes Undo possible, so Data.fs grows. To remove old versions, open Control Panel > Databases > main, enter how many days of history to keep and click Pack.
For a consistent backup, stop Zope for the copy (a few seconds) and keep the copy somewhere safe, for example an encrypted S3 bucket:
sudo systemctl stop zope
sudo tar -C /var/lib/zope -czf /root/zope-backup-$(date +%F).tar.gz Data.fs
sudo systemctl start zope
For backups without stopping Zope, ZODB's own repozo tool (/opt/zope/venv/bin/repozo) makes full and incremental backups of a running Data.fs; run it as the zope user into a directory that user can write. An EBS snapshot of the instance's volume also captures everything. The ZODB holds password hashes and all your content, so keep backups as private as the instance itself.
To restore, stop Zope, put the backed up Data.fs back in /var/lib/zope owned by zope with mode 0600, remove Data.fs.index and start Zope.
Step 8: Security checks
Port 80 and the instance's IP address send browsers to the configured HTTPS host; the ZMI refuses requests without the right password; WebDAV methods, client virtual-host paths and unknown host names are refused:
H=$(sed -n 's/^set \$zope_host "\(.*\)";$/\1/p' /etc/nginx/zope-host.conf)
curl -s -o /dev/null -D - --resolve "$H:80:127.0.0.1" "http://$H/manage" | tr -d '\r' | awk '/^HTTP/ {printf "port 80: %s ", $2} /^[Ll]ocation:/ {print "->", $2}' | sed -E 's#https://[^/]+#https://<your host>#'
IP=$(hostname -I | awk '{print $1}'); curl -sk -o /dev/null -D - "https://$IP/manage" | tr -d '\r' | awk '/^HTTP/ {printf "by IP address: %s ", $2} /^[Ll]ocation:/ {print "->", $2}' | sed -E 's#https://[^/]+#https://<your host>#'
curl -sk -o /dev/null -w 'welcome page: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'ZMI without a password: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/manage"
curl -sk -o /dev/null -w 'ZMI as admin/admin: %{http_code}\n' -u admin:admin --resolve "$H:443:127.0.0.1" "https://$H/manage_main"
for m in PROPFIND PUT MKCOL LOCK DELETE OPTIONS; do curl -sk -o /dev/null -w "$m /: %{http_code}\n" -X $m --resolve "$H:443:127.0.0.1" "https://$H/"; done
curl -sk -o /dev/null -w 'client VirtualHostBase path: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/VirtualHostBase/https/example.com:443/VirtualHostRoot/"
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || true
port 80: 301 -> https://<your host>/manage
by IP address: 301 -> https://<your host>/manage
welcome page: 200
ZMI without a password: 401
ZMI as admin/admin: 401
PROPFIND /: 405
PUT /: 405
MKCOL /: 405
LOCK /: 405
DELETE /: 405
OPTIONS /: 405
client VirtualHostBase path: 400
unknown host name: 000
(000 means nginx closed the connection without an answer.) A form posted to your site from another site, or by a script that does not say it comes from your site, and a link with arguments followed from another site are refused; your site's own pages pass:
H=$(sed -n 's/^set \$zope_host "\(.*\)";$/\1/p' /etc/nginx/zope-host.conf)
curl -sk -o /dev/null -w 'POST from another site: %{http_code}\n' --resolve "$H:443:127.0.0.1" -X POST -H 'Origin: https://example.com' --data 'x=1' "https://$H/manage_main"
curl -sk -o /dev/null -w 'POST with no origin at all: %{http_code}\n' --resolve "$H:443:127.0.0.1" -X POST --data 'x=1' "https://$H/"
curl -sk -o /dev/null -w 'link with arguments from another site: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Sec-Fetch-Site: cross-site' "https://$H/manage_delObjects?ids:list=x"
curl -sk -o /dev/null -w 'POST from your site: %{http_code}\n' --resolve "$H:443:127.0.0.1" -X POST -H "Origin: https://$H" --data 'x=1' "https://$H/"
POST from another site: 403
POST with no origin at all: 403
link with arguments from another site: 403
POST from your site: 200
Repeated wrong passwords from one address are cut off (here 70 attempts in a few seconds from one loopback address; the limit recovers at one attempt a second):
H=$(sed -n 's/^set \$zope_host "\(.*\)";$/\1/p' /etc/nginx/zope-host.conf)
for i in $(seq 1 70); do curl -sk -o /dev/null -w '%{http_code}\n' --interface 127.0.0.11 -u "admin:wrong-$i" --resolve "$H:443:127.0.0.1" "https://$H/manage_main"; done | sort | uniq -c
61 401
9 429
The credentials file and the TLS key are readable by root only, the code and configuration belong to root, and the zope user owns only its database directory and cannot change its own code:
sudo stat -c '%a %U:%G %n' /root/zope-credentials.txt /opt/zope/venv /opt/zope/instance/etc /opt/zope/instance/etc/zope.conf /var/lib/zope /var/lib/zope/Data.fs /etc/nginx/ssl/zope.key
sudo setpriv --reuid=zope --regid=zope --init-groups test -w /opt/zope/venv && echo 'zope CAN write /opt/zope/venv' || echo 'zope cannot write /opt/zope/venv'
600 root:root /root/zope-credentials.txt
755 root:root /opt/zope/venv
750 root:zope /opt/zope/instance/etc
640 root:zope /opt/zope/instance/etc/zope.conf
700 zope:zope /var/lib/zope
600 zope:zope /var/lib/zope/Data.fs
600 root:root /etc/nginx/ssl/zope.key
zope cannot write /opt/zope/venv
Zope, nginx and the first boot service cannot reach the instance metadata service:
for u in zope nginx zope-firstboot; do printf '%s: ' $u; systemctl show -p IPAddressDeny --value $u; done
zope: 169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
nginx: 169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
zope-firstboot: 169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Step 9: Your own DNS name and certificate
Zope is served under one host name, your instance's public DNS name by default. To use your own name, point a DNS record at the instance (an Elastic IP keeps the address stable), then run:
sudo zope-set-hostname zope.example.com
It generates a new self-signed certificate for that name, makes nginx serve Zope under it (the instance's other names redirect to it), and records the name in /etc/cloudimg/zope-hostname, so it survives reboots. Every URL Zope generates follows the new name at once.
To use a certificate from your certificate authority (or Let's Encrypt), replace /etc/nginx/ssl/zope.crt (the full chain) and /etc/nginx/ssl/zope.key (mode 0600, owner root) and run sudo systemctl reload nginx. With a trusted certificate you can also add a Strict-Transport-Security header in /etc/nginx/snippets/cloudimg-zope-headers.conf.
Behind an Application Load Balancer, send HTTPS to the instance on port 443 and keep the instance's security group closed to everyone but the load balancer. The health check path /healthz answers ok without signing in.
Step 10: Passwords, users, add-on products and upgrades
- Change the administrator password (a new random password is written to the credentials file; Zope stops for a few seconds):
text
sudo zope-set-password
To choose the password yourself (16 to 256 characters), put it in a file only you can read and run sudo zope-set-password --stdin < <your-password-file>. You can also change it in the ZMI under acl_users > admin. Do not use Zope's addzopeuser command: it takes the password as a command argument, which ends up in logs.
* Add-on products (for example Products.PythonScripts for Script (Python) objects, or Products.ExternalMethod) install into Zope's virtual environment as root, with Zope's version list as constraints so the packages Zope shares stay at the versions it was released with, followed by a Zope restart. Add-ons are not part of the audited package set, so check them for known vulnerabilities yourself (for example with pip-audit):
text
sudo /opt/zope/venv/bin/pip install -c https://zopefoundation.github.io/Zope/releases/6.3/constraints.txt Products.PythonScripts
sudo systemctl restart zope
Each product extends what a Manager can do (External Methods run unrestricted Python from files on the server), so install only products you trust and need.
* Upgrades. Zope publishes a constraints file for every release; to move to a newer Zope 6 release, back up Data.fs (Step 7) and reinstall the virtual environment's packages from the new release's constraints file, then restart Zope.
Step 11: Server components
| Component | Location |
|---|---|
| Zope 6.3 virtual environment (root-owned) | /opt/zope/venv |
| Zope instance configuration | /opt/zope/instance/etc/zope.conf, zope.ini |
| ZODB database | /var/lib/zope/Data.fs |
| Zope service | zope.service (runs runwsgi as zope on /run/zope/zope.sock) |
| nginx site and guards | /etc/nginx/sites-available/cloudimg-zope, /etc/nginx/conf.d/cloudimg-zope-guard.conf |
| TLS certificate and key | /etc/nginx/ssl/zope.crt, /etc/nginx/ssl/zope.key |
| Helpers | zope-set-hostname, zope-set-password, zope-cred-roundtrip in /usr/local/sbin |
| Licence and release notes | /usr/share/doc/zope/ |
| Credentials | /root/zope-credentials.txt (mode 0600) |
Zope logs to the journal (sudo journalctl -u zope), nginx to /var/log/nginx/.
Step 12: Security recommendations
- Allow port 22 in the security group from your own networks only, and the ZMI too if you can (or put it behind a VPN or a load balancer with its own access rules).
- Keep the
adminpassword secret and rotate it if it may have been shared (Step 10). Give each person their own user with the least role they need. - Keep
Data.fsand your backups private (Step 7): they hold your content and the password hashes. - Use your own DNS name and a certificate your browsers trust (Step 9).
- Install only add-on products you trust, and keep Zope on the newest release of the Zope 6 line; follow the Zope Foundation's security announcements.
- Keep the operating system patched; unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else.
Support and licensing
Zope is free software under the Zope Public License 2.1 (a copy is at /usr/share/doc/zope/LICENSE.txt), and the Python packages it uses carry their own open source licences (ZPL, MIT, BSD and similar; the Chameleon template engine uses the BSD-style Repoze licence). There is no per user or per CPU licence fee and no licence key. This image installs the unmodified upstream releases plus the cloudimg first boot, configuration and helpers described above. Zope is a project of the Zope Foundation; cloudimg is not affiliated with it. The AMI price covers cloudimg's build, hardening and per-instance setup.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA