Cc
Applications AWS

Concrete CMS 9.5.7 on AWS User Guide

| Product: Concrete CMS 9.5.7 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Concrete CMS 9.5.7 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg BookStack, CMS, Blogs and Wikis listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

Concrete CMS (formerly concrete5) is a free and open source content management system known for in-context editing: signed-in editors change a page on the page itself, adding and arranging blocks of content, while the Dashboard manages the sitemap, files, users, permissions, forms, calendars and themes. The image starts with Concrete's own Elemental sample site (pages, a blog, a portfolio and sample images) for you to edit or replace.

The cloudimg image adds:

  • No web installer, no shared database. Concrete's web installer creates the database connection and the first administrator for whoever opens it first. This image ships no database at all: the MariaDB data directory is empty in the image, and there is no database.php. Your instance's first boot initialises its own database and runs Concrete's own command line installer (c5:install) before the web server starts, which creates the schema, the sample site, the super user admin with a password generated on your instance, and the configuration.
  • Secrets of your instance's own. The database password, the admin password, Concrete's three security tokens (they protect scheduled jobs, encrypted values and validation links) and, when you switch the REST API on, its RSA key pair are all generated on your instance. Nothing from the image build is reused. No secret is ever passed on a command line.
  • The current release. Concrete CMS 9.5.7 is the newest stable release. It includes the security fixes of 9.5.3 to 9.5.5 (more than sixty CVEs, several reachable without signing in) and updated bundled libraries; the download is verified against a pinned checksum.
  • No calls home. Concrete normally connects a new site to its marketplace, checks concretecms.com for updates, and fetches news, tutorials and background images from concretecms.com servers. On this image the server makes none of those calls (Step 7 shows each switch), and the geoPlugin visitor geolocation lookup is off. The Elemental theme loads its web font from Google Fonts and the Dashboard loads Google Charts in your browser.
  • Closed by default. Public registration is off, e-mail is off until you configure a mail server, and error details are never shown to visitors.
  • HTTPS from the first boot. nginx serves Concrete on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
  • Fail closed. nginx does not start until first boot has finished; if first boot cannot complete, the site stays offline rather than open.
  • Only the front controller runs PHP. Every page goes through Concrete's index.php. Its configuration (application/config), the command line tool, the bundled libraries (concrete/vendor), the update folder, add-on code and composer files are never served, and no other PHP file can be called. Uploaded files in application/files are served as static files only: every one with nosniff and a sandboxing content security policy, and only passive types (images, PDF, fonts, audio and video) open in the browser; anything else, SVG and HTML included, is sent as a download.
  • A rate-limited login. nginx limits sign in and registration requests to 6 a minute from one address after a burst of 10, and Concrete bans an address for ten minutes after five failed sign ins in five minutes. Both use the address of the connection, never a header a client sends.
  • Hardened defaults. The Concrete code belongs to root and is read only to PHP (only application/files and application/config are writable); PHP cannot run shell commands and is confined to Concrete's folders. MariaDB has no network listener at all (Concrete talks to it over a local socket) and its root account signs in only through the socket as the operating system's root. Sessions are kept in the database. nginx, PHP and MariaDB are blocked from the instance metadata service, the AMI requires IMDSv2, requests for a host name the instance does not serve are dropped, and Concrete always uses the address you configure (its canonical URL), never one a request claims.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Concrete CMS 9.5.7 (the official release, checksum verified, Elemental starting point), MariaDB 10.11, PHP 8.3 FPM, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. The database, uploads and Concrete's configuration are on a separate 30 GiB EBS volume mounted at /var/lib/concrete.

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 (Concrete CMS) and TCP 80 (redirects to HTTPS) from the networks that should reach them. Keep 22 to your own networks. Open 443 to your visitors for a public site, or only to your team while you build it.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Concrete CMS 9.5.7 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. Keep the second volume (the database, uploads and configuration) in the launch settings; you can make it larger there.

With the AWS CLI (replace the values in angle brackets):

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=concrete-cms}]'

Step 2: Connect to your instance

Version SSH user
Concrete CMS 9.5.7 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

First boot takes about a minute (it creates the database and runs Concrete's installer with the sample site). Then nginx, PHP, MariaDB and the first boot unit are active:

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

nginx serves ports 80 and 443; PHP and MariaDB listen on local sockets only, so the only open ports are 22, 80 and 443:

sudo ss -tlnp | awk 'NR==1 || /nginx|sshd|php|maria/' | 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 [::]:22 users:(("sshd"),("systemd"))
LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))

The Concrete release, the PHP and MariaDB versions and the data volume:

grep "'version' =>" /var/www/concrete/concrete/config/concrete.php | head -1
php -v | head -1
mariadb --version
findmnt -no TARGET,FSTYPE,SIZE /var/lib/concrete
    'version' => '9.5.7',
PHP 8.3.6 (cli) (built: Oct 19 2026 15:16:58) (NTS)
mariadb  Ver 15.1 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64) using  EditLine wrapper
/var/lib/concrete ext4   29.4G

Step 4: Get your sign-in details

The credentials file holds the address Concrete is served as (your instance's public DNS name by default), the sign in page, the administrator login and password, and the database credentials, all generated on this instance. Show it with:

sudo cat /root/concrete-cms-credentials.txt
CONCRETE_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
CONCRETE_LOGIN_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/login
CONCRETE_ADMIN_USER=admin
CONCRETE_ADMIN_PASSWORD=<24 letters and digits>
CONCRETE_DB_NAME=concrete
CONCRETE_DB_USER=concrete
CONCRETE_DB_PASSWORD=<48 hexadecimal characters>

Step 5: Visit the site and sign in

Open CONCRETE_URL in your browser. The certificate is self signed (generated on your instance), so accept the browser warning once, or install your own certificate (Step 11). The Elemental sample site appears:

The Elemental sample site home page with its image slider and three feature columns

Open CONCRETE_LOGIN_URL and sign in with the user name admin and CONCRETE_ADMIN_PASSWORD:

The Concrete CMS sign in page with the User Name and Password fields

admin is Concrete's super user, the one account that can always do everything. On the first sign in Concrete offers a short survey ("Tell us a little about your site") and a "learn the basics" panel; choose Skip and Got It! to close them. With the marketplace connection off, survey answers stay on your instance. Create a separate account for each editor under Dashboard > Members > Add User and keep admin for administration.

Step 6: Edit pages in place

Signed in, every page shows the Concrete toolbar. Choose the pencil to put the page in edit mode: each area and block is outlined, and you click a block to edit it, or drag a new one from the Add panel (the plus icon). Publish Changes makes the new version live; every page keeps its full version history.

A page in edit mode with the sitewide header, the image slider and the three columns outlined as editable blocks

The Dashboard (the sliders icon at the top right) manages the whole site:

The Concrete Dashboard Welcome page with the latest form and site health tiles

Sitemap lists every page; drag pages to move them, and add new pages from a page's menu:

The Full Sitemap with the Elemental pages Services, Portfolio, Team, Blog, Search and Contact

Files > File Manager holds every uploaded image and document, with thumbnails, sets and folders:

The File Manager listing the sample site's images

Concrete's command line tool runs from /var/www/concrete as the web server user. c5:info summarises the installation:

cd /var/www/concrete && sudo -u www-data php concrete/bin/concrete c5:info | sed -n '/# Version/,/^$/p'
# Version
Installed - Yes
Core Version - 9.5.7
Version Installed - 9.5.7
Database Version - 20261020120000

Step 7: The closed-by-default settings

The image's settings are written into Concrete's own configuration store (/var/lib/concrete/config/generated_overrides), the same place the Dashboard writes, so you can change each one in the Dashboard or from the shell. The settings in effect, as Concrete reads them:

cd /var/www/concrete && for k in concrete.security.production.mode concrete.user.registration.enabled concrete.marketplace.enabled concrete.updates.skip_core concrete.updates.services.get_available_updates concrete.urls.activity_slots concrete.white_label.background_image concrete.email.enabled concrete.session.handler; do printf '%s = %s\n' "$k" "$(sudo -u www-data php concrete/bin/concrete c5:config get "$k")"; done
concrete.security.production.mode = production
concrete.user.registration.enabled = false
concrete.marketplace.enabled = false
concrete.updates.skip_core = true
concrete.updates.services.get_available_updates = 
concrete.urls.activity_slots = 
concrete.white_label.background_image = none
concrete.email.enabled = false
concrete.session.handler = database

What each switch does, and how to turn it back on:

  • Public registration is off (Dashboard > System & Settings > Login & Registration > Account Options). Create accounts under Dashboard > Members > Add User; if you open registration to the internet, require validation and a CAPTCHA first.
  • Marketplace and updates. The marketplace connection, the add-on update checks and the core update check are off, and the update service address is empty, so the server never contacts concretecms.com. Upgrade from the shell instead (Step 12). To connect the site to the marketplace, set concrete.marketplace.enabled to true and restore concrete.updates.services.get_available_updates (its default is https://www.concretecms.com/api/remote_update/update_core).
  • News, tutorials and backgrounds. The Dashboard's news and tutorial tiles (fetched by the server from concretecms.com) were removed from the Welcome page, the activity feed address is empty, and the sign in page and Dashboard use no remote background image. Add an RSS Displayer block to the Welcome page (Customize) for a feed you choose.
  • Geolocation. Concrete's geoPlugin geolocator, which sends visitors' addresses to geoplugin.net, is off (Dashboard > System & Settings > Environment > Geolocation).
  • E-mail is off, because the image has no mail server; password reset links and form notifications are not sent. Configure your SMTP server under Dashboard > System & Settings > Email > SMTP Method and then turn e-mail on. Until then, reset a forgotten password from the shell (Step 12).

For example, to set a value from the shell (-g writes it where the Dashboard does):

cd /var/www/concrete && sudo -u www-data php concrete/bin/concrete c5:config set -g concrete.email.enabled true

Concrete's own sign in protection: the failed_login rule bans an address for 600 seconds after 5 failures in 300 seconds (Dashboard > System & Settings > Permissions & Access > IP Deny List), and the geolocator is inactive:

sudo mysql -N concrete -e "SELECT iaccHandle, iaccMaxEvents, iaccTimeWindow, iaccBanDuration, iaccEnabled FROM IpAccessControlCategories; SELECT glHandle, glActive FROM Geolocators"
failed_login    5   300 600 1
forgot_password 2   30  600 1
geoplugin   0

Add-ons. The packages folder belongs to root, so PHP cannot write code into the site. To install an add-on, copy it into /var/www/concrete/packages, give it to root (sudo chown -R root:root /var/www/concrete/packages) and install it with sudo -u www-data php concrete/bin/concrete c5:package:install <handle>; an add-on is PHP code that runs with Concrete's rights, so install only ones you trust.

Step 8: Your data

Concrete's data is on its own EBS volume at /var/lib/concrete: the MariaDB data directory (mysql, initialised on this instance's first boot), uploads (files, which the site serves as /application/files/) and Concrete's configuration (config, which it reads as application/config):

sudo ls -l /var/lib/concrete | sed -E 's/ +/ /g' | cut -d' ' -f1,3,4,9-
total
drwxr-x--- www-data www-data config
drwxr-x--- www-data www-data files
drwx------ root root lost+found
drwxr-x--- mysql mysql mysql

/var/lib/concrete/config/database.php was written by Concrete's installer on first boot and holds the database password. Keep it with your backups.

MariaDB has four accounts: the system accounts root and mysql (which sign in only through the local socket as the matching operating system user), the locked mariadb.sys, and concrete, Concrete's own user with this instance's password. It has no network listener:

sudo mysql -N -e "SELECT CONCAT(User,'@',Host) FROM mysql.global_priv ORDER BY 1; SELECT @@skip_networking, @@datadir"
concrete@localhost
mariadb.sys@localhost
mysql@localhost
root@localhost
1   /var/lib/concrete/mysql/

Back the site up with EBS snapshots of the data volume and the root volume, or as files:

sudo mysqldump --single-transaction concrete | gzip > concrete-db.sql.gz
sudo tar -czf concrete-files.tar.gz -C / var/lib/concrete/files var/lib/concrete/config

Step 9: Security checks

Port 80 sends browsers to HTTPS, the health endpoint answers for load balancer checks, and the installer, registration, the configuration, the command line tool, the bundled libraries and the composer files are not reachable:

H=$(sed -n 's/^set \$cc_host "\(.*\)";$/\1/p' /etc/nginx/concrete-host.conf)
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/login"
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/healthz"
for p in /install /register /application/config/database.php /concrete/bin/concrete /concrete/vendor/autoload.php /composer.json /updates/; do
  curl -sk -o /dev/null -w "$p: %{http_code}\n" --resolve "$H:443:127.0.0.1" "https://$H$p"
done
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || true
port 80: 301 -> https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/login
ok
/install: 404
/register: 404
/application/config/database.php: 404
/concrete/bin/concrete: 404
/concrete/vendor/autoload.php: 404
/composer.json: 404
/updates/: 404
unknown host name: 000

(000 means nginx closed the connection without an answer.) The credentials file is readable by root only, database.php by the web server only, the uploads and configuration folders by the web server, and the code (including packages) belongs to root:

sudo stat -c '%a %U:%G %n' /root/concrete-cms-credentials.txt /var/lib/concrete/config/database.php /var/lib/concrete/config /var/lib/concrete/files /var/www/concrete/packages /var/www/concrete/index.php
600 root:root /root/concrete-cms-credentials.txt
600 www-data:www-data /var/lib/concrete/config/database.php
750 www-data:www-data /var/lib/concrete/config
750 www-data:www-data /var/lib/concrete/files
755 root:root /var/www/concrete/packages
644 root:root /var/www/concrete/index.php

The certificate was generated on this instance and names its own addresses:

H=$(sed -n 's/^set \$cc_host "\(.*\)";$/\1/p' /etc/nginx/concrete-host.conf)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
    DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost

nginx, PHP and MariaDB cannot reach the instance metadata service; PHP cannot run shell commands and is confined to Concrete's folders; nginx runs only index.php, hands PHP the configured host name and the real client address from the connection (never values a client sends, and never an Authorization header), and rate-limits sign in and other requests:

for u in nginx php8.3-fpm mariadb; do systemctl show $u -p IPAddressDeny; done
php-fpm8.3 -i 2>/dev/null | grep -E '^(open_basedir|disable_functions|expose_php) ' | cut -d' ' -f1-3
grep -E 'fastcgi_param (SCRIPT_FILENAME|HTTP_HOST|SERVER_NAME|HTTP_X_FORWARDED_FOR|HTTP_X_FORWARDED_HOST|HTTP_AUTHORIZATION) ' /etc/nginx/snippets/cloudimg-concrete-php.conf
grep -h 'limit_req_zone\|limit_req_status' /etc/nginx/conf.d/cloudimg-concrete-guard.conf
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
disable_functions => exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec
expose_php => Off
open_basedir => /var/www/concrete/:/var/lib/concrete/files/:/var/lib/concrete/config/:/var/lib/php/sessions/:/tmp/
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
fastcgi_param HTTP_HOST $cc_host;
fastcgi_param SERVER_NAME $cc_host;
fastcgi_param HTTP_X_FORWARDED_HOST "";
fastcgi_param HTTP_X_FORWARDED_FOR $remote_addr;
fastcgi_param HTTP_AUTHORIZATION "";
limit_req_zone $cc_login_key zone=cc_login:10m rate=6r/m;
limit_req_zone $cc_post_key zone=cc_post:10m rate=120r/m;
limit_req_status 429;

The cc_post limit allows 120 form submissions and editing requests a minute from one address (after a burst of the same size), which is plenty for editors; raise it in /etc/nginx/conf.d/cloudimg-concrete-guard.conf if you need to. If a burst of wrong passwords locks you out (HTTP 429, or "your IP address has been banned"), wait ten minutes, or lift the ban under Dashboard > System & Settings > Permissions & Access > IP Deny List from another address.

sudo concrete-cred-roundtrip checks the whole sign in path on the instance: the redirect to HTTPS, that no installer is offered and database.php is refused, a sign in through Concrete's login form with the password from the credentials file, and that wrong and common passwords are refused:

sudo concrete-cred-roundtrip
OK https 301 + no installer + database.php 404 + admin login + wrong and default passwords refused

Step 10: Command line tools

Concrete's command line tool runs from /var/www/concrete as the web server user. A few useful commands:

cd /var/www/concrete
sudo -u www-data php concrete/bin/concrete list
sudo -u www-data php concrete/bin/concrete c5:clear-cache
sudo -u www-data php concrete/bin/concrete c5:job

Never put a password on a command line (it would be stored in your shell history and the system logs); use the helpers in Step 12.

Step 11: Your own DNS name and certificate

Point a DNS record at the instance, then set the name Concrete is served as. This mints a new self signed certificate for the name, updates nginx, Concrete's canonical URL (used in links, redirects and password reset mails) and the addresses in /root/concrete-cms-credentials.txt, clears Concrete's cache and keeps the name across reboots:

sudo concrete-set-hostname <your-domain>

To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running concrete-set-hostname again replaces it with a self signed one):

