Storage AWS

SeedDMS 6.0.42 on AWS User Guide

| Product: SeedDMS 6.0.42 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the SeedDMS 6.0.42 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.

SeedDMS is an open source (GPL-2.0-or-later) document management system written in PHP: a folder tree of documents with full version history, access control per folder and per document, review and approval workflows, categories and custom attributes, check-out and locking, expiry dates, notifications, a WebDAV endpoint and a REST API. This AMI runs the official SeedDMS 6.0.42 release with nginx, PHP 8.3 FPM and MariaDB, and adds the security work below.

  • HTTPS from the first boot. nginx serves SeedDMS on port 443 with a certificate generated on your instance's first boot (a server certificate, not a certificate authority). Port 80 redirects to HTTPS, and a request for a host name the instance does not serve is dropped.
  • No installer, no default login. SeedDMS's web install tool is never reachable, and the upstream admin / admin login never exists: first boot creates this instance's database and sets the administrator's password to a value generated on your instance. It also generates SeedDMS's form-token key. The image ships no database, no settings file and no secret.
  • No database password at all. MariaDB listens on its local socket only (no TCP port), and SeedDMS's PHP runs as its own system user, which MariaDB recognises over that socket.
  • The public address comes from configuration. The address SeedDMS puts in its links is set from EC2 instance metadata at first boot (or by you, with one command), never taken from a request.
  • Your documents cannot run code in your browser. Every document SeedDMS sends, whether opened online, downloaded, or read over WebDAV or the REST API, is served with a sandboxing Content-Security-Policy and nosniff, so an uploaded HTML or SVG file cannot act as your account. The type SeedDMS stores for an upload is worked out on the server from the file's content, never taken from the uploading browser.
  • PDF preview works; scripts embedded in PDFs are disabled. SeedDMS's built-in pdf.js viewer renders PDFs normally, but its scripting engine is not served, so JavaScript inside a PDF never runs.
  • Sign-in attempts are limited. Web logins are limited per client address, and so is every WebDAV and REST API request that carries a password (Basic authentication), so neither can be used to guess passwords at speed.
  • Fewer ways out. SeedDMS's "upload from a URL" feature, its outbound look-ups and extension downloads and imports are switched off; PHP runs only through SeedDMS's own entry points, as a user that cannot change the code, the settings or the extensions.
  • No service can reach the instance metadata service, and the AMI requires IMDSv2.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: SeedDMS 6.0.42 (the official seeddms-quickstart-6.0.42.tar.gz release, checked against the SHA-256 and MD5 values SourceForge publishes), PHP 8.3 FPM, MariaDB 10.11 and nginx 1.24 from Ubuntu, pdftotext for full-text extraction, 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 22 (SSH), TCP 443 (SeedDMS, WebDAV and the REST API) and TCP 80 (redirects to HTTPS) from the networks your users use. Nothing else needs to be open.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick SeedDMS 6.0.42 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (or larger), your key pair, a security group as above, and a subnet. Documents are stored on the root volume, so give it the size your documents need under Configure storage.

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> \
  --block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=50,VolumeType=gp3}' \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=seeddms}]'

SeedDMS is served under one name (Step 10). For an address that never changes, attach an Elastic IP (and ideally your own DNS name) before you hand the address to your users.

Step 2: Connect to your instance

Version SSH user
SeedDMS 6.0.42 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

The login message shows where the address and the administrator password are.

Step 3: Check the services

First boot initialises MariaDB, creates the SeedDMS database, generates the password and the keys, and issues the certificate before nginx is allowed to start. It usually finishes within a minute or two:

systemctl is-active nginx php8.3-fpm mariadb
active
active
active

Only SSH (22) and nginx (80, 443) listen on the network. MariaDB has no TCP listener at all, and PHP is reached through a local socket; the two port 53 entries are Ubuntu's local DNS resolver:

sudo ss -lntp | awk 'NR>1 {print $4, $6}' | sed -E 's/users:\(\("([^"]+)".*/\1/' | sort -u
0.0.0.0:22 sshd
0.0.0.0:443 nginx
0.0.0.0:80 nginx
127.0.0.53%lo:53 systemd-resolve
127.0.0.54:53 systemd-resolve
[::]:22 sshd
[::]:443 nginx
[::]:80 nginx

If nginx is not active, first boot has not finished, or it failed and left the site down rather than open. Check systemctl status seeddms-firstboot and journalctl -u seeddms-firstboot.

Step 4: Your address and the administrator password

The web address, the administrator login and its password are in /root/seeddms-credentials.txt, which only root can read. Everything except the password line:

sudo grep -v '^SEEDDMS_ADMIN_PASSWORD=' /root/seeddms-credentials.txt
# SeedDMS 6.0.42 - generated on first boot by seeddms-firstboot.service
# These credentials are unique to this instance; none was baked into the image. Keep this file secret.

SEEDDMS_URL=https://<public-dns-name>/
SEEDDMS_ADMIN_USER=admin
SEEDDMS_WEBDAV_URL=https://<public-dns-name>/webdav/index.php/
SEEDDMS_RESTAPI_URL=https://<public-dns-name>/restapi/index.php/
#
# Open SEEDDMS_URL in a browser (self-signed certificate minted on this instance's first boot; port 80 redirects to
# https) and sign in as 'admin' with the password above. Set the administrator's e-mail in My Account.
# MariaDB: unix socket only (no TCP listener, no database password: SeedDMS's PHP runs as the OS user seeddms);
#          sudo mysql seeddms   (root signs in over the socket)
# Settings file: /var/www/seeddms/conf/settings.xml (root-owned; edit with sudo, then: sudo systemctl reload php8.3-fpm)
# Change the password:            sudo seeddms-set-password
# Change the name it serves as:   sudo seeddms-set-hostname <your-dns-name>
# User guide: https://www.cloudimg.co.uk/guides/seeddms-6-0-42-on-ubuntu-24-04-aws/

Your instance shows its own public DNS name. To show the password itself (24 letters and digits, unique to your instance):

sudo grep '^SEEDDMS_ADMIN_PASSWORD=' /root/seeddms-credentials.txt | cut -d= -f2-

The certificate is self-signed for this instance only. Your browser warns about it until you install a certificate from a public authority (Step 11); before you continue past the warning, compare the fingerprint your browser shows with the one the instance serves:

H=$(sed -n 's/^set \$sd_host "\(.*\)";$/\1/p' /etc/nginx/seeddms-host.conf)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -fingerprint -sha256
sudo openssl x509 -in /etc/nginx/ssl/seeddms.crt -noout -subject -enddate -ext basicConstraints,extendedKeyUsage
sha256 Fingerprint=21:0A:F9:EC:E4:81:58:F0:6A:D7:87:A9:93:45:4A:BC:35:48:CF:EC:82:6A:A9:7B:05:BE:8A:5E:D1:36:A3:06
subject=CN = <public-dns-name>, O = cloudimg
notAfter=Feb  2 17:45:34 2029 GMT
X509v3 Basic Constraints: critical
    CA:FALSE
X509v3 Extended Key Usage: 
    TLS Web Server Authentication

Your instance shows its own fingerprint and dates.

Step 5: Sign in

Open https://<public-dns-name>/ (the SEEDDMS_URL from Step 4) and sign in with user ID admin and the password from the file.

SeedDMS sign-in page

The folder view opens on the root folder, DMS. The folder tree is on the left, the folder's details and contents in the middle, and the Fast upload area on the right takes files dragged onto it. Here a folder, Contracts, holds three documents:

A folder with three documents

The image's own check signs in through the real login form with the instance's password, and proves that a wrong password and the upstream default admin are refused, that port 80 redirects to HTTPS and that the installer, the settings file, PHP include files and the PDF scripting engine are not served:

sudo seeddms-cred-roundtrip
OK https 301 + no installer/settings/includes/pdf.js sandbox + administrator sign-in + wrong and default passwords refused

Only two accounts exist on a new instance, admin (role 1, administrator) and SeedDMS's built-in guest account, which is disabled (disabled 1). No account has the password admin:

sudo mariadb -N -B seeddms -e "SELECT id, login, role, disabled FROM tblUsers ORDER BY id"
sudo mariadb -N -B seeddms -e "SELECT COUNT(*) AS accounts_with_password_admin FROM tblUsers WHERE pwd = MD5('admin')"
1   admin   1   0
2   guest   2   1
0

Step 6: Folders, documents and versions

In a folder, Add subfolder builds your structure and Add document uploads a file with its name, comment, keywords, categories, expiry date, reviewers and approvers. One file can be up to 256 MiB (nginx and PHP are set to match):

The Add document form

Click a document to open its page: its details, the current version with Download and View online, its status and workflow history. Update document adds a new version, and every earlier version is kept:

A document page

Access rights on a folder or document control who can read, write or manage it, and folders pass their rights down unless you change them. SeedDMS's full-text index is off by default (enableFullSearch in the settings file, Step 8 shows how the file is edited); the search box always searches names, comments and keywords.

Step 7: Users, the administrator password and sign-in limits

Users. Create an account for each person under Admin tools > Users/Groups management > Users. The form preselects the Admin role, so choose User for everyone who should not administer SeedDMS. Groups are on the same menu, and both can be named in access rights, reviews and approvals:

Users management

Change the administrator password. Run the helper; it generates a new random password, stores it with SeedDMS's own scheme, signs every session of the admin account out and writes the new password to the credentials file (read it as in Step 4):

sudo seeddms-set-password
seeddms-set-password: password updated for 'admin' (see /root/seeddms-credentials.txt)

To choose the password yourself, pipe it in instead (24 to 72 letters and digits); it never appears on a command line: printf '%s\n' "$NEW_PASSWORD" | sudo seeddms-set-password --stdin. Users can also change their own password under My Account.

Sign-in limits. nginx allows ten web sign-in attempts a minute from one address (with a burst of ten); after that it answers 429 Too Many Requests until the minute has passed. From the instance itself, with an account that does not exist (SeedDMS answers each attempt with a redirect back to the sign-in page):

H=$(sed -n 's/^set \$sd_host "\(.*\)";$/\1/p' /etc/nginx/seeddms-host.conf)
for i in $(seq 1 13); do
  curl -s -o /dev/null -w '%{http_code} ' --resolve "$H:443:127.0.0.1" --cacert /etc/nginx/ssl/seeddms.crt \
    --data-urlencode 'login=no-such-user' --data-urlencode 'pwd=not-a-real-password' "https://$H/op/op.Login.php"
done; echo
302 302 302 302 302 302 302 302 302 302 302 429 429 

SeedDMS's own lock-out after failed logins stays off on this image, because it would lock any account, the administrator included, after a few wrong tries by anyone; the per-address limit above slows guessing without letting a stranger lock you out. SeedDMS's "forgot password" e-mail, notifications by e-mail and guest login are off (the image has no mail server); configure mail in the settings file if you want them.

Step 8: Opening documents, PDF preview and the full preview

Documents are sandboxed. View online opens a document in the browser, but every document response, from the web interface, WebDAV or the REST API, carries a sandboxing Content-Security-Policy, so an HTML, SVG or other active file opens with no access to SeedDMS or your session. The one exception is a response whose type is exactly application/pdf, which the browser's own PDF viewer shows. SeedDMS also never offers HTML, HTM or XML files for viewing online; they are downloaded.

PDF preview works; scripts embedded in PDFs are disabled. SeedDMS includes the pdf.js 5.4.449 viewer, which renders PDFs in the page. A PDF can also carry JavaScript, which pdf.js would run in its scripting engine: it can open dialogs (for example a fake password prompt) for whoever views the file. On this image nginx answers the scripting engine with 404 Not Found, so a PDF's JavaScript never runs, while the viewer itself loads normally. Forms still display, but their scripts (calculations, validation) and PDF actions do nothing in the preview; download the file and open it in a desktop PDF reader if you trust it and need them. cloudimg checks this in real Google Chrome before each version is released, together with a control run in which the browser is handed the original scripting engine and the PDF's script does run. Checked from the instance:

H=$(sed -n 's/^set \$sd_host "\(.*\)";$/\1/p' /etc/nginx/seeddms-host.conf)
for p in /ext/pdfviewer/res/build/pdf.sandbox.mjs /ext/pdfviewer/res/build/pdf.mjs /ext/pdfviewer/res/build/pdf.worker.mjs /ext/pdfviewer/res/web/viewer.mjs; do
  echo "$(curl -s -o /dev/null -w '%{http_code} %{content_type}' --resolve "$H:443:127.0.0.1" --cacert /etc/nginx/ssl/seeddms.crt "https://$H$p") $p"
done
404 text/html /ext/pdfviewer/res/build/pdf.sandbox.mjs
200 text/javascript /ext/pdfviewer/res/build/pdf.mjs
200 text/javascript /ext/pdfviewer/res/build/pdf.worker.mjs
200 text/javascript /ext/pdfviewer/res/web/viewer.mjs

The first is the scripting engine; the other three are the viewer, which loads. (Ubuntu's nginx has no type for pdf.js's .mjs files, and a browser refuses a module served as application/octet-stream, so the image labels them text/javascript.)

The full preview is off by default. SeedDMS ships with "Show full preview" switched off and its pdfviewer extension not enabled, so a document page shows the file's details and the View online and Download buttons, but no preview panel. That is SeedDMS's own default and this image keeps it. To show PDFs inside the document page, switch both on. The settings file is read-only to SeedDMS by design (a compromised administrator session cannot change it), so edit it with sudo; this keeps a backup, adds the two settings, checks the file and reloads PHP:

sudo cp -p /var/www/seeddms/conf/settings.xml /root/settings.xml.before-preview
sudo sed -i -e '0,/<display /s//<display showFullPreview="true" /' \
  -e 's|<extensions>|<extensions><extension name="pdfviewer" disable="false"/>|' /var/www/seeddms/conf/settings.xml
sudo php -r 'exit(simplexml_load_file("/var/www/seeddms/conf/settings.xml") === false ? 1 : 0);' && sudo systemctl reload php8.3-fpm

A PDF's page then shows a Preview panel with the pdf.js viewer (same rule: the PDF renders, its scripts do not run):

A document page with the full preview switched on

To switch it off again, copy the backup back (sudo cp -p /root/settings.xml.before-preview /var/www/seeddms/conf/settings.xml) and run sudo systemctl reload php8.3-fpm.

Step 9: WebDAV and the REST API

WebDAV. The whole folder tree is available at https://<host>/webdav/index.php/ (the SEEDDMS_WEBDAV_URL from Step 4) with a SeedDMS login. A folder appears as a directory and each document as a file (its latest version); uploading a file into a folder creates a document. With curl from your computer (--netrc-file keeps the password off the command line; point --cacert at the instance's certificate after comparing its fingerprint, or leave it out once a public certificate is installed):

printf 'machine <host> login admin password %s\n' '<password>' > ~/.seeddms.netrc && chmod 600 ~/.seeddms.netrc
D=https://<host>/webdav/index.php
curl --netrc-file ~/.seeddms.netrc -X MKCOL "$D/Reports"
curl --netrc-file ~/.seeddms.netrc -T q3-summary.txt "$D/Reports/q3-summary.txt"
curl --netrc-file ~/.seeddms.netrc -X PROPFIND -H 'Depth: 1' "$D/Reports/"
curl --netrc-file ~/.seeddms.netrc -o q3-copy.txt "$D/Reports/q3-summary.txt"

A real run against the instance used for this guide (status codes only; the folder and file were then deleted the same way, with -X DELETE):

PROPFIND / -> 207
MKCOL /Reports -> 201
PUT /Reports/q3-summary.txt -> 201
PROPFIND /Reports -> 207   (lists /webdav/index.php/Reports/q3-summary.txt)
GET -> 200                 (content identical; served text/plain with the sandboxing policy)
DELETE file -> 204
DELETE folder -> 204
PROPFIND /Reports after -> 404

Other WebDAV clients (file managers, rclone and similar tools) connect the same way. They check the certificate, so install one from a public authority (Step 11) before you set up desktop clients.

REST API. The API at https://<host>/restapi/index.php/ signs in once with POST /login (form fields user and pass) and then uses the session cookie it returns; GET /logout ends the session. A real run (status codes and the account's login only):

POST /login -> 200 (success true, login admin)
GET /account -> 200 (login admin, role Admin)
GET /folder/1/children -> 200
GET /logout -> 200
GET /account after logout -> 403

Limits and policy. Without credentials both answer with a refusal, and every response from them, error pages included, carries the sandboxing policy:

H=$(sed -n 's/^set \$sd_host "\(.*\)";$/\1/p' /etc/nginx/seeddms-host.conf)
echo "WebDAV without credentials -> $(curl -s -o /dev/null -w '%{http_code}' -X PROPFIND --resolve "$H:443:127.0.0.1" --cacert /etc/nginx/ssl/seeddms.crt "https://$H/webdav/index.php/")"
echo "REST API account without a session -> $(curl -s -o /dev/null -w '%{http_code}' --resolve "$H:443:127.0.0.1" --cacert /etc/nginx/ssl/seeddms.crt "https://$H/restapi/index.php/account")"
curl -s -o /dev/null -D - --resolve "$H:443:127.0.0.1" --cacert /etc/nginx/ssl/seeddms.crt "https://$H/restapi/index.php/document/1/content" | grep -i -e '^HTTP' -e '^content-security-policy' -e '^x-content-type'
WebDAV without credentials -> 401
REST API account without a session -> 403
HTTP/2 403 
x-content-type-options: nosniff
content-security-policy: sandbox; default-src 'none'; img-src 'self' data:; media-src 'self'; style-src 'unsafe-inline'

WebDAV clients send the password with every request (Basic authentication), so each of those requests is a password try: nginx allows 30 a minute from one address, with a burst of 40, and then answers 429 until the rate falls. With a wrong password, 45 requests in a row:

H=$(sed -n 's/^set \$sd_host "\(.*\)";$/\1/p' /etc/nginx/seeddms-host.conf)
for i in $(seq 1 45); do
  curl -s -o /dev/null -w '%{http_code}\n' -X PROPFIND -u 'no-such-user:not-a-real-password' --resolve "$H:443:127.0.0.1" --cacert /etc/nginx/ssl/seeddms.crt "https://$H/webdav/index.php/"
done | sort | uniq -c
     41 401
      4 429

The same limit applies to a correct password, so a WebDAV client copying hundreds of small files is slowed to about one request every two seconds once its burst is used. For bulk transfers, use the REST API (one sign-in, then the session cookie, which is not counted as a password try), or raise rate=30r/m for the sd_auth zone in /etc/nginx/conf.d/cloudimg-seeddms.conf and burst=40 in /etc/nginx/sites-available/cloudimg-seeddms, then sudo nginx -t && sudo systemctl reload nginx.

Step 10: Host name and public address

SeedDMS is served under one name, and every link it builds uses that name. On first boot it is the instance's public DNS name from EC2 instance metadata (IMDSv2); without one, the public IP address, then the private one. The name is written to nginx (/etc/nginx/seeddms-host.conf) and to SeedDMS's baseUrl:

sed -n 's/^set \$sd_host "\(.*\)";$/\1/p' /etc/nginx/seeddms-host.conf
sudo grep -o 'baseUrl="[^"]*"' /var/www/seeddms/conf/settings.xml
<public-dns-name>
baseUrl="https://<public-dns-name>"

The instance's own IP addresses also reach SeedDMS over HTTPS (the certificate names them), port 80 redirects to the configured name, and any other host name is dropped without an answer (curl reports 000):

P=$(hostname -I | awk '{print $1}')
echo "private IP, https -> $(curl -sk -o /dev/null -w '%{http_code}' "https://$P/")"
echo "private IP, http  -> $(curl -s -o /dev/null -w '%{http_code}' "http://$P/")"
curl -sk -o /dev/null -w 'unknown host -> %{http_code}' -H 'Host: unknown.example.com' https://127.0.0.1/; echo " (curl exit code $?)"
private IP, https -> 302
private IP, http  -> 301
unknown host -> 000 (curl exit code 92)

(The 302 is SeedDMS's own redirect from its start page to the folder view.)

Use your own name, or follow a new address. Point a DNS record at the instance (with an Elastic IP), then run the helper with the name. It issues a new self-signed certificate for the name, updates nginx, the cross-site guard and SeedDMS's baseUrl and the credentials file, reloads nginx and keeps the name in /etc/cloudimg/seeddms-hostname:

sudo seeddms-set-hostname <your-domain>
seeddms-set-hostname: SeedDMS is served at https://<your-domain>/ (certificate SHA-256 <fingerprint>)

The name is only chosen on first boot and when you run the helper; it is not re-read on later boots. If you stop and start an instance without an Elastic IP, it gets a new public address and DNS name that nginx does not answer for: run sudo seeddms-set-hostname <new-public-dns-name> once (connect over SSH with the new address), or use an Elastic IP so that never happens.

Step 11: The TLS certificate

nginx serves /etc/nginx/ssl/seeddms.crt (the full chain) and /etc/nginx/ssl/seeddms.key. The first-boot certificate is valid for 825 days. To use a certificate from a public authority (for example Let's Encrypt with certbot, or one you bought), install it in place of those two files as root and reload nginx:

sudo install -m 0644 -o root -g root fullchain.pem /etc/nginx/ssl/seeddms.crt
sudo install -m 0600 -o root -g root privkey.pem /etc/nginx/ssl/seeddms.key
sudo nginx -t && sudo systemctl reload nginx

Run seeddms-set-hostname first, then install your certificate. The helper always issues a new self-signed certificate into those two files, so running it again later replaces your certificate; install yours again afterwards. Nothing else on the image touches the files, including reboots. Once a public certificate is installed you can also add a Strict-Transport-Security header in /etc/nginx/snippets/cloudimg-seeddms-headers.conf (the image leaves it out, because with a self-signed certificate it would stop browsers from ever reaching the site).

Step 12: Storage and backups

SeedDMS keeps the documents and its working data in /var/lib/seeddms, outside the web root and owned by the seeddms service user; the code under /var/www/seeddms belongs to root and the settings directory is readable by SeedDMS but not writable:

sudo ls /var/lib/seeddms
sudo stat -c '%U:%G %a %n' /var/lib/seeddms/data /var/www/seeddms/conf /var/www/seeddms/www
backup
cache
data
lucene
staging
seeddms:seeddms 750 /var/lib/seeddms/data
root:seeddms 750 /var/www/seeddms/conf
root:root 755 /var/www/seeddms/www

data holds every document version, lucene the full-text index, staging uploads in progress, cache previews and backup SeedDMS's own backups (Admin tools > Backup/Logging). The MariaDB database (seeddms) is in /var/lib/mysql.

Backups. The simplest complete backup is an EBS snapshot of the instance's volume (or AWS Backup on the instance). For a file-level backup, dump the database and copy the documents and the settings file, for example to /root/backup (root reaches MariaDB through its socket, so no password is needed):

sudo install -d -m 0700 /root/backup
sudo mariadb-dump --single-transaction --routines --triggers seeddms | gzip | sudo tee /root/backup/seeddms-$(date +%F).sql.gz > /dev/null
sudo tar -C /var/lib/seeddms -czf /root/backup/seeddms-data-$(date +%F).tar.gz data
sudo cp -p /var/www/seeddms/conf/settings.xml /root/backup/settings-$(date +%F).xml

Keep settings.xml with the backups and keep it private: it holds the key SeedDMS uses for its form tokens. To restore on a new instance, load the dump into the seeddms database, unpack data into /var/lib/seeddms (owned by seeddms), copy the settings file back and run sudo seeddms-set-hostname <name> for the new address. cloudimg support can help with the move.

Growing the disk. Increase the root volume's size in the EC2 console (or aws ec2 modify-volume); Ubuntu grows the partition and file system on the next boot, or at once with sudo growpart /dev/nvme0n1 1 && sudo resize2fs /dev/nvme0n1p1.

Step 13: Updates

SeedDMS is updated by new versions of this image, which cloudimg publishes after the SeedDMS project releases. To move to one, launch it alongside your instance and bring the database, documents and settings across from a backup (Step 12). Every service is enabled, so SeedDMS returns by itself after a reboot.

Ubuntu, PHP, MariaDB and nginx come from Ubuntu's archive and unattended upgrades install their security updates automatically. The PHP build on the image includes the fixes for CVE-2026-109618 and CVE-2026-109619:

systemctl is-enabled unattended-upgrades
dpkg-query -W php8.3-fpm mariadb-server nginx
enabled
mariadb-server  1:10.11.14-0ubuntu0.24.04.1
nginx   1.24.0-2ubuntu7.19
php8.3-fpm  8.3.6-0ubuntu0.24.04.13

Security notes

  • Listeners. Only SSH and nginx are reachable from the network (Step 3). nginx replaces any X-Forwarded-For, X-Real-IP, Client-IP or Forwarded header a client sends, so SeedDMS logs the real connection address, and it hands SeedDMS the configured host name, never the one in the request.
  • Instance metadata. nginx, PHP, MariaDB and the first-boot service are blocked from the EC2 instance metadata service by systemd, and the AMI requires IMDSv2:
systemctl show -p IPAddressDeny nginx php8.3-fpm mariadb
IPAddressDeny=168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128

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

IPAddressDeny=168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
  • PHP. SeedDMS's PHP runs in its own pool as the seeddms user, only for SeedDMS's entry points (index.php, op/op.*.php, out/out.*.php, the REST API and WebDAV); every other PHP file, the installer, the settings and include files answer 404. It cannot open remote URLs, cannot run the shell functions SeedDMS does not need, and cannot read outside SeedDMS's own directories. curl_exec is disabled because SeedDMS's "upload from a URL" would fetch any address a signed-in user supplied (including internal ones and local files), so that feature reports an error on this image; upload files from your computer instead:
grep -E '^php_admin_(value|flag)\[(open_basedir|disable_functions|allow_url_fopen)\]' /etc/php/8.3/fpm/pool.d/seeddms.conf
php_admin_value[open_basedir] = /var/www/seeddms/:/var/lib/seeddms/:/tmp/
php_admin_value[disable_functions] = exec,passthru,shell_exec,popen,pcntl_exec,curl_exec,curl_multi_exec,dl
php_admin_flag[allow_url_fopen] = off
  • Session cookie. SeedDMS's session cookie is sent with Secure, HttpOnly and SameSite=Lax (a small, disclosed change to SeedDMS's source, below), and the links SeedDMS uses to change something with a plain GET request (sign-out, clipboard, check-out, notifications, language and the Ajax commands) are refused by nginx when another web site triggers them.
  • Extensions. Installing extensions from the web interface is off and the extension directory belongs to root: a SeedDMS extension is PHP code, so an administrator account must not be able to add one (CVE-2025-45752).
  • Passwords. SeedDMS stores passwords as MD5 hashes, its own long-standing scheme. The database is reachable only by root and SeedDMS over the local socket; use long, unique passwords (the generated administrator password is 24 random letters and digits).
  • Restrict 22/tcp to your management network and 80/443 to the networks your users need.

What cloudimg changed, and the advisories that remain

The image records exactly what differs from the SeedDMS release in /usr/share/doc/seeddms/SOURCE.txt, including the SHA-256 of every changed file before and after:

sed -n '/^Source patch/,/^Served differently/p' /usr/share/doc/seeddms/SOURCE.txt | sed '$d' | head -3
Source patch (session cookie): every setcookie("mydms_session", ...) call (13 sites) uses the options form with
secure, httponly and samesite=Lax. Files (sha256 before -> after):
b575164e40f1d72b09605d983488c564699e3c81c213f70a7df050d1e9392d1c  inc/inc.ClassAuthenticationMiddleware.php

In short: one source change (the session cookie flags, in five files); as served, nginx answers the pdf.js scripting engine with 404 (the files on disk are unchanged), labels the viewer's .mjs modules text/javascript, and never serves the legacy bootstrap theme's old libraries (jQuery 1.12.4, jquery-validation 1.19.2, bootbox 2.5.1) or the style files the current themes do not use; the theme selector is off; PHP's remote URL access and curl_exec are off; and SeedDMS's sample data (with the upstream default administrator), its development tools and the pdf.js distribution archives are not shipped.

Advisories that remain. cloudimg checks every bundled PHP package against the published advisory databases before each release: none applies to the versions shipped. One browser library carries an advisory that cannot be reached: moment 2.30.1 (the date library the bootstrap4 and bootstrap5 themes load) is listed for CVE-2026-17495, a path traversal in moment's Node.js locale loading (require()), which only exists when moment runs under Node.js on a server. SeedDMS only sends moment to the browser, where that code path does not exist. Older copies (moment 2.29.1 in the legacy theme, 2.17.1 under styles/) are on disk but never served:

H=$(sed -n 's/^set \$sd_host "\(.*\)";$/\1/p' /etc/nginx/seeddms-host.conf)
for p in /views/bootstrap4/vendors/moment/moment.min.js /views/bootstrap/vendors/moment/moment.min.js /styles/bootstrap/fullcalendar/lib/moment.min.js /views/bootstrap/vendors/jquery/jquery.min.js; do
  echo "$(curl -s -o /dev/null -w '%{http_code}' --resolve "$H:443:127.0.0.1" --cacert /etc/nginx/ssl/seeddms.crt "https://$H$p") $p"
done
200 /views/bootstrap4/vendors/moment/moment.min.js
404 /views/bootstrap/vendors/moment/moment.min.js
404 /styles/bootstrap/fullcalendar/lib/moment.min.js
404 /views/bootstrap/vendors/jquery/jquery.min.js

Architecture summary

Component Detail
Application SeedDMS 6.0.42 (GPL-2.0-or-later), code in /var/www/seeddms (root-owned), PHP 8.3 FPM pool seeddms on /run/php/seeddms-fpm.sock
Database MariaDB 10.11 (Ubuntu), local socket only (no TCP), database seeddms, SeedDMS signs in as the seeddms system user (no password)
Web server nginx on 443 (HTTPS, per-instance certificate in /etc/nginx/ssl), 80 redirects to HTTPS, unknown host names dropped, /healthz for load balancer checks
Documents /var/lib/seeddms (data, lucene, staging, cache, backup), owned by seeddms
Settings /var/www/seeddms/conf/settings.xml (0640 root:seeddms; written at first boot; edit with sudo, then reload php8.3-fpm)
Credentials /root/seeddms-credentials.txt, mode 0600, root only
Helpers seeddms-set-password, seeddms-set-hostname, seeddms-cred-roundtrip (in /usr/local/sbin)
First boot seeddms-firstboot.service (runs once; nginx starts only after it has finished)
Ports 22 SSH, 80 redirect, 443 SeedDMS (web, WebDAV, REST API)
Base image Ubuntu 24.04 LTS, fully patched, unattended security upgrades enabled, IMDSv2 required

Support

cloudimg provides 24/7 support for this image, the Ubuntu operating system and its deployment. Email support@cloudimg.co.uk with your AWS account ID and the instance ID. For SeedDMS itself, see the SeedDMS website and the SeedDMS project on SourceForge. SeedDMS is a trademark of its owner; cloudimg is not affiliated with, endorsed by or sponsored by the SeedDMS project.