ProjeQtOr 13.1.1 on AWS User Guide
Overview
This guide covers launching and using the ProjeQtOr 13.1.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg help desk, IT service and project management listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
ProjeQtOr is an open source project management suite. One web application covers planning (projects, activities, milestones, dependencies and a Gantt chart that ProjeQtOr calculates from workload and resource availability), ticketing, agile boards, time and expense tracking, risks, issues, decisions, meetings, quality and requirements, budgets, invoices and reports, with profiles that control what each user sees and changes.
What the cloudimg image adds:
- No web installer and no default login. Upstream asks you to finish the setup in a browser page that writes the database settings, and seeds the accounts
admin/adminandguest/guest. On this image first boot creates the database and the settings itself, the installer pages are not shipped,admingets a password unique to your instance (upstream'sadmin/adminis refused) and theguestaccount is closed and locked. - No database password at all. ProjeQtOr runs as its own system user,
projeqtor, and MariaDB accepts that user only over the local socket from processes running asprojeqtor(unix_socketauthentication). MariaDB has no network listener. The image ships with an empty database directory, so every instance builds its own database on first boot. - HTTPS from the first boot. nginx serves ProjeQtOr on port 443 with a certificate generated on your instance, port 80 redirects to HTTPS, requests for a host name the instance does not serve are dropped, and the session cookie is
Secure,HttpOnlyandSameSite=Lax. ProjeQtOr's "remember me" sign in is off and passwords must be at least 12 characters. - Only the application on the web. PHP runs only for ProjeQtOr's own pages and endpoints. The database scripts, the model classes, the plugin installer, the web self-updater, the data cloning tool, uploaded attachments and documents, logs and the configuration answer 404. Uploaded images are served as static files under a sandboxing content security policy, never as code, and attachments are downloaded only through ProjeQtOr's access checks.
- No call home. On every sign in, ProjeQtOr's pages ask the browser to fetch a news feed from projeqtor.org and send it statistics about the instance. Every page on this image carries
Content-Security-Policy: connect-src 'self', so the browser refuses that request, and sending statistics is switched off. - The REST API and SAML sign in are closed. ProjeQtOr's API identifies callers by the user name the web server hands it, so behind nginx it would accept any password; it answers 404 until you put it behind nginx authentication (Step 9). The bundled SAML toolkit has a published signature bypass advisory, so
/sso/answers 404 as well. - A hardened rich text editor. ProjeQtOr 13.1.1 bundles CKEditor 4.11.2, which has three high severity advisories. The image replaces it with CKEditor 4.22.1, removes the Source button and source mode, and turns on CKEditor's content filter with an explicit list of allowed elements (Step 7).
- Hardened defaults. nginx, PHP-FPM, MariaDB, ProjeQtOr's cron and the first boot service are blocked from the instance metadata service, the AMI requires IMDSv2, the code is owned by root and read only to PHP, PHP is handed the configured host name and the real client address, never values a client sends, sign in attempts are rate limited and cross site requests to ProjeQtOr's endpoints are refused.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: ProjeQtOr 13.1.1 (the official release from SourceForge, checksum verified), CKEditor 4.22.1, nginx, PHP 8.3 FPM, MariaDB 10.11, 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) from your admin network, and TCP 443 (ProjeQtOr) and TCP 80 (redirects to HTTPS) from the networks your users work from. Your project data is in ProjeQtOr, so keep 80 and 443 as narrow as you can.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick ProjeQtOr 13.1.1 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (or larger for many users), your key pair and a security group as above.
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=projeqtor}]'
First boot builds the database and takes about a minute after the instance is running.
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| ProjeQtOr 13.1.1 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot creates the database, sets the password and mints the certificate; nginx and ProjeQtOr's cron start only after it has finished:
systemctl is-active nginx php8.3-fpm mariadb projeqtor-cron projeqtor-firstboot
sudo test -f /var/lib/cloudimg/projeqtor-firstboot.done && echo FIRSTBOOT_DONE
All five units are active and the first boot marker is present:
active
active
active
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on network ports. MariaDB has no network listener at all and PHP-FPM uses a local socket:
sudo ss -lntp | awk 'NR>1 && $4 !~ /^127\.0\.0\.5[34]/ {split($6, p, "\""); print $4, p[2]}' | sort
0.0.0.0:22 sshd
0.0.0.0:443 nginx
0.0.0.0:80 nginx
[::]:22 sshd
[::]:443 nginx
[::]:80 nginx
Step 4: Get your URL and password
sudo cat /root/projeqtor-credentials.txt
The file is readable by root only:
# ProjeQtOr 13.1.1 - generated on first boot by projeqtor-firstboot.service
# These credentials are unique to this instance; none was baked into the image. Keep this file secret.
PROJEQTOR_URL=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/
PROJEQTOR_ADMIN_USER=admin
PROJEQTOR_ADMIN_PASSWORD=<PROJEQTOR_ADMIN_PASSWORD>
#
# Open PROJEQTOR_URL in a browser (self-signed certificate minted on this instance's first boot; port 80 redirects to
# https) and sign in with the user and password above. The seeded 'guest' account is closed and locked.
# MariaDB: unix socket only (no TCP listener, no database password: ProjeQtOr runs as the OS user projeqtor);
# sudo mysql projeqtor (root signs in over the socket)
# Cron: systemctl status projeqtor-cron PHP CLI as ProjeQtOr: sudo projeqtor-php <script.php>
# Change the password: sudo projeqtor-set-password
# Change the name it serves as: sudo projeqtor-set-hostname <your-dns-name>
# User guide: https://www.cloudimg.co.uk/guides/projeqtor-13-1-1-on-ubuntu-24-04-aws/
The URL uses the instance's public DNS name from the EC2 instance metadata, and the certificate also names the public and private IP addresses. nginx answers only to the names in the certificate. The name is fixed at first boot, so if you stop and start the instance without an Elastic IP and its public address changes, run sudo projeqtor-set-hostname with the new name (Step 10), or better, give the instance an Elastic IP or your own DNS name.
Step 5: Sign in
Open the PROJEQTOR_URL in your browser. The certificate is self signed, so your browser asks you to confirm it the first time (or install your own, Step 10). Sign in as admin with the password from Step 4:

ProjeQtOr opens its Start Guide, which walks you through the first things to set up: clients, contacts, resources and users, then planning and follow up. The Users box lists the two seeded accounts; guest is closed and locked on this image:

To choose your own password, open the user menu (admin, top right) and select Change password; it must be at least 12 characters. Then delete or update PROJEQTOR_ADMIN_PASSWORD in /root/projeqtor-credentials.txt so it does not hold an old password. If you lose the admin password, set a new one from the instance. Without an option it generates a random password and writes it to the credentials file; with --stdin it reads the password you type (20 to 72 letters and digits). Either way it unlocks admin and signs every session out:
sudo projeqtor-set-password
sudo projeqtor-set-password --stdin
Step 6: Plan a project
Create your project under Projects, add its activities and milestones under Activities and Milestones, link them with predecessors, allocate resources and then open Planning. Select the calculator button (Calculate planning) and ProjeQtOr schedules every activity from its work, its dependencies and the resources' availability, and draws the Gantt chart. The example below is a demonstration project with five chained activities and a milestone:

The project selector at the top (Project:) limits every list and the Gantt chart to one project or shows them all. Ticketing, Agile scrum, Follow-up, Financial and Reports in the main menu cover the rest of the suite, and Configuration holds the profiles, access rights, workflows and the global parameters.
Step 7: The rich text editor and who can use it
Descriptions, results and notes use a rich text editor, CKEditor 4.22.1 on this image:

Text written in the editor is stored as formatted HTML and shown to every user who can open the item. The image removes the editor's Source button and source mode, and runs CKEditor's content filter with an explicit list of allowed elements, so scripts, styles, frames, embedded objects, forms, SVG and event handlers are dropped, and javascript: and vbscript: links are removed:
grep -oE 'version:"[0-9.]+' /var/www/projeqtor/external/ckeditor/ckeditor.js | head -1 | sed 's/version:"/CKEditor /'
C=/var/www/projeqtor/external/ckeditor/projeqtorConfig.js
grep -q "'Source'" "$C" && echo "Source button: present" || echo "Source button: none"
grep -oE "config.removePlugins='[^']*'" "$C"
CKEditor 4.22.1
Source button: none
config.removePlugins='magicline,exportpdf,sourcearea,sourcedialog'
That filter runs in the user's browser, so a user who sends requests to the server by hand can still store HTML that other users then see. Give accounts that can create or edit items only to people you trust, such as colleagues, and use ProjeQtOr's profiles (Configuration, Access rights) to keep everyone else read only.
Step 8: Users, e-mail and what the server contacts
Users. Add each person under Configuration, Users (or as a resource marked as a user) with their own profile. There is no self registration: only an administrator creates accounts. Sign in with an LDAP directory is off; an administrator can turn it on under Configuration, Global parameters. SAML single sign on is not available on this image.
E-mail. ProjeQtOr sends notifications and alerts by e-mail, and can read replies from a mailbox. Set your SMTP server under Configuration, Global parameters, Mailing. Amazon SES works as the relay (port 587 with STARTTLS); allow outbound 587 if you restrict egress.
The cron. Alerts, notifications, imports and the mailbox reader run in ProjeQtOr's own cron loop. On this image it is a systemd service that runs as projeqtor from the PHP command line, not a web request:
systemctl is-active projeqtor-cron
systemctl show projeqtor-cron -p User
active
User=projeqtor
What the server contacts. ProjeQtOr connects only to the SMTP, IMAP and LDAP servers an administrator configures, plus one update check: when admin signs in, the server asks projeqtor.org for the newest release number (a plain request that sends no data about your instance). To turn that off, open Configuration, Global parameters, System and set check for new version to No in the Miscellaneous section. Nothing else is sent: the browser news and statistics call is blocked and switched off, as these checks show.
H=$(sudo grep '^PROJEQTOR_URL=' /root/projeqtor-credentials.txt | cut -d= -f2- | sed -E 's#https://([^/]+)/#\1#')
curl -sk -D - -o /dev/null --resolve "$H:443:127.0.0.1" "https://$H/view/main.php" | tr -d '\r' \
| grep -iE '^(content-security-policy|x-frame-options|x-content-type-options|referrer-policy|set-cookie: PHPSESSID)' \
| sed -E 's/PHPSESSID=[^;]*/PHPSESSID=<session>/'
sudo mysql -N -B projeqtor -e "SELECT parameterCode, parameterValue FROM parameter WHERE idUser IS NULL AND idProject IS NULL AND parameterCode IN ('allowSendingStatistics', 'rememberMe', 'paramPasswordMinLength', 'paramHttps') ORDER BY parameterCode;"
set-cookie: PHPSESSID=<session>; path=/; secure; HttpOnly; SameSite=Lax
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
content-security-policy: connect-src 'self'
allowSendingStatistics NO
paramHttps on
paramPasswordMinLength 12
rememberMe NO
Step 9: The REST API
ProjeQtOr's REST API (/api/) lets scripts read and write ProjeQtOr items as a user. It does not check a password itself: it trusts the user name the web server hands it after the web server has authenticated the caller. That is why it answers 404 on this image. To use it, put it behind nginx Basic authentication with a password file that only nginx reads. Each API user name must be a ProjeQtOr user, and the caller gets that user's access rights.
Create the password file with an API password for each user (here admin; use a long random password that is not the user's ProjeQtOr password). openssl reads the password you type and stores only its hash:
printf 'admin:%s\n' "$(openssl passwd -6 -stdin)" | sudo install -m 0640 -o root -g www-data /dev/stdin /etc/nginx/projeqtor-api.htpasswd
Add the API location to nginx and include it in the ProjeQtOr site, just before the final location / { return 404; }:
sudo tee /etc/nginx/snippets/projeqtor-api.conf >/dev/null <<'CONF'
# ProjeQtOr REST API behind nginx Basic authentication.
location ^~ /api/ {
auth_basic "ProjeQtOr API";
auth_basic_user_file /etc/nginx/projeqtor-api.htpasswd;
rewrite ^/api/(.*)$ /api/index.php?uri=$1 break;
include /etc/nginx/snippets/cloudimg-projeqtor-fastcgi.conf;
fastcgi_param REMOTE_USER $remote_user;
}
CONF
sudo sed -i 's|^ location / { return 404; }$| include /etc/nginx/snippets/projeqtor-api.conf;\n location / { return 404; }|' /etc/nginx/sites-available/cloudimg-projeqtor
sudo nginx -t && sudo systemctl reload nginx
A call without the API password is refused with 401, and with it ProjeQtOr answers in JSON (this one lists the projects the user can see):
curl -s -o /dev/null -w '%{http_code}\n' https://<your-host>/api/Project/all
401
curl -s -u admin https://<your-host>/api/Project/all
{"identifier":"id", "items":[ ... ] }
A closed or locked ProjeQtOr user is refused even with a valid API password. Writes (PUT, POST, DELETE) also need the data encrypted with the user's API key, shown read only on the user's form under Configuration, Users; see ProjeQtOr's API documentation. Keep the API to the networks that need it (for example with nginx allow and deny lines in the same location), and remove the include line and reload nginx to close it again.
Step 10: Your own DNS name and certificate
Point a DNS record at the instance (an Elastic IP keeps the address stable), then set the name ProjeQtOr is served as. This mints a new self signed certificate for the name, points nginx and ProjeQtOr's links (including the password reset link) at it, updates the URL in /root/projeqtor-credentials.txt, and keeps the name across reboots:
sudo projeqtor-set-hostname <your-domain>
To use a certificate from a public or internal certificate authority instead (for example one issued by Let's Encrypt for <your-domain>), copy it over the generated pair and reload nginx (running projeqtor-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/nginx/ssl/projeqtor.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/ssl/projeqtor.key
sudo nginx -t && sudo systemctl reload nginx
Step 11: Security checks
nginx serves only the application. The installer and self-update endpoints, the database scripts, the plugin directory, the stored files and the closed API and SAML paths answer 404, plain HTTP redirects to HTTPS, and a request for a name the instance does not serve gets no answer at all. (Requests to ProjeQtOr's /tool/ endpoints that do not come from ProjeQtOr's own pages are refused with 403 before routing, so the check sends the header a browser sends from those pages.)
H=$(sudo grep '^PROJEQTOR_URL=' /root/projeqtor-credentials.txt | cut -d= -f2- | sed -E 's#https://([^/]+)/#\1#')
for p in / /view/main.php /api/Project/all /sso/ /tool/config.php /tool/installAutoDownload.php /db/projeqtor_V13.1.1.sql /plugin/ /files/attach/ /files/logs/; do
curl -sk -o /dev/null -w "%{http_code} $p\n" -H 'Sec-Fetch-Site: same-origin' --resolve "$H:443:127.0.0.1" "https://$H$p"
done
curl -s -o /dev/null -w 'http:// -> %{http_code}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -sk -o /dev/null -w 'unknown host name -> %{http_code}\n' --resolve "other.example:443:127.0.0.1" "https://other.example/" || true
200 /
200 /view/main.php
404 /api/Project/all
404 /sso/
404 /tool/config.php
404 /tool/installAutoDownload.php
404 /db/projeqtor_V13.1.1.sql
404 /plugin/
404 /files/attach/
404 /files/logs/
http:// -> 301
unknown host name -> 000
admin is the only account that can sign in; guest is closed and locked:
sudo mysql -N -B projeqtor -e "SELECT CONCAT(name, ': locked ', IF(locked=1, 'yes', 'no'), ', closed ', IF(idle=1, 'yes', 'no')) FROM resource WHERE isUser = 1 ORDER BY id;"
admin: locked no, closed no
guest: locked yes, closed yes
ProjeQtOr's database account has no password; MariaDB accepts it only over the local socket from processes running as projeqtor, and MariaDB does not listen on the network:
sudo mysql -N -B -e "SELECT CONCAT(User, '@', Host, ' authenticates by ', JSON_VALUE(Priv, '$.plugin'), ', password set: ', IF(IFNULL(JSON_VALUE(Priv, '$.authentication_string'), '') = '', 'no', 'yes')) FROM mysql.global_priv WHERE User = 'projeqtor'; SELECT CONCAT('skip_networking: ', @@skip_networking);"
projeqtor@localhost authenticates by unix_socket, password set: no
skip_networking: ON
ProjeQtOr reports version 13.1.1 in its code and its database:
grep -oE '^\$version="V[0-9.]+"' /var/www/projeqtor/tool/projeqtor.php
sudo mysql -N -B projeqtor -e "SELECT CONCAT('database: ', parameterValue) FROM parameter WHERE parameterCode = 'dbVersion';"
$version="V13.1.1"
database: V13.1.1
The certificate was generated on this instance and names its own addresses:
H=$(sudo grep '^PROJEQTOR_URL=' /root/projeqtor-credentials.txt | cut -d= -f2- | sed -E 's#https://([^/]+)/#\1#')
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>.compute-1.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
DNS:ec2-<public-ip-dashed>.compute-1.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost
None of the services can reach the instance metadata service, so an SMTP, IMAP or LDAP address pointed at it gets nothing back:
systemctl show nginx php8.3-fpm mariadb projeqtor-cron projeqtor-firstboot -p Id -p IPAddressDeny
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Id=nginx.service
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Id=php8.3-fpm.service
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Id=mariadb.service
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Id=projeqtor-cron.service
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Id=projeqtor-firstboot.service
sudo projeqtor-php <script.php> runs a PHP script as ProjeQtOr with the same block.
Step 12: Backup and maintenance
Everything you enter is in the MariaDB database projeqtor (/var/lib/mysql); attachments, documents, uploaded images and ProjeQtOr's logs are under /var/lib/projeqtor/files. Both are on the root volume, so the simplest backup is an EBS snapshot of it. For a consistent copy while ProjeQtOr is running:
sudo mariadb-dump --single-transaction --routines projeqtor | gzip > <backup-dir>/projeqtor-$(date +%F).sql.gz
sudo tar -C /var/lib/projeqtor -czf <backup-dir>/projeqtor-files-$(date +%F).tar.gz files
The dump holds every password hash and API key, so store it as carefully as the instance. To give attachments and the database more room, enlarge the root volume in the EC2 console (Volumes, Modify); Ubuntu grows the file system on the next boot.
ProjeQtOr's web self-updater and plugin installer are switched off on this image, because the code is read only to PHP. New ProjeQtOr releases arrive as new versions of this listing; contact support if you would like help moving your data to one. Operating system security updates install automatically; run sudo apt-get update && sudo apt-get upgrade to apply everything else. nginx, PHP-FPM and MariaDB restart cleanly and ProjeQtOr keeps all of its data.
| Component | Path |
|---|---|
| ProjeQtOr | /var/www/projeqtor (owned by root, read only to PHP) |
| Configuration | /etc/projeqtor/parameters.php (mode 0640) |
| Database | MariaDB database projeqtor (/var/lib/mysql) |
| Files, sessions and logs | /var/lib/projeqtor |
| nginx site | /etc/nginx/sites-available/cloudimg-projeqtor |
| Certificate and key | /etc/nginx/ssl/projeqtor.crt, /etc/nginx/ssl/projeqtor.key |
| Cron | projeqtor-cron.service |
| Helpers | /usr/local/sbin/projeqtor-set-password, projeqtor-set-hostname, projeqtor-php |
| Credentials | /root/projeqtor-credentials.txt (mode 0600) |
| Source and changes | /usr/share/doc/projeqtor/SOURCE.txt |
Step 13: Security recommendations
- Allow port 22 from your admin network only, and ports 80 and 443 only from the networks your users work from.
- Use your own DNS name and a certificate your users' browsers trust (Step 10).
- Give each person their own ProjeQtOr user and the narrowest profile that fits, and give accounts that can create or edit items only to people you trust (Step 7). Change the
adminpassword if it may have been shared. - Leave
/api/closed unless you need it, and then only behind nginx authentication and for the networks that call it (Step 9). Leave/sso/closed. - Keep the operating system patched, and snapshot the root volume regularly.
Support and licensing
ProjeQtOr is open source under the GNU Affero General Public License version 3 or later, by the ProjeQtOr team; there is no per user or per CPU licence fee. The AGPL gives every user of your instance the right to its source: the image keeps the complete source in /var/www/projeqtor, and /usr/share/doc/projeqtor/SOURCE.txt names the release it came from and lists every file the image removes or changes (the editor hardening is marked cloudimg in the changed file). 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
All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.