Deployment guide

SeedDMS 6.0.42

Overview

This guide covers launching and using the SeedDMS 6.0.42 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud file sharing and sync listing on AWS Marketplace. You choose 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. It gives you 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.

What you get:

  • 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. 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, with no TCP port. 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. An uploaded HTML or SVG file therefore cannot act as your account. The server works out the type SeedDMS stores for an upload from the file's content, never from the uploading browser.
  • PDF preview works; scripts embedded in PDFs are disabled. SeedDMS's built-in pdf.js viewer renders PDFs normally. 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. 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. The AMI requires IMDSv2.
  • Fully patched. Ubuntu 24.04 LTS is patched 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.
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, choose your Region, then choose Launch through EC2.

Select m5.large (or larger), your key pair, a security group as described above, and a subnet. Documents are stored on the root volume, so size it for your documents under Configure storage.

To launch with the AWS CLI instead, 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.

2

Connect to your instance

Connect over SSH with these values:

Version: SeedDMS 6.0.42 on Ubuntu 24.04; SSH user: ubuntu

ssh -i <your-key.pem> ubuntu@<public-ip>

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

3

Check the services

First boot initialises MariaDB, creates the SeedDMS database, generates the password and the keys, and issues the certificate. nginx is not allowed to start until it finishes. This usually takes 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.

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. This command shows 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://secureanycloud.com/stacks/seeddms-6-0-42/

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 = sac 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.

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.

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

The image's own check signs in through the real login form with the instance's password. It 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
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, and nginx and PHP are set to match:

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

Access rights on a folder or document control who can read, write or manage it. 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 to edit the file). The search box always searches names, comments and keywords.

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. You can name both users and groups in access rights, reviews and approvals:

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

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.

To see this from the instance itself, try 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, because the image has no mail server. Configure mail in the settings file if you want them.

8

Opening documents, PDF preview and the full preview

Documents are sandboxed. View online opens a document in the browser. Every document response, from the web interface, WebDAV or the REST API, carries a sandboxing Content-Security-Policy. An HTML, SVG or other active file therefore 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. That script can open dialogs, such as 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. If you trust the file and need them, download it and open it in a desktop PDF reader.

SecureAnyCloud checks this in real Google Chrome before each version is released. It also runs a control in which the browser is handed the original scripting engine and the PDF's script does run. Check it 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. A document page therefore shows the file's details and the View online and Download buttons, but no preview panel. This is SeedDMS's own default, and this image keeps it.

To show PDFs inside the document page, switch both settings on. The settings file is read-only to SeedDMS by design, so a compromised administrator session cannot change it. Edit it with sudo. The commands below keep a backup, add the two settings, check the file and reload 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. The same rule applies: the PDF renders, and its scripts do not run:

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.

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.

Use curl from your computer. The --netrc-file option 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"

This is a real run against the instance used for this guide, showing 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, such as file managers and rclone, 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 is at https://<host>/restapi/index.php/. Sign in once with POST /login (form fields user and pass), then use the session cookie it returns. GET /logout ends the session. This is a real run, showing 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. 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. This example sends 45 requests in a row with a wrong password:

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. 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. It needs one sign-in and then the session cookie, which is not counted as a password try. Alternatively, raise rate=30r/m for the sd_auth zone in /etc/nginx/conf.d/sac-seeddms.conf and burst=40 in /etc/nginx/sites-available/sac-seeddms, then sudo nginx -t && sudo systemctl reload nginx.

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, it is 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, because 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. The helper issues a new self-signed certificate for the name. It updates nginx, the cross-site guard, SeedDMS's baseUrl and the credentials file. It then reloads nginx and keeps the name in /etc/sac/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 chosen only 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. Connect over SSH with the new address and run sudo seeddms-set-hostname <new-public-dns-name> once. Alternatively, use an Elastic IP so that never happens.

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, such as 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. Running it again later replaces your certificate, so 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/sac-seeddms-headers.conf. The image leaves it out, because with a self-signed certificate it would stop browsers from ever reaching the site.

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, because it holds the key SeedDMS uses for its form tokens.

To restore on a new instance, follow these steps:

  • Load the dump into the seeddms database.
  • Unpack data into /var/lib/seeddms (owned by seeddms).
  • Copy the settings file back.
  • Run sudo seeddms-set-hostname <name> for the new address.

SecureAnyCloud support can help with the move.

Growing the disk. Increase the root volume's size in the EC2 console (or with 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.

13

Updates

SeedDMS is updated by new versions of this image, which SecureAnyCloud 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. It also 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. PHP cannot open remote URLs, cannot run the shell functions SeedDMS does not need, and cannot read outside SeedDMS's own directories.
  • Upload from a URL. 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. That feature therefore 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. This is a small, disclosed change to SeedDMS's source, described below. nginx also refuses the links SeedDMS uses to change something with a plain GET request (sign-out, clipboard, check-out, notifications, language and the Ajax commands) 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. Only root and SeedDMS can reach the database, 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 SecureAnyCloud 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, there is one source change: the session cookie flags, in five files. The rest of the differences are in how the image serves and ships SeedDMS:

  • nginx answers the pdf.js scripting engine with 404. The files on disk are unchanged.
  • nginx labels the viewer's .mjs modules text/javascript.
  • nginx 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.
  • SeedDMS's sample data (with the upstream default administrator), its development tools and the pdf.js distribution archives are not shipped.

Advisories that remain. SecureAnyCloud 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. That is 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

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). Port 80 redirects to HTTPS, unknown host names are dropped, and /healthz is available 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 it with sudo, then reload php8.3-fpm.

Credentials: /root/seeddms-credentials.txt, mode 0600, root only.

Helpers: seeddms-set-password, seeddms-set-hostname and seeddms-cred-roundtrip (in /usr/local/sbin).

First boot: seeddms-firstboot.service. It runs once, and 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

SecureAnyCloud provides 24/7 support for this image, the Ubuntu operating system and its deployment. Email sales@secureanycloud.com 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. SecureAnyCloud is not affiliated with, endorsed by or sponsored by the SeedDMS project.

Deploy on AWS

Launch SeedDMS 6.0.42 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS