Deployment guide

Concrete CMS 9.5.7

Overview

Concrete CMS (formerly concrete5) is a free, 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. The Dashboard manages the sitemap, files, users, permissions, forms, calendars and themes.

This guide covers the Concrete CMS 9.5.7 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud BookStack, CMS, Blogs and Wikis listing on AWS Marketplace. You choose it as the software version when you subscribe and launch. The image starts with Concrete's own Elemental sample site (pages, a blog, a portfolio and sample images) for you to edit or replace.

What you get:

  • 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: the MariaDB data directory is empty in the image, and there is no database.php. On first boot, your instance initialises its own database and runs Concrete's command-line installer (c5:install) before the web server starts. The installer creates the schema, the sample site, the super user admin with a password generated on your instance, and the configuration.
  • Per-instance secrets. 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, and 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. In your browser, the Elemental theme still loads its web font from Google Fonts and the Dashboard loads Google Charts.
  • Closed by default. Public registration is off, email 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, and no other PHP file can be called. The configuration (application/config), the command-line tool, the bundled libraries (concrete/vendor), the update folder, add-on code and composer files are never served. Uploaded files in application/files are served as static files only, each with nosniff and a sandboxing content security policy. Only passive types (images, PDF, fonts, audio and video) open in the browser. Anything else, including SVG and HTML, 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. 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, and 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.
  • A patched base. Ubuntu 24.04 LTS is fully patched at build time, with unattended security upgrades switched on.

Image contents: Concrete CMS 9.5.7 (the official release, checksum verified, with the 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.
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, choose your Region, then choose Launch through EC2.

Select 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.

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> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=concrete-cms}]'
2

Connect to your instance

Connect over SSH as the ubuntu user, the default user for Concrete CMS 9.5.7 on Ubuntu 24.04:

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

Check the services

First boot takes about a minute. It creates the database and runs Concrete's installer with the sample site. Afterwards, 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"))

Check 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
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. Display 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>
5

Visit the site and sign in

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

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

admin is Concrete's super user, the one account that can always do everything. On your 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.

6

Edit pages in place

When you are signed in, every page shows the Concrete toolbar. Choose the pencil to put the page in edit mode. Each area and block is outlined. Click a block to edit it, or drag a new one from the Add panel (the plus icon). Choose Publish Changes to make the new version live. Every page keeps its full version history.

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

Sitemap lists every page. Drag pages to move them, and add new pages from a page's menu.

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

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
7

Review the closed-by-default settings

The image writes its settings into Concrete's own configuration store (/var/lib/concrete/config/generated_overrides), the same place the Dashboard writes. You can change each one in the Dashboard or from the shell. These are 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

Each switch, 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).
  • Email is off because the image has no mail server, so password reset links and form notifications are not sent. Configure your SMTP server under Dashboard > System & Settings > Email > SMTP Method, then turn email on. Until then, reset a forgotten password from the shell (Step 12).

To set a value from the shell, use -g, which writes it where the Dashboard does. For example:

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 is the failed_login rule. It bans an address for 600 seconds after 5 failures in 300 seconds (Dashboard > System & Settings > Permissions & Access > IP Deny List). 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 add-ons you trust.

8

Your data

Concrete's data is on its own EBS volume at /var/lib/concrete. It holds the MariaDB data directory (mysql, initialised on this instance's first boot), the 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/

To back up the site, take EBS snapshots of the data volume and the root volume, or copy the data 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
9

Security checks

Confirm that port 80 sends browsers to HTTPS and that the health endpoint answers for load balancer checks. The installer, registration, the configuration, the command-line tool, the bundled libraries and the composer files must not be 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

A 000 means that nginx closed the connection without an answer.

Only root can read the credentials file. Only the web server can read database.php and the uploads and configuration folders. 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 = sac 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/sac-concrete-php.conf grep -h 'limit_req_zone\|limit_req_status' /etc/nginx/conf.d/sac-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/sac-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. Alternatively, lift the ban from another address under Dashboard > System & Settings > Permissions & Access > IP Deny List.

Run sudo concrete-cred-roundtrip to check the whole sign-in path on the instance. It verifies the redirect to HTTPS, that no installer is offered, that database.php is refused, that a sign-in through Concrete's login form works 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
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, because it would be stored in your shell history and the system logs. Use the helpers in Step 12 instead.

11

Your own DNS name and certificate

Point a DNS record at the instance, then set the name Concrete is served as. The command below creates a new self-signed certificate for the name and updates nginx, Concrete's canonical URL (used in links, redirects and password reset emails) and the addresses in /root/concrete-cms-credentials.txt. It also 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 certificate.

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/sac-concrete-headers.conf. The image does not send one, because browsers ignore HSTS from a site whose certificate they do not trust.

12

Passwords and upgrades

Run concrete-set-password to give admin a new random password. It uses 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. To upgrade, snapshot the instance and download the new release zip from https://github.com/concretecms/concretecms/releases. Compare its SHA-256 with the release page and read the release notes. Then replace the concrete folder as root, keeping application and packages, and 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
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/sac-concrete, /etc/nginx/conf.d/sac-concrete-guard.conf

PHP settings /etc/php/8.3/fpm/conf.d/99-sac-concrete.ini

MariaDB settings /etc/mysql/mariadb.conf.d/99-sac-concrete.cnf

Certificate and key /etc/nginx/ssl/

First boot script and SecureAnyCloud helpers /usr/local/sbin/concrete-firstboot.sh, /usr/local/lib/sac/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)

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. Run sudo pro security-status --esm-apps to see 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, and 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 SecureAnyCloud first-boot setup, settings and helpers described above. The AMI price covers SecureAnyCloud's hardening, first-boot setup and support.

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

Deploy on AWS

Launch Concrete CMS 9.5.7 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