sudo install -m 0644 <your-domain>.crt /etc/nginx/ssl/concrete.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/ssl/concrete.key
sudo nginx -t && sudo systemctl reload nginx

Once browsers trust the certificate you can also add an HSTS header (add_header Strict-Transport-Security "max-age=31536000" always;) to /etc/nginx/snippets/cloudimg-concrete-headers.conf. The image does not send one: browsers ignore HSTS from a site whose certificate they do not trust.

Step 12: Passwords and upgrades

concrete-set-password gives admin a new random password through Concrete's own user API, signs out every session and records the password in /root/concrete-cms-credentials.txt. Run it without arguments, or pass your own password (24 to 96 letters and digits) on stdin. Users change their own password under My Account.

sudo concrete-set-password
printf '%s\n' '<your-new-password>' | sudo concrete-set-password --stdin

Upgrades. Concrete releases are announced on concretecms.org and on GitHub. Snapshot the instance, download the new release zip from https://github.com/concretecms/concretecms/releases (compare its SHA-256 with the release page), read its release notes, and replace the concrete folder as root while keeping application and packages; then run the database migrations:

unzip -q concrete-cms-9.5.<n>.zip -d /tmp
sudo systemctl stop nginx
sudo rsync -a --delete /tmp/concrete-cms-9.5.<n>/concrete/ /var/www/concrete/concrete/
sudo chown -R root:root /var/www/concrete/concrete
cd /var/www/concrete && sudo -u www-data php concrete/bin/concrete c5:update
sudo -u www-data php concrete/bin/concrete c5:clear-cache
sudo systemctl start nginx

Step 13: Server components

Component Path
Concrete code (root owned) /var/www/concrete
Configuration: database connection, generated settings /var/lib/concrete/config (application/config)
Uploads, thumbnails and Concrete's cache /var/lib/concrete/files (application/files)
Database (MariaDB datadir) /var/lib/concrete/mysql (on the same EBS volume)
nginx site and request limits /etc/nginx/sites-available/cloudimg-concrete, /etc/nginx/conf.d/cloudimg-concrete-guard.conf
PHP settings /etc/php/8.3/fpm/conf.d/99-cloudimg-concrete.ini
MariaDB settings /etc/mysql/mariadb.conf.d/99-cloudimg-concrete.cnf
Certificate and key /etc/nginx/ssl/
First boot script and cloudimg helpers /usr/local/sbin/concrete-firstboot.sh, /usr/local/lib/cloudimg/concrete/
Helpers /usr/local/sbin/concrete-set-hostname, concrete-set-password, concrete-cred-roundtrip
Licence, release and image notes /usr/share/doc/concrete/
Credentials /root/concrete-cms-credentials.txt (mode 0600)

Step 14: Security recommendations

  • Allow port 22 in the security group from your own networks only.
  • Keep the admin password secret and rotate it if it may have been shared (Step 12). Give each editor their own account with only the permissions they need.
  • Keep /var/lib/concrete/config secret and backed up with the database (Step 8).
  • Think before opening public registration to the internet (Step 7).
  • Use your own DNS name and a certificate your browsers trust (Step 11).
  • Keep Concrete on the newest release (Step 12) and install only add-ons you trust (Step 7).
  • Keep the operating system patched; unattended security upgrades are on, and sudo apt-get update && sudo apt-get upgrade applies everything else. MariaDB, PHP-FPM and PHP's zip extension come from Ubuntu's universe repository; sudo pro security-status --esm-apps shows whether Ubuntu Pro (ESM Apps) has security updates for them (none were pending when the image was built).
  • Snapshot the data volume regularly (Step 8).

Support and licensing

Concrete CMS is free software under the MIT licence (a copy is at /usr/share/doc/concrete/LICENSE); the bundled libraries carry their own open source licences. There is no per user or per CPU licence fee. This image installs the unmodified upstream release, without its vendored test suites and documentation, plus the cloudimg first boot, settings and helpers described above. 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