Kc
Applications AWS

Krayin CRM 2.2.8 on AWS User Guide

| Product: Krayin CRM 2.2.8 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Krayin CRM 2.2.8 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg ERP and CRM listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

Krayin CRM is an open source customer relationship management system built on Laravel by Webkul. It tracks leads through sales pipelines, keeps people and organisations, products and quotes, logs activities and e-mail against each lead, captures leads from web forms and notifies other systems through webhooks. Roles and permissions control what each user can see and do.

The cloudimg image is a hardened, ready-to-use Krayin CRM server:

  • The upstream release. Krayin CRM 2.2.8 from its GitHub tag, verified against its SHA-256. No Krayin source file is modified. Its PHP dependencies are installed from the tag's own lock file, unchanged (no known advisory), and its compiled front end ships as released. SOURCE.txt lists exactly what is not shipped.
  • No default login. Krayin's sample administrator (admin@example.com / admin123) never exists on this image. First boot creates one administrator, admin@cloudimg.local, with a random password unique to your instance. APP_KEY (Laravel's encryption key) and the MySQL password are also generated on your instance. Until first boot has finished, neither nginx nor PHP starts, and Krayin's web installer is refused.
  • HTTPS from the first boot. nginx serves Krayin on 443 with a certificate generated on your instance (an end-entity certificate, not a certificate authority). Port 80 redirects to HTTPS. The server's name is set from the instance's EC2 public DNS name at first boot, and requests for any other name are dropped. You can change it to your own domain.
  • MySQL 8.0 on the local socket only. The database server does not listen on any network port. Krayin connects over the Unix socket with its own user, which can reach only the krayin database. The image ships an empty data directory: the database, its keys and its users are created on your instance.
  • Hardened web tier. PHP runs as its own unprivileged user on a Unix socket only nginx can reach, with every way of starting a program disabled. nginx passes Krayin the real client address from the connection, never a header a client sends, limits sign-in and password-reset attempts per address (Krayin also locks an e-mail address and client address out after five failed attempts), refuses cross-site requests to the data-import steps and the dashboard PDF export, and refuses the front controller path, the web installer, configuration, logs and the source tree. Debug mode is off and the environment is production.
  • Outbound integrations off until you configure them. Mail goes to Krayin's log until you set an SMTP server, inbound e-mail answers "not configured" until you set a token or an IMAP account, and no web form, webhook or AI key exists. No credential for any outside service is on the image.
  • Egress backstop. The krayin user (the web application, the queue worker, the scheduler and every command run through krayin-artisan) cannot reach the instance metadata service, link-local, reserved or multicast addresses, any IPv6 address, or any private address inside your VPC. A webhook URL, an IMAP server or a mail server typed into Krayin therefore cannot be used to probe your network. You can allow specific in-VPC hosts (Step 12).
  • Metadata service blocked. nginx, PHP, MySQL, the queue worker, the scheduler and the first-boot service cannot reach the EC2 instance metadata service. The AMI requires IMDSv2.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Krayin CRM 2.2.8 (MIT), PHP 8.3 FPM and CLI, MySQL 8.0, nginx, nftables, cloud-init and the AWS Systems Manager agent, all from Ubuntu 24.04 except Krayin. Node.js and Composer are not on the image. Four PHP packages come from Ubuntu's universe component: php8.3-fpm, php8.3-intl, php8.3-zip and php8.3-bcmath; they are listed in /var/lib/cloudimg/krayin-universe-packages.txt.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large (2 vCPUs, 8 GiB). A t3.large is enough for a small team.

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet.
  • A security group that allows TCP 22 (SSH) from your own networks only, TCP 443 from wherever your users are, and TCP 80 (redirects to HTTPS). No other port is needed.
  • For production, a DNS name you control (for example crm.example.com) and, ideally, a certificate from a certificate authority. Until then the instance uses its EC2 public DNS name with its own self-signed certificate.

Connecting to your instance

Operating system SSH user
Ubuntu 24.04 ubuntu

Step 1: Launch the instance

Launch from the AWS Marketplace page (choose the Krayin CRM 2.2.8 on Ubuntu 24.04 version), or with the AWS CLI using the AMI ID shown on the listing's launch page:

aws ec2 run-instances --image-id ami-xxxxxxxxxxxxxxxxx --instance-type m5.large \
  --key-name my-key --security-group-ids sg-xxxxxxxx --subnet-id subnet-xxxxxxxx \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=krayin-crm}]'

First boot takes about a minute once the instance is up: it initialises MySQL, creates the database, runs Krayin's migrations and seeders (pipelines, stages, attributes, roles, e-mail templates), and creates the administrator and the certificate. The web interface stays offline until that has finished.

Step 2: Connect to your instance

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

Step 3: Check the services

systemctl is-active mysql.service nginx.service php8.3-fpm.service krayin-queue.service krayin-egress-guard.service
sudo ss -ltnp | awk 'NR==1 || /:(22|80|443|3306|33060) /'
sudo ss -lxp | grep krayin-fpm
active
active
active
active
active
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      511          0.0.0.0:443       0.0.0.0:*    users:(("nginx",pid=72867,fd=7),("nginx",pid=72866,fd=7),("nginx",pid=69663,fd=7))
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*    users:(("nginx",pid=72867,fd=5),("nginx",pid=72866,fd=5),("nginx",pid=69663,fd=5))
LISTEN 0      4096         0.0.0.0:22        0.0.0.0:*    users:(("sshd",pid=898,fd=3),("systemd",pid=1,fd=88))
LISTEN 0      511             [::]:443          [::]:*    users:(("nginx",pid=72867,fd=8),("nginx",pid=72866,fd=8),("nginx",pid=69663,fd=8))
LISTEN 0      511             [::]:80           [::]:*    users:(("nginx",pid=72867,fd=6),("nginx",pid=72866,fd=6),("nginx",pid=69663,fd=6))
LISTEN 0      4096            [::]:22           [::]:*    users:(("sshd",pid=898,fd=4),("systemd",pid=1,fd=89))
u_str LISTEN 0      4096                          /run/php/krayin-fpm.sock 151018            * 0    users:(("php-fpm8.3",pid=69659,fd=10),("php-fpm8.3",pid=69658,fd=10),("php-fpm8.3",pid=69653,fd=8))

Only SSH, HTTP and HTTPS listen on the network. MySQL (3306, and its X protocol on 33060) does not listen at all; PHP listens only on its Unix socket.

Step 4: Get your address and the administrator password

First boot writes everything you need to sign in to a file only root can read:

sudo cat /root/krayin-credentials.txt
# Krayin CRM 2.2.8 - generated on first boot by krayin-firstboot.service
# These credentials are unique to this instance; none was baked into the image. Keep this file secret.

KRAYIN_URL=https://ec2-3-82-139-119.compute-1.amazonaws.com/admin/login
KRAYIN_ADMIN_EMAIL=admin@cloudimg.local
KRAYIN_ADMIN_PASSWORD=<24 random letters and digits>
KRAYIN_TLS_SHA256=<the certificate's SHA-256 fingerprint>
...
  • KRAYIN_URL is your Krayin sign-in page.
  • KRAYIN_TLS_SHA256 is the fingerprint of the instance's own certificate. Your browser will warn that the certificate is not trusted; compare the fingerprint it shows with this one before you continue. Replace it with a certificate from a certificate authority for your own name when you can (Step 13).
  • KRAYIN_ADMIN_EMAIL / KRAYIN_ADMIN_PASSWORD sign in to Krayin as its administrator.

Step 5: Run the self-test

The image includes an end-to-end test. Over HTTPS on the instance it checks that port 80 redirects, that the front controller path, the web installer, the configuration file, the data and the sources are refused, that the health check answers, that the administrator signs in through Krayin's own sign-in form, and that a wrong password and common default passwords are refused:

sudo krayin-cred-roundtrip
KRAYIN_ROUNDTRIP_OK https 301 + no index.php/installer/env/data/sources + healthz + administrator sign-in (Krayin's own sign-in form, CSRF token) + wrong and default passwords refused

Step 6: Sign in to the web interface

Open KRAYIN_URL in your browser, check the certificate fingerprint, and sign in with KRAYIN_ADMIN_EMAIL and KRAYIN_ADMIN_PASSWORD.

The Krayin Sign In page with the e-mail address admin@cloudimg.local entered, an empty password field, a Forget Password link and a Sign In button

Change the administrator's name and e-mail address under Settings > Users (or My Account from the avatar at the top right). To change the password, run one of these on the instance. The first generates a new random password; the second reads yours (20 to 72 letters and digits) from the terminal, never from the command line. Either way every session is signed out and the new password is written to /root/krayin-credentials.txt:

sudo krayin-set-password
# Type <new-password> at the prompt; nothing is echoed.
read -rs -p 'New Krayin password: ' P; echo
printf '%s\n' "$P" | sudo krayin-set-password --stdin; unset P

Once you have stored the password somewhere safe, you can delete the KRAYIN_ADMIN_PASSWORD line from /root/krayin-credentials.txt.

Step 7: Work with leads, contacts and quotes

After signing in you land on the Dashboard, which summarises won and lost revenue, leads, quotes, people and organisations for a date range:

The Krayin Dashboard with Won Revenue and Lost Revenue, Average Lead Value, Total Leads, Average Leads Per Day, Total Quotations, Total Persons and Total Organizations tiles, a Leads chart, the Open Leads By Stages and Revenue By Sources panels and an Export PDF button

Leads shows the Default Pipeline as a board with the stages New, Follow Up, Prospect, Negotiation, Won and Lost. Create Lead adds a lead with its contact person, value, source and products; drag a card between stages as it progresses, or switch to the list view with the icon at the top right:

The Krayin Leads board for the Default Pipeline with the columns New, Follow Up, Prospect, Negotiation and Won, each empty with a Create Lead button, a Search by Title box, a Filter button and a Create Lead button at the top

Contacts holds persons and organisations, Products your catalogue, Quotes quotations linked to leads, and Activities calls, meetings and notes. Under Settings you add users, roles and groups, edit pipelines and stages, define custom attributes, sources, types and tags, and create Web Forms (lead capture forms for your website) and Webhooks. Webhook URLs and web forms are yours to add; none exists on a new instance.

Step 8: The queue worker and the scheduler

Krayin sends e-mail and runs other background work through a queue. On this image the queue worker is the krayin-queue service, running as the unprivileged krayin user:

systemctl show -p User -p ActiveState -p SubState krayin-queue.service
sudo krayin-artisan schedule:list
systemctl show -p UnitFileState krayin-schedule.timer
User=krayin
ActiveState=active
SubState=running

  0   0 * * *  php artisan campaign:process ...... Next Due: 14 hours from now
  */5 * * * *  php artisan inbound-emails:process  Next Due: 52 seconds from now

UnitFileState=disabled

Krayin's scheduler has two tasks: inbound-emails:process fetches mail from an IMAP account every five minutes, and campaign:process sends marketing campaigns once a day. Both need mail that you configure (Steps 10 and 11), so the scheduler timer is installed but switched off on a new instance. Switch it on once mail is set up:

sudo systemctl enable --now krayin-schedule.timer

Step 9: Krayin's command line

krayin-artisan runs Krayin's own artisan commands as the krayin user, with the same metadata block and egress backstop as the web tier:

sudo krayin-artisan --version
sudo krayin-artisan about --only=environment
Laravel Framework 12.71.0

  Environment ................................................................
  Application Name ................................................ Krayin CRM
  Laravel Version .................................................... 12.71.0
  PHP Version .......................................................... 8.3.6
  Composer Version ......................................................... -
  Environment ..................................................... production
  Debug Mode ............................................................. OFF
  URL ............................... ec2-3-82-139-119.compute-1.amazonaws.com
  Maintenance Mode ....................................................... OFF
  Timezone ............................................................... UTC
  Locale .................................................................. en

Step 10: Send e-mail

Out of the box Krayin writes outgoing mail (password resets, e-mail to leads, campaigns) to its log instead of sending it. To send through your mail server or a service such as Amazon SES, edit /etc/krayin/env as root and set the mail values:

MAIL_MAILER=smtp
MAIL_HOST=email-smtp.us-east-1.amazonaws.com
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_USERNAME=<your SMTP user name>
MAIL_PASSWORD=<your SMTP password>
MAIL_FROM_ADDRESS=crm@example.com
MAIL_FROM_NAME="Krayin CRM"

Then clear Krayin's cached configuration and restart PHP and the queue worker:

sudo krayin-artisan config:clear
sudo systemctl restart php8.3-fpm krayin-queue

The file is readable by root and the krayin group only. If your mail server has a private address inside your VPC, allow it first (Step 12).

Step 11: Receive e-mail

Krayin can file incoming e-mail against leads and contacts in two ways:

  • IMAP. Set MAIL_RECEIVER_DRIVER=webklex-imap in /etc/krayin/env, run sudo krayin-artisan config:clear, enter the mail account under Configuration > Email > IMAP Settings, then switch on the scheduler (Step 8), which fetches new mail every five minutes. sudo krayin-artisan inbound-emails:process fetches it once by hand.

The Krayin IMAP Settings page under Configuration with the IMAP Account fields Host, Port, Encryption Type, Validate SSL Certificate, IMAP Username and IMAP Password and a Save Configuration button

  • SendGrid Inbound Parse. Leave MAIL_RECEIVER_DRIVER=sendgrid, set MAIL_RECEIVER_INBOUND_TOKEN in /etc/krayin/env to a long random value (for example the output of openssl rand -hex 32), run sudo krayin-artisan config:clear and sudo systemctl restart php8.3-fpm, and point SendGrid's Inbound Parse webhook at https://<your-domain>/admin/mail/inbound-parse with the token in an X-Inbound-Token header. Until a token is set the webhook answers 503 Inbound parsing is not configured, so nobody can inject mail.

Step 12: Allow hosts inside your VPC

The egress backstop stops the krayin user (the web application, the queue worker, the scheduler and krayin-artisan) from connecting to any address in your VPC's own address ranges, which it reads from the instance metadata at every boot. Public addresses work as usual. You can see the ranges, and that the metadata service is refused to Krayin while public sites are not:

sudo nft list set inet cloudimg_krayin_egress vpc4 | grep elements
sudo -u krayin curl -s -m 5 -o /dev/null -w 'metadata: %{http_code}\n' http://169.254.169.254/latest/meta-data/ || true
sudo -u krayin curl -s -m 5 -o /dev/null -w 'public HTTPS: %{http_code}\n' https://www.cloudimg.co.uk/ || true
        elements = { 172.31.0.0/16 }
metadata: 000
public HTTPS: 200

If Krayin must reach a host inside the VPC (an SMTP relay, an IMAP server, a webhook receiver or a VPC interface endpoint on a private address), add its address or CIDR, one per line, to /etc/cloudimg/krayin-egress-allow.conf and apply it. This restarts PHP and the queue worker with the new rules:

echo '172.31.10.25' | sudo tee -a /etc/cloudimg/krayin-egress-allow.conf >/dev/null
sudo systemctl restart krayin-egress-guard
sudo nft list set inet cloudimg_krayin_egress allow4 | grep elements
        elements = { 172.31.10.25 }

Only the addresses you list are opened; the metadata service, link-local, reserved and multicast ranges and IPv6 stay refused whatever the file says.

Step 13: Use your own domain and certificate

Point a DNS name at the instance (an Elastic IP is a good idea), then:

sudo krayin-set-hostname <your-domain>

This creates a new self-signed certificate for the name, pins nginx to it (other names are dropped), sets Krayin's APP_URL (used in links in its e-mails and web forms) and the cross-site guard, and updates the URL and fingerprint in /root/krayin-credentials.txt. To use a certificate from a certificate authority, replace /etc/nginx/ssl/krayin.crt and krayin.key (keep the key 0600 root) and run sudo systemctl reload nginx.

Step 14: Restrict access to your networks

Limit who can reach the web interface in the instance's security group. For example, to allow HTTPS only from your office network:

aws ec2 authorize-security-group-ingress --group-id sg-xxxxxxxx --protocol tcp --port 443 --cidr <your-mgmt-cidr>
aws ec2 revoke-security-group-ingress --group-id sg-xxxxxxxx --protocol tcp --port 443 --cidr 0.0.0.0/0

Krayin locks an e-mail address and client address out of sign-in after five failed attempts (and a client address after twenty), and nginx separately rate limits requests for the sign-in and password-reset pages per client address.

Step 15: Check the security posture

U=$(sudo sed -n 's/^KRAYIN_URL=//p' /root/krayin-credentials.txt); U=${U%/admin/login}
C=/etc/nginx/ssl/krayin.crt
for p in admin/login healthz up; do
  curl -s --cacert "$C" -o /dev/null -w "GET /$p: %{http_code}\n" "${U}/$p"
done
for p in index.php index.php/admin/login .env install _debugbar/open storage/logs/laravel.log; do
  curl -s --cacert "$C" -o /dev/null -w "GET /$p: %{http_code}\n" "${U}/$p"
done
curl -s --cacert "$C" -o /dev/null -w 'GET /admin/dashboard/export-pdf from another site: %{http_code}\n' -H 'Sec-Fetch-Site: cross-site' "${U}/admin/dashboard/export-pdf"
curl -s -o /dev/null -w 'GET http://: %{http_code}\n' "${U/https:/http:}/admin/login"
curl -sk -o /dev/null -w 'GET with an unknown Host: %{http_code}\n' -H 'Host: not-this-server.example' https://127.0.0.1/ || true
for i in $(seq 1 12); do curl -s --cacert "$C" -o /dev/null -w '%{http_code} ' "${U}/admin/login"; done; echo
systemctl show -p IPAddressDeny nginx.service php8.3-fpm.service mysql.service krayin-queue.service
sudo mysql -e "SELECT @@version, @@bind_address, @@skip_networking, @@local_infile"
curl -s http://127.0.0.1/healthz
GET /admin/login: 200
GET /healthz: 200
GET /up: 200
GET /index.php: 404
GET /index.php/admin/login: 404
GET /.env: 404
GET /install: 404
GET /_debugbar/open: 404
GET /storage/logs/laravel.log: 404
GET /admin/dashboard/export-pdf from another site: 403
GET http://: 301
GET with an unknown Host: 000
200 200 200 200 200 200 200 200 200 200 429 429
IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32

IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32

IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32

IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32
@@version   @@bind_address  @@skip_networking   @@local_infile
8.0.46-0ubuntu0.24.04.4 127.0.0.1   1   0
ok
  • The front controller path (/index.php/...), the configuration file, the web installer, logs and the debug bar are refused. Krayin's code, configuration and data live outside the web root, so any other path that is not a Krayin page gets Krayin's own "404 Page Not Found" page. A link from another site cannot trigger the PDF export or a data import (403). Requests for any name other than the configured one are dropped (000: nginx closes the connection).
  • The twelve quick requests for the sign-in page show nginx's per-address limit (429).
  • IPAddressDeny is the metadata-service block on nginx, PHP, MySQL and the queue worker; the scheduler and the first-boot service have the same one.
  • MySQL has networking switched off (skip_networking 1) and refuses LOAD DATA LOCAL.
  • http://127.0.0.1/healthz returns ok for a monitoring agent on the instance.

Step 16: Back up your data

Your data is the MySQL database plus uploaded files in /var/lib/krayin/storage/app and /var/lib/krayin-public. Keep /etc/krayin/env too: its APP_KEY decrypts the values Krayin stores encrypted, and a backup restored without it cannot read them. Copy them to files only root can read, and keep copies off the instance (for example in an encrypted S3 bucket):

sudo sh -c 'umask 077; mysqldump --single-transaction --routines --triggers krayin | gzip > /var/backups/krayin-db-$(date +%F).sql.gz'
sudo sh -c 'umask 077; tar -C /var/lib -czf /var/backups/krayin-files-$(date +%F).tar.gz krayin/storage/app krayin-public'
sudo sh -c 'umask 077; cp /etc/krayin/env /var/backups/krayin-env-$(date +%F)'
sudo ls /var/backups/ | grep krayin
krayin-db-2026-11-08.sql.gz
krayin-env-2026-11-08
krayin-files-2026-11-08.tar.gz

To restore into a new instance of the same version, stop nginx, PHP-FPM and krayin-queue (and krayin-schedule.timer if you switched it on), load the dump into the krayin database with gunzip -c <dump> | sudo mysql krayin, unpack the files archive into /var/lib, put your APP_KEY line into /etc/krayin/env, and start the services again.

EBS snapshots of the instance are a simple way to keep point-in-time copies of the whole server.

Step 17: Updates

Ubuntu, PHP and MySQL security updates install automatically (unattended upgrades). New Krayin releases are published as new versions of this listing: launch the new version, stop its services, copy your database, uploads and the APP_KEY line of /etc/krayin/env across (keeping the new instance's own database password and APP_URL), then run sudo krayin-artisan migrate --force and start the services again.

Step 18: Licences and source

ls /usr/share/doc/krayin/
sed -n '1,6p' /usr/share/doc/krayin/SOURCE.txt
LICENSE
SOURCE.txt
Krayin CRM 2.2.8 on Ubuntu 24.04 by cloudimg - source and changes

Upstream: the Krayin CRM 2.2.8 tag archive, https://github.com/krayin/laravel-crm/archive/refs/tags/v2.2.8.tar.gz
(sha256 beb916c4a73ec7a7acc5f08e9192b0a0687ad28940e221a781484603d1de0fcc; GitHub tag v2.2.8, commit ed6deebfcb8c616eaf352c893a10a6d8e666fc6e).
Licence: MIT (LICENSE, shipped here and in /var/www/krayin). No Krayin source file is modified.

Krayin CRM is licensed under the MIT License by Webkul, and every Krayin package on the image is part of the same open source repository; the image contains no paid or proprietary extension. SOURCE.txt names the exact upstream archive and the files that are not shipped (tests, sample configuration, agent and lint tooling, front-end build configuration, Apache and IIS files, and one library README that carries example keys).

Security notes

  • Secrets. The administrator password is in /root/krayin-credentials.txt until you remove it; Krayin stores only its bcrypt hash. APP_KEY and the MySQL password are in /etc/krayin/env, readable by root and the krayin group only (not by the web server). The TLS key is in /etc/nginx/ssl/krayin.key (root only). No secret is on any command line, in a systemd unit or in a log.
  • Client addresses. nginx gives Krayin the address of the connection; forwarding headers sent by clients are replaced or removed. Behind a load balancer every request therefore comes from the load balancer's address.
  • PHP. The PHP pool runs as krayin, is confined to Krayin's code, data and configuration directories, and cannot start programs.
  • Uploads. Files under /storage/ are served as downloads with a sandboxing Content-Security-Policy and are never executed.
  • Outbound connections. Webhooks, IMAP, SMTP and any other address entered in Krayin are fetched by the krayin user, which the egress backstop keeps away from the metadata service and your VPC's private addresses (Step 12).

Support

cloudimg provides support for this image by email at support@cloudimg.co.uk. Krayin documentation is at devdocs.krayincrm.com.