Deployment guide

Zenphoto

Overview

ZenphotoCMS (Zenphoto) is a mature, open source photo gallery and lightweight content management system. You organize images and videos into albums and sub-albums, and Zenphoto publishes them as a themed web gallery. The gallery includes 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. This guide covers deploying and using ZenphotoCMS 1.6.8 on Ubuntu 24.04 from SecureAnyCloud on Microsoft Azure.

What you get:

  • HTTPS and a unique admin password from the first boot. nginx serves Zenphoto on port 443 with a certificate generated on your VM. Port 80 redirects to HTTPS, and requests for a host name the VM does not serve get no answer. The admin password, 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-data over the local socket, and MariaDB checks the connecting process instead of a password. MariaDB listens on 127.0.0.1 only. 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). It also applies a SecureAnyCloud fix for links such as javascript:... that got through Zenphoto's description filter. It updates the editor (TinyMCE 7.9.3), the file manager (elFinder 2.1.70) and the HTML sanitizer (htmLawed 1.2.15) to versions without known advisories. The patch is at /usr/share/doc/sac/zenphoto-1.6.8-sac-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, MariaDB and the first boot service are blocked from the Azure Instance Metadata Service and the wire server. 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.
  • One place for your data. Your photos (albums), uploads, the image cache and the MariaDB database live under /data on the OS disk. They are easy to back up and to move to a data disk of your own (Step 11).
  • No sample content. The gallery starts empty, and 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 Azure Linux Agent.

Default user: the admin user you choose (examples use azureuser). Architecture: x64, Gen2. Recommended size: Standard_B2s.

Prerequisites

  • An Azure subscription and a subscription to this offer in Azure Marketplace.
  • An SSH public key or a password for the VM's admin user.
  • A network security group that allows TCP 443 (the gallery and its backend) and TCP 80 (redirects to HTTPS) from the people who will use it. It must allow TCP 22 (SSH) from your own networks only. Do not open 3306, because MariaDB listens on the VM only.
1

Deploy the virtual machine

In Azure Marketplace, search for ZenphotoCMS, select the SecureAnyCloud offer and click Create. On the Basics tab, choose your subscription, resource group and region, and name the VM. Choose Standard_B2s or larger and set the administrator account.

Under Inbound port rules, allow SSH (22), HTTP (80) and HTTPS (443). On the Disks tab, size the OS disk for your photos plus roughly a third again for Zenphoto's resized copies. Click Review + create, then Create.

With the Azure CLI, replace the values in angle brackets:

az vm image terms accept --urn sac1647283583153:zenphoto-ubuntu-24-04:default:latest
az vm create --resource-group <your-resource-group> --name zenphoto \
  --image sac1647283583153:zenphoto-ubuntu-24-04:default:latest \
  --size Standard_B2s --admin-username azureuser --ssh-key-values ~/.ssh/id_rsa.pub --os-disk-size-gb 128
az vm open-port --resource-group <your-resource-group> --name zenphoto --port 80,443 --priority 900
2

Connect to your VM

ssh azureuser@<vm-public-ip>
3

Check the services

First boot takes a few seconds. After that, 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:22 users:(("sshd"),("systemd")) LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx")) LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx")) LISTEN 127.0.0.1:3306 users:(("mariadbd")) LISTEN [::]:22 users:(("sshd"),("systemd")) LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx")) LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))

This command shows the release and what the image carries on top of it:

cat /etc/sac/zenphoto-release.txt
sac: 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-sac-security.patch (sha256 210d8d9cbd0fe665c8e03824cf9751e5bd7475b180f9a374f892fe95b9f71757; upstream master d0a9edb4df, 6feaa5401f, 33ec9ab613, 0e5e3b7ada; sac kses scheme-separator fix), htmLawed 1.2.15, TinyMCE 7.9.3, elFinder 2.1.70.
4

Get your URL and password

The credentials file holds the address Zenphoto is served as, the backend address and the admin user. By default the address is your VM's public IP address, taken from the Azure Instance Metadata Service:

sudo grep -E '^ZENPHOTO_(URL|ADMIN_URL|ADMIN_USER)=' /root/zenphoto-credentials.txt
ZENPHOTO_URL=https://20.42.94.124/ ZENPHOTO_ADMIN_URL=https://20.42.94.124/zp-core/admin.php ZENPHOTO_ADMIN_USER=admin

This command prints the admin password, which was generated on this VM:

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.

5

Open the gallery and sign in

Open ZENPHOTO_URL in your browser. The certificate is self-signed and was generated on your VM, 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 sample pictures. The image itself 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.

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. For example, from your own computer:

rsync -av --progress ~/Pictures/Your-Photos/ azureuser@<vm-public-ip>:Your-Photos/

Then on the VM:

sudo mv ~/Your-Photos /var/www/zenphoto/albums/
sudo chown -R www-data:www-data /var/www/zenphoto/albums/Your-Photos
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 albums 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. They open an image to see it full size, with next and previous links.

Other backend tabs cover the rest of the gallery:

  • 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).
  • Options holds the gallery title, image sizes, watermarks, search and security settings.
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 VM 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://20.42.94.124/ /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. The value in Zenphoto's configuration is only a dummy value that Zenphoto requires, and 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 // sac: 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, MariaDB and the first boot service cannot reach the Azure Instance Metadata Service or the wire server. A separate zenphoto-netinfo.service, which runs no PHP, reads the VM's addresses for the certificate. The secrets are readable by root only:

systemctl show nginx php8.3-fpm mariadb zenphoto-firstboot -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 Id=nginx.service IPAddressDeny=169.254.169.254/32 168.63.129.16/32 Id=php8.3-fpm.service IPAddressDeny=169.254.169.254/32 168.63.129.16/32 Id=mariadb.service IPAddressDeny=169.254.169.254/32 168.63.129.16/32 Id=zenphoto-firstboot.service 600 root:root /root/zenphoto-credentials.txt 600 root:root /etc/ssl/zenphoto/zenphoto.key
9

Your own DNS name and certificate

Point a DNS record at the VM. A static public IP keeps the address stable. Then set the name Zenphoto is served as.

The command below mints a new self-signed certificate for the name. It also updates nginx, which hands the name to Zenphoto so its links and feeds use it, and the URLs in /root/zenphoto-credentials.txt. The name is kept 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
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 the password, run the helper on the VM. 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
11

Storage, backups and server components

Your albums, uploads, image cache and the MariaDB data are under /data on the OS disk and bind-mounted into place. A backup of /data (and the database dump below) therefore holds everything.

To give them more room, grow the OS disk in the Azure portal and restart the VM. Cloud-init grows the root file system at boot. Alternatively, attach a data disk, copy /data onto it with the services stopped, and mount it at /data.

Back up /data/zenphoto/albums (your originals) and the database (sudo mariadb-dump zenphoto > zenphoto.sql), or snapshot the OS disk. 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. In that case, follow these steps:

  • 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, then reload nginx.
  • Run setup from the backend, then reverse both steps.

The Run setup and restore scripts buttons in the backend do not work while the lock is in place.

Component paths on the image:

  • 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 folder: /data (on the OS disk).
  • 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/sac-zenphoto.
  • Certificate and key: /etc/ssl/zenphoto/.
  • First boot script: /usr/local/sbin/zenphoto-firstboot.sh.
  • Address lookup: /usr/local/sbin/zenphoto-netinfo (zenphoto-netinfo.service).
  • 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 licenses: /usr/share/doc/sac/.
12

Security recommendations

  • Allow port 22 in the network 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 upgrade applies 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 license, and htmLawed under LGPL 3 or GPL 2 or later. Their licenses are in /usr/share/doc/sac/ 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 license fee. The image price covers SecureAnyCloud's hardening, first boot setup and support.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com
  • Support: 24/7, with a 24 hour response SLA

Deploy on Azure

Launch ZenphotoCMS 1.6.8 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure