ZenphotoCMS 1.6.8 on AWS User Guide
Overview
This guide covers launching and using the ZenphotoCMS 1.6.8 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg File Sharing and Sync listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
ZenphotoCMS (Zenphoto) is a mature, open source photo gallery and lightweight content management system. You organise images and videos into albums and sub albums, and Zenphoto publishes them as a themed web gallery with thumbnails, EXIF and IPTC metadata, tags, search, RSS feeds and per album access control. Its Zenpage module adds pages and a news blog, and its backend manages uploads, users, themes and plugins.
The cloudimg image adds:
- HTTPS and a unique admin password from the first boot. nginx serves Zenphoto on port 443 with a certificate generated on your instance; port 80 redirects to HTTPS, and requests for a host name the instance does not serve get no answer. The
adminpassword, and Zenphoto's per install password salt (which also keys its form tokens and cookies), are generated on first boot. The password is written to/root/zenphoto-credentials.txt(readable by root only); nothing secret is baked into the image. - No web installer, ever. Zenphoto's setup wizard was completed when the image was built and then locked with Zenphoto's own protection. The web server refuses the setup pages, and the web server user cannot unlock them. A fresh Zenphoto install would give the first visitor full admin rights until an administrator exists; on this image the gallery does not start until first boot has created the administrator.
- No database password at all. Zenphoto reaches MariaDB as the system user
www-dataover the local socket, and MariaDB checks the connecting process instead of a password. MariaDB listens on 127.0.0.1 only, and the database is created on first boot, so no database from the build ships in the image. - Security fixes that are not in a release yet. ZenphotoCMS 1.6.8 is the newest release, but the Zenphoto developers have fixed several security issues since then in their code. This image applies those fixes (search sorting, the GoogleMap plugin's map page, the rating plugin and backup restore), plus a cloudimg fix for links such as
javascript:...that got through Zenphoto's description filter. It also updates the editor (TinyMCE 7.9.3), the file manager (elFinder 2.1.70) and the HTML sanitiser (htmLawed 1.2.15) to versions without known advisories. The patch is at/usr/share/doc/cloudimg/zenphoto-1.6.8-cloudimg-security.patch. - Hardened defaults. PHP runs only for Zenphoto's own scripts: the configuration, logs, backups and uploaded files are never served or executed. nginx, PHP-FPM and MariaDB are blocked from the instance metadata service, and the AMI requires IMDSv2. Client addresses come from the connection, not from headers a client can forge. A browser request that changes something and comes from another web site is refused.
- A dedicated data volume. Your photos (
albums), uploads, the image cache and the MariaDB database live on their own 20 GiB EBS volume mounted at/data, which you can grow on its own. - No sample content. The gallery starts empty; you add your own photos.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: ZenphotoCMS 1.6.8 (/var/www/zenphoto), nginx, PHP 8.3 FPM, MariaDB 10.11, Ubuntu 24.04 LTS, 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 443 (the gallery and its backend) and TCP 80 (redirects to HTTPS) from the people who will use it, and TCP 22 (SSH) from your own networks only. Do not open 3306; MariaDB listens on the instance only.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick ZenphotoCMS 1.6.8 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.
The AMI has two volumes: the 8 GiB root volume and a 20 GiB data volume for /data. Size the data volume for your photos plus roughly a third again for Zenphoto's resized copies (for example 200 GiB instead of 20 GiB); the first boot grows its file system to fill the volume.
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/sdf,Ebs={VolumeSize=200,VolumeType=gp3}' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=zenphoto}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| ZenphotoCMS 1.6.8 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot takes a few seconds. Then nginx, PHP-FPM, MariaDB and the first boot unit are active:
systemctl is-active nginx php8.3-fpm mariadb zenphoto-firstboot
active
active
active
active
nginx is the only web listener, and MariaDB listens on 127.0.0.1 only:
sudo ss -tlnp | awk 'NR==1 || /nginx|sshd|mariadb/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
State Local Peer
LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN 0.0.0.0:22 users:(("sshd"),("systemd"))
LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN 127.0.0.1:3306 users:(("mariadbd"))
LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:22 users:(("sshd"),("systemd"))
LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx"))
The release, what the image carries on top of it, and the data volume:
cat /etc/cloudimg/zenphoto-release.txt
findmnt -no SOURCE,FSTYPE,SIZE /data
cloudimg: ZenphotoCMS 1.6.8 (GPL-2.0-or-later) from the upstream v1.6.8 tag; setup completed at build time and protected.
Carries: zenphoto-1.6.8-cloudimg-security.patch (sha256 210d8d9cbd0fe665c8e03824cf9751e5bd7475b180f9a374f892fe95b9f71757; upstream master d0a9edb4df, 6feaa5401f, 33ec9ab613, 0e5e3b7ada; cloudimg kses scheme-separator fix), htmLawed 1.2.15, TinyMCE 7.9.3, elFinder 2.1.70.
/dev/nvme1n1 ext4 19.5G
Step 4: Get your URL and password
The credentials file holds the address Zenphoto is served as (your instance's public DNS name by default), the backend address and the admin user:
sudo grep -E '^ZENPHOTO_(URL|ADMIN_URL|ADMIN_USER)=' /root/zenphoto-credentials.txt
ZENPHOTO_URL=https://<public-dns-name>/
ZENPHOTO_ADMIN_URL=https://<public-dns-name>/zp-core/admin.php
ZENPHOTO_ADMIN_USER=admin
The admin password, generated on this instance:
sudo grep '^ZENPHOTO_ADMIN_PASSWORD=' /root/zenphoto-credentials.txt | cut -d= -f2-
<ZENPHOTO_ADMIN_PASSWORD>
The rest of the file explains the database account (there is no database password) and the helper commands used in Steps 9 and 10.
Step 5: Open the gallery and sign in
Open ZENPHOTO_URL in your browser. The certificate is self signed (generated on your instance), so accept the browser warning once, or use your own name and certificate (Step 9). The gallery starts empty; once you add albums the front page lists them:

The images in these screenshots are generated test pictures; the image ships with an empty gallery.
Open ZENPHOTO_ADMIN_URL (or add /zp-core/admin.php to the address) and sign in as admin with the password from Step 4:

The Overview tab shows the installation (Zenphoto 1.6.8, PHP 8.3, MariaDB, the GD image library), utility functions and gallery statistics:

The notice "There is no mail handler configured" is expected: e-mail (password reset links, comment notifications) needs a mail plugin and your own mail server settings, under Plugins and Options.
Step 6: Upload photos
Choose the Upload tab, pick an existing album or type a title for a new one, then add images (GIF, JPEG, PNG, WEBP or a ZIP of images). One request can carry up to 256 MB:

For large collections, copy folders straight into the album folder over SSH; every folder becomes an album and Zenphoto picks them up the next time the gallery is opened. The album folder belongs to www-data, so copy to your home directory first and then move the files in. From your own computer, for example:
rsync -av --progress -e "ssh -i <your-key.pem>" ~/Pictures/Your-Photos/ ubuntu@<public-ip>:Your-Photos/
Then on the instance:
sudo mv ~/Your-Photos /var/www/zenphoto/albums/
sudo chown -R www-data:www-data /var/www/zenphoto/albums/Your-Photos
Step 7: Manage albums
The Albums tab lists your albums with their thumbnails, sub album and image counts, and publish, view, refresh and delete actions. Drag them to set the order, or click one to edit its title, description, tags, thumbnail, password and publishing:

Visitors open an album to see its thumbnails:

and an image to see it full size, with next and previous links:

Users creates further accounts and sets what each may do (for example upload to their own album only), Themes switches the gallery's look, Plugins turns on optional features (comments, contact form, Zenpage pages and news, maps, user registration) and Options holds the gallery title, image sizes, watermarks, search and security settings.
Step 8: Check the security model
The image ships a self test that proves the login over HTTPS: the admin password from your credentials file signs in, and blank, whitespace, common and malformed (array) passwords are refused with Zenphoto's own error and no session:
sudo /usr/local/sbin/zenphoto-cred-roundtrip.sh
OK per-instance admin signs in over HTTPS; blank, whitespace, weak and array passwords refused without a session
Plain HTTP redirects to HTTPS; the setup pages, the configuration folder, backups and stray PHP files are refused; a sign in posted from another web site is refused; and a request for a host name the instance does not serve gets no answer:
H=$(sudo sed -nE 's#^ZENPHOTO_URL=https://([^/]+)/.*#\1#p' /root/zenphoto-credentials.txt)
curl -s -o /dev/null -w 'plain HTTP: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
for p in /zp-core/setup/ /zp-core/setup.php /zp-data/zenphoto.cfg.php /backup/ /uploaded/x.php; do curl -sk -o /dev/null -w "$p: %{http_code}\n" --resolve "$H:443:127.0.0.1" "https://$H$p"; done
curl -sk -o /dev/null -w 'sign in posted from another web site: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Origin: https://evil.example' --data 'login=1&user=admin&pass=x' "https://$H/zp-core/admin.php"
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: evil.example' http://127.0.0.1/ || true
plain HTTP: 301 -> https://<public-dns-name>/
/zp-core/setup/: 404
/zp-core/setup.php: 404
/zp-data/zenphoto.cfg.php: 404
/backup/: 404
/uploaded/x.php: 404
sign in posted from another web site: 403
unknown host name: 000
The database account has no password, and the value in Zenphoto's configuration is only a placeholder Zenphoto requires (MariaDB ignores it for a socket account):
sudo mariadb -N -e "SELECT CONCAT(User,'@',Host,' ',JSON_VALUE(Priv,'\$.plugin')) FROM mysql.global_priv WHERE User='www-data'"
sudo grep -B2 "^\$conf\['mysql_pass'\]" /var/www/zenphoto/zp-data/zenphoto.cfg.php
www-data@localhost unix_socket
// cloudimg: NOT a password. MariaDB authenticates www-data by the local socket peer (unix_socket) and ignores
// this value; Zenphoto only requires the field to be non-empty. No database password exists on this instance.
$conf['mysql_pass'] = 'unix_socket-auth-no-password'; // Supply your Database password.
nginx, PHP-FPM and MariaDB cannot reach the instance metadata service, and the secrets are readable by root only:
systemctl show nginx php8.3-fpm mariadb -p Id -p IPAddressDeny
sudo stat -c '%a %U:%G %n' /root/zenphoto-credentials.txt /etc/ssl/zenphoto/zenphoto.key
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Id=nginx.service
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Id=php8.3-fpm.service
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Id=mariadb.service
600 root:root /root/zenphoto-credentials.txt
600 root:root /etc/ssl/zenphoto/zenphoto.key
Step 9: Your own DNS name and certificate
Point a DNS record at the instance, then set the name Zenphoto is served as. This mints a new self signed certificate for the name, updates nginx (which hands the name to Zenphoto, so its links and feeds use it) and the URLs in /root/zenphoto-credentials.txt, and keeps the name across reboots:
sudo zenphoto-set-hostname <your-domain>
To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running zenphoto-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/ssl/zenphoto/zenphoto.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/zenphoto/zenphoto.key
sudo nginx -t && sudo systemctl reload nginx
Step 10: Change the password
Change your password in the backend under Users (open the admin user, type the new password twice and apply). If you lose it, run the helper on the instance: it sets a new random password, prints it once and updates /root/zenphoto-credentials.txt. Zenphoto's sign in cookie is derived from the stored password, so changing it signs that user out everywhere:
sudo zenphoto-set-password
Step 11: Storage, backups and server components
To grow the data volume, modify the EBS volume in the console or CLI, then grow the file system on the instance (it is mounted by UUID with nofail; first boot also does this on every start):
sudo resize2fs "$(findmnt -no SOURCE /data)"
Back up /data/zenphoto/albums (your originals) and the database (sudo mariadb-dump zenphoto > zenphoto.sql), or snapshot the data volume. The image cache is rebuilt when missing. Zenphoto's own Backup/Restore utility writes database backups to /var/www/zenphoto/backup, which the web server never serves.
Running Zenphoto's setup again. Setup is locked on this image. You only need it after you upgrade Zenphoto's files yourself. Then rename the protected setup scripts back as root (cd /var/www/zenphoto/zp-core/setup && for f in *.php.xxx; do sudo mv "$f" "${f%.xxx}"; done), comment out the two lines in /etc/nginx/zenphoto-setup-lock.conf, reload nginx, run setup from the backend, and then reverse both steps. The Run setup and restore scripts buttons in the backend do not work while the lock is in place.
| Component | Path |
|---|---|
| ZenphotoCMS 1.6.8 | /var/www/zenphoto (root owned) |
| Configuration | /var/www/zenphoto/zp-data/zenphoto.cfg.php (www-data, 0640; holds no secret) |
| Data volume | /data (20 GiB gp3, ext4) |
| Your albums | /data/zenphoto/albums, mounted at /var/www/zenphoto/albums |
| Uploads and image cache | /data/zenphoto/uploaded, /data/zenphoto/cache |
| MariaDB data | /data/mysql, mounted at /var/lib/mysql |
| nginx site | /etc/nginx/sites-available/cloudimg-zenphoto |
| Certificate and key | /etc/ssl/zenphoto/ |
| First boot script | /usr/local/sbin/zenphoto-firstboot.sh |
| Host name helper | /usr/local/sbin/zenphoto-set-hostname |
| Password helper | /usr/local/sbin/zenphoto-set-password |
| Self test | /usr/local/sbin/zenphoto-cred-roundtrip.sh |
| Credentials | /root/zenphoto-credentials.txt (mode 0600) |
| Security patch and library licences | /usr/share/doc/cloudimg/ |
Step 12: Security recommendations
- Allow port 22 in the security group from your own networks only; open 80 and 443 only to the people who should see the gallery.
- Keep the admin password private and change it if it may have leaked (Step 10). Give other people their own users with only the rights they need.
- Use your own DNS name and a certificate your users trust (Step 9), so nobody learns to click through certificate warnings.
- Turn on optional plugins (comments, registration, the elFinder file manager, maps) only when you need them, and keep their spam protection on.
- Keep the operating system patched; unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else.
Support and licensing
ZenphotoCMS is free software under the GNU General Public License, version 2 or later (/var/www/zenphoto/LICENSE). TinyMCE is under GPL 2.0 or later, elFinder under the BSD 3 clause licence and htmLawed under LGPL 3 or GPL 2 or later; their licences are in /usr/share/doc/cloudimg/ and the Zenphoto tree. This image runs the ZenphotoCMS 1.6.8 release from https://github.com/zenphoto/zenphoto with the security changes listed above. There is no per user or per CPU licence fee. The AMI price covers cloudimg's hardening, first boot setup and support.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA