Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ and on top of that:

**Security note**: Updates to EspoCRM may require supervision so
they **ARE NOT** configured to install automatically. See `EspoCRM
documentation`_ for upgrading.
documentation`_ for upgrading. The supported command-line updater is
``sudo -u www-data php /var/www/espocrm/command.php upgrade``. Back up
the appliance before applying an upgrade.

- Websocket_ preconfigured and enabled.

Expand All @@ -40,4 +42,3 @@ Credentials *(passwords set at first boot)*
.. _EspoCRM documentation: https://www.espocrm.com/documentation/administration/upgrading/
.. _Websocket: https://docs.espocrm.com/administration/websocket/
.. _Adminer: https://www.adminer.org

17 changes: 17 additions & 0 deletions changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
turnkey-espocrm-19.0 (1) turnkey; urgency=low

* Install EspoCRM 10.0.6 from the official upstream release archive and
verify its published SHA-256 digest during the build.

* Update the appliance for Debian 13/Trixie, PHP 8.4 and EspoCRM's current
bcrypt password format.

* Set the firstboot site URL through EspoCRM's supported configuration
command.

* Restrict the EspoCRM database account to the EspoCRM database.

* Upgrade the base distribution to Debian 13/Trixie.

-- Jeremy Davis <jeremy@turnkeylinux.org> Tue, 25 Aug 2026 00:00:00 +0000

turnkey-espocrm-18.0 (1) turnkey; urgency=low

* Install latest upstream version of EspoCRM: v8.0.6.
Expand Down
21 changes: 13 additions & 8 deletions conf.d/downloads
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
#!/bin/bash -ex
#!/bin/bash
set -euo pipefail

dl() {
[ "$FAB_HTTP_PROXY" ] && PROXY="--proxy $FAB_HTTP_PROXY"
cd $2; curl -L -f -O $PROXY $1; cd -
}
readonly VERSION=10.0.6
readonly ARCHIVE="EspoCRM-${VERSION}.zip"
readonly URL="https://github.com/espocrm/espocrm/releases/download/${VERSION}/${ARCHIVE}"
readonly SHA256=88bcb177dbe38b79ec3c13d491d78b727dc14fe18fc91deea1bfa72b313f7fb4

VERSION=$(gh_releases espocrm/espocrm | grep -vi 'beta' | sort -V | tail -1)
URL="https://github.com/espocrm/espocrm/releases/download/$VERSION/EspoCRM-$VERSION.zip"
proxy_args=()
if [[ -n ${FAB_HTTP_PROXY:-} ]]; then
proxy_args=(--proxy "$FAB_HTTP_PROXY")
fi

dl $URL /usr/local/src
curl --fail --location --show-error "${proxy_args[@]}" \
--output "/usr/local/src/${ARCHIVE}" "$URL"
printf '%s %s\n' "$SHA256" "/usr/local/src/${ARCHIVE}" | sha256sum --check --strict
51 changes: 26 additions & 25 deletions conf.d/main
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/sh -ex
#!/bin/bash
set -euo pipefail

DB_NAME=espocrm
DB_USER=espocrm
Expand All @@ -12,13 +13,13 @@ WEBROOT=/var/www/espocrm

# unpack and set required permissions

unzip $SRC/EspoCRM-*.zip -d $SRC
rm $SRC/EspoCRM-*.zip
mv $SRC/EspoCRM-* $WEBROOT
chown -R www-data:www-data $WEBROOT
unzip "$SRC"/EspoCRM-*.zip -d "$SRC"
rm "$SRC"/EspoCRM-*.zip
mv "$SRC"/EspoCRM-* "$WEBROOT"
chown -R www-data:www-data "$WEBROOT"

# php.ini
CONF=/etc/php/8.2/apache2/php.ini
CONF=/etc/php/8.4/apache2/php.ini
sed -i "s|^memory_limit.*|memory_limit = 256M|" $CONF
sed -i "s|^upload_max_filesize.*|upload_max_filesize = 50M|" $CONF
sed -i "s|^post_max_size.*|post_max_size = 50M|" $CONF
Expand All @@ -29,6 +30,7 @@ sed -i "s|^max_execution_time.*|max_execution_time = 180|" $CONF
a2dissite 000-default
a2enmod headers
a2ensite espocrm
a2enmod ssl
a2enmod rewrite
a2enmod proxy
a2enmod proxy_wstunnel
Expand All @@ -38,30 +40,29 @@ service mysql start
service apache2 start


MYSQL_BATCH="mysql --batch"
MYSQL_BATCH=(mysql --batch)

mysqladmin create $DB_NAME

$MYSQL_BATCH --execute "CREATE USER 'espocrm'@'localhost' IDENTIFIED BY '$DB_PASS';"
$MYSQL_BATCH --execute "GRANT ALL PRIVILEGES ON * . * TO 'espocrm'@'localhost';"
"${MYSQL_BATCH[@]}" --execute "CREATE DATABASE \`${DB_NAME}\`;"
"${MYSQL_BATCH[@]}" --execute "CREATE USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';"
"${MYSQL_BATCH[@]}" --execute "GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'localhost';"

# curl
URL="http://127.0.0.1/install/"
CURL="curl --request POST -c /tmp/cookie -b /tmp/cookie"

$CURL $URL --data "user-lang=en_US&action=step1"
$CURL $URL --data "license-agree=1&action=step2"
$CURL ${URL}index.php --data "hostName=localhost&dbName=$DB_NAME&dbUserName=$DB_USER&dbUserPass=$DB_PASS&action=settingsTest"
$CURL $URL --data "host-name=localhost&db-name=$DB_NAME&db-user-name=$DB_USER&db-user-password=$DB_PASS&action=setupConfirmation"
$CURL ${URL}index.php --data "action=saveSettings"
$CURL ${URL}index.php --data "action=buildDatabase"
$CURL $URL --data "action=step3"
$CURL ${URL}index.php --data "name=$ADMIN_NAME&pass=$ADMIN_PASS&confPass=$ADMIN_PASS&user-name=$ADMIN_NAME&user-pass=$ADMIN_PASS&action=createUser"
$CURL $URL --data "user-name=$ADMIN_NAME&user-pass=$ADMIN_PASS&user-confirm-pass=$ADMIN_PASS&action=step4"
$CURL ${URL}index.php --data "dateFormat=MM%2FDD%2FYYYY&timeFormat=HH%3Amm&timeZone=UTC&weekStart=0&defaultCurrency=USD&thousandSeparator=%2C&decimalMark=.&language=en_US&action=savePreferences"
$CURL ${URL} --data "dateFormat=MM%2FDD%2FYYYY&timeFormat=HH%3Amm&timeZone=UTC&weekStart=0&defaultCurrency=USD&thousandSeparator=%2C&decimalMark=.&language=en_US&action=step5"
$CURL ${URL}index.php --data "smtpServer=&smtpPort=25&smtpAuth=false&smtpSecurity=TLS&smtpUsername=&smtpPassword=&outboundEmailFromName=&outboundEmailFromAddress=&outboundEmailIsShared=false&action=saveEmailSettings"
$CURL ${URL} --data "outboundEmailFromName=&outboundEmailFromAddress=&outboundEmailIsShared=on&smtpServer=&smtpPort=25&smtpSecurity=TLS&smtpUsername=&smtpPassword=&action=finish"
$CURL "$URL" --data "user-lang=en_US&action=step1"
$CURL "$URL" --data "license-agree=1&action=step2"
$CURL "${URL}index.php" --data "hostName=localhost&dbName=$DB_NAME&dbUserName=$DB_USER&dbUserPass=$DB_PASS&action=settingsTest"
$CURL "$URL" --data "host-name=localhost&db-name=$DB_NAME&db-user-name=$DB_USER&db-user-password=$DB_PASS&action=setupConfirmation"
$CURL "${URL}index.php" --data "action=saveSettings"
$CURL "${URL}index.php" --data "action=buildDatabase"
$CURL "$URL" --data "action=step3"
$CURL "${URL}index.php" --data "name=$ADMIN_NAME&pass=$ADMIN_PASS&confPass=$ADMIN_PASS&user-name=$ADMIN_NAME&user-pass=$ADMIN_PASS&action=createUser"
$CURL "$URL" --data "user-name=$ADMIN_NAME&user-pass=$ADMIN_PASS&user-confirm-pass=$ADMIN_PASS&action=step4"
$CURL "${URL}index.php" --data "dateFormat=MM%2FDD%2FYYYY&timeFormat=HH%3Amm&timeZone=UTC&weekStart=0&defaultCurrency=USD&thousandSeparator=%2C&decimalMark=.&language=en_US&action=savePreferences"
$CURL "$URL" --data "dateFormat=MM%2FDD%2FYYYY&timeFormat=HH%3Amm&timeZone=UTC&weekStart=0&defaultCurrency=USD&thousandSeparator=%2C&decimalMark=.&language=en_US&action=step5"
$CURL "${URL}index.php" --data "smtpServer=&smtpPort=25&smtpAuth=false&smtpSecurity=TLS&smtpUsername=&smtpPassword=&outboundEmailFromName=&outboundEmailFromAddress=&outboundEmailIsShared=false&action=saveEmailSettings"
$CURL "$URL" --data "outboundEmailFromName=&outboundEmailFromAddress=&outboundEmailIsShared=on&smtpServer=&smtpPort=25&smtpSecurity=TLS&smtpUsername=&smtpPassword=&action=finish"

rm -f /tmp/cookie

Expand All @@ -70,5 +71,5 @@ service apache2 stop
service mysql stop

# enable websocket
sed -i "\|useWebSocket|s|>.*|> true,|" $WEBROOT/data/config.php
sed -i "\|useWebSocket|s|>.*|> true,|" "$WEBROOT/data/config.php"
systemctl enable espocrm-websocket.service
77 changes: 77 additions & 0 deletions docs/v19.0-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# EspoCRM 19.0 testing

## Dependency source decision

EspoCRM is not packaged by Debian Trixie. The appliance installs EspoCRM
10.0.6 from the official GitHub release archive. The build pins the version and
verifies GitHub's published SHA-256 digest before unpacking it. PHP 8.4,
MariaDB, Apache, the required PHP extensions, Postfix, Adminer and Webmin come
from the TurnKey LAMP plan and Debian Trixie packages.

The obsolete v18 `php-imap` plan entry is removed. Trixie does not provide that
extension, and EspoCRM 10.0.6 does not declare it as a system requirement or
use PHP's removed IMAP functions.

EspoCRM supports supervised upgrades with:

```sh
cd /var/www/espocrm
sudo -u www-data php command.php upgrade
```

Back up the appliance before upgrading. The acceptance test queries official
GitHub release metadata without changing the installed application.

## README primary-flow crosswalk

| README claim | Acceptance evidence |
| --- | --- |
| EspoCRM installed at `/var/www/espocrm` | The test reads the installed version with EspoCRM's command runner and requires 10.0.6. |
| HTTPS application | The test requests the application through Apache on HTTPS and checks that the firstboot domain is stored as the site URL. |
| Administrator credentials set at first boot | The test authenticates `admin` using the firstboot password and obtains an EspoCRM API token. |
| CRM lead workflow | The authenticated test creates a lead, reads it back through the REST API, and confirms the record in MariaDB. |
| WebSocket integration | The test requires the EspoCRM WebSocket service to be active. |
| Scheduled jobs | The test invokes the documented cron entry point as `www-data` and requires successful completion. |
| MariaDB and Adminer | The test pings MariaDB with the firstboot root password, verifies lead persistence, and requires the Adminer package. |
| Postfix and Webmin modules | The test requires Postfix and the Apache, MariaDB and Postfix Webmin modules in the installed package database. |
| Supervised upstream updates | The test queries the official stable release channel, records the available version, and confirms the installed version is unchanged. |
| Standard TurnKey Core features | Covered by the established Core 19 baseline. |

## Validation commands

Static validation:

```sh
bash -n conf.d/downloads conf.d/main tests/v19.sh \
overlay/usr/lib/inithooks/firstboot.d/20regen-espocrm-secrets \
overlay/usr/lib/inithooks/firstboot.d/40espocrm
python3 -m py_compile overlay/usr/lib/inithooks/bin/espocrm.py
git diff --check
```

Full appliance validation:

```sh
/sandboxed-git/turnkey/tools/test-v19-appliance espocrm \
--source /home/agent/.local/worktrees/turnkey-apps/espocrm/wish-espocrm-v19-trixie
```

The full run builds the Trixie root filesystem, completes normal boot and
firstboot configuration, and then executes `tests/v19.sh` inside the running
Docker appliance.

## Inherited Core 19 evidence

The accepted Core 19 Docker baseline verifies normal init, multi-user
operation, SSH, cron, Debian Trixie identity, updater channels and signed APT
metadata. This appliance test focuses on the additional EspoCRM contract. The
Core or TKLDev ISO validation covers the shared ISO path, so this migration does
not repeat an ISO boot.

## Deferred minor issues

- Outbound email delivery is not exercised because it requires an external
recipient and is not part of the normal CRM data-entry flow. The test checks
that Postfix and its Webmin integration are installed.
- The test validates the WebSocket daemon and ZMQ integration but does not run
a browser notification exchange.
56 changes: 34 additions & 22 deletions overlay/usr/lib/inithooks/bin/espocrm.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@

import sys
import getopt
import hashlib
import crypt
import re
import subprocess
from libinithooks import inithooks_cache

from libinithooks.dialog_wrapper import Dialog
Expand Down Expand Up @@ -65,25 +63,39 @@ def main():

inithooks_cache.write('APP_DOMAIN', domain)

conf = "/var/www/espocrm/data/config-internal.php"

lines = []
with open(conf, 'r') as fob:
for line in fob:
match = re.search("'passwordSalt' => '([^']*)',", line)
if match != None:
normSalt = ('$6$%s$' % match.group(1))
hashed = crypt.crypt(hashlib.md5(password.encode('utf8')).hexdigest(), normSalt).replace(normSalt, '')

m = MySQL()
m.execute('UPDATE espocrm.user SET password=%s WHERE user_name=\"admin\"', (hashed))
if 'siteUrl' in line:
line = re.sub("=> '([^']*)'", f"=> 'https://{domain}'", line)

lines.append(line)

with open(conf, 'w') as fob:
fob.writelines(lines)
subprocess.run(
[
'runuser',
'-u',
'www-data',
'--',
'php',
'command.php',
'config:set',
'siteUrl',
f'https://{domain}',
],
cwd='/var/www/espocrm',
check=True,
)

hashed = subprocess.run(
[
'php',
'-r',
'echo password_hash(stream_get_contents(STDIN), PASSWORD_BCRYPT);',
],
input=password,
text=True,
check=True,
capture_output=True,
).stdout

m = MySQL()
m.execute(
'UPDATE espocrm.user SET password=%s WHERE user_name="admin"',
(hashed,),
)

if __name__ == "__main__":
main()
3 changes: 2 additions & 1 deletion plan/main
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
php-gd
php-cli
php-curl
php-imap
php-mbstring
php-zip
php-xml
php-zmq
php-bcmath
php-exif

unzip
uuid-runtime
Expand Down
Loading