Applications AWS

django-helpdesk 2.6.1 on AWS User Guide

| Product: django-helpdesk 2.6.1 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the django-helpdesk 2.6.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg Helpdesk, ITSM and Project Management listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

django-helpdesk is a mature open source ticket tracker built on the Django web framework. Requests arrive in queues with their own priorities; anyone can raise a ticket from a public web portal without an account; support staff sign in to a dashboard, filter and search the ticket list, take ownership, add public or private follow ups, record time spent and resolve tickets; and a searchable knowledge base answers the questions that come up again and again. A REST API covers the same ground for integrations.

django-helpdesk is a Django application rather than a finished server, so the image supplies the production project around it: django-helpdesk 2.6.1 in a dedicated Python virtual environment, Django 5.2 LTS, gunicorn bound to loopback, nginx in front, and PostgreSQL 16 on the same instance.

The cloudimg image adds:

  • HTTPS from the first boot. nginx serves the helpdesk on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
  • Secrets unique to your instance. The Django secret key, the PostgreSQL password and the password of the helpdeskadmin staff account are generated on your instance's first boot and written to root only files. No account and no secret ships in the image.
  • Fail closed. The application server and nginx refuse to start until first boot has finished, so a new instance is never reachable before its secrets exist.
  • Host names from configuration, never from a request. The names the helpdesk answers to come from your instance's EC2 metadata (or a name you set). Requests for any other host name get no answer, and the links django-helpdesk puts in ticket notifications and password reset mails are always built from the configured name.
  • Attachments always download. Files attached to tickets are served as downloads with nosniff and a sandboxing content security policy, so an attachment can never run script in a staff member's browser. django-helpdesk 2.6.1 also refuses .htm and .html uploads.
  • Loopback only, metadata blocked. gunicorn (127.0.0.1:8020) and PostgreSQL (127.0.0.1:5432) are reachable only on the instance. The only open ports are 22, 80 and 443. gunicorn, nginx and PostgreSQL are blocked from the instance metadata service, and the AMI requires IMDSv2.
  • The real client address. nginx passes the address of the actual connection to the application, never a header a client can forge.
  • Current release. 2.6.1 fixes every published django-helpdesk security advisory up to October 2026, including the stored cross site scripting issues in earlier releases.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: django-helpdesk 2.6.1 (the PyPI wheel, checksum verified), Django 5.2 LTS, gunicorn, whitenoise, psycopg2, PostgreSQL 16, nginx, Python 3.12, 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), TCP 443 (the helpdesk) and TCP 80 (redirects to HTTPS). Allow 22 from your own networks only. The public portal is meant to be reached by the people who raise tickets, so open 443 to them, or keep it to your own networks for an internal helpdesk.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick django-helpdesk 2.6.1 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.

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=django-helpdesk}]'

Step 2: Connect to your instance

Version SSH user
django-helpdesk 2.6.1 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

First boot takes about half a minute. Then the four units are active:

systemctl is-active postgresql django-helpdesk nginx django-helpdesk-firstboot
active
active
active
active

gunicorn and PostgreSQL listen on loopback only; nginx serves ports 80 and 443:

sudo ss -tlnp | awk 'NR==1 || /gunicorn|postgres|nginx|sshd/' | awk '{print $1, $4}'
State Local
LISTEN 0.0.0.0:80
LISTEN 0.0.0.0:22
LISTEN 0.0.0.0:443
LISTEN 127.0.0.1:8020
LISTEN 127.0.0.1:5432
LISTEN [::]:80
LISTEN [::]:22
LISTEN [::]:443

Step 4: Get your URL and staff password

sudo cat /root/django-helpdesk-credentials.txt

The file holds the address the helpdesk is served as (your instance's public IP address by default), the staff user name, and the two passwords generated on this instance:

HELPDESK_URL=https://<public-ip>/
HELPDESK_ADMIN_USER=helpdeskadmin
HELPDESK_ADMIN_PASSWORD=<24 letters and digits>
HELPDESK_DB_PASSWORD=<32 letters and digits>

The credentials file is readable by root only and the environment file that holds the Django secret key by root and the helpdesk service only. A checker on the image proves that none of these secrets reached the system journal, any log, /var/backups, shell history or a command line:

sudo stat -c '%a %U:%G %n' /root/django-helpdesk-credentials.txt /opt/django-helpdesk/helpdesk.env /etc/ssl/django-helpdesk/django-helpdesk.key
sudo bash /usr/local/sbin/django-helpdesk-secret-leak-check.sh
600 root:root /root/django-helpdesk-credentials.txt
640 root:helpdesk /opt/django-helpdesk/helpdesk.env
600 root:root /etc/ssl/django-helpdesk/django-helpdesk.key
LEAKCHECK_OK no per-instance secret in the journal, any log, /var/backups, history, MOTD, /etc or a command line

Step 5: Open the public portal

Open HELPDESK_URL in your browser. The certificate is self signed (generated on your instance), so accept the browser warning once, or set your own name and certificate (Step 11). The public portal is the front door: anyone who can reach it can raise a ticket or search the knowledge base, without an account.

The django-helpdesk public portal over HTTPS with the Submit a Ticket form and the knowledge base categories

Step 6: Raise a ticket and follow it

Choose Submit a Ticket, pick the queue that matches the request, give a summary, a description and an email address, and submit. The image ships with two queues, IT Support and General Enquiries, so this works straight away.

When you submit, django-helpdesk opens the ticket's own page. Its link carries the ticket's private key: bookmark it, because it is how the person who raised the ticket follows progress without an account. When outbound email is set up (Step 10) the same link is emailed to them.

The public ticket page showing the new ticket, its reference, status and description

Since django-helpdesk 2.6.1 a ticket can only be opened anonymously with its private key; a ticket number and email address alone are refused. The upstream View a Ticket lookup form (ticket number and email) cannot find a ticket without that key, so this image switches it off (HELPDESK_VIEW_A_TICKET_PUBLIC=false). The setting is in /opt/django-helpdesk/helpdesk.env if you want the form back for signed in users.

Public ticket submission is on, which is what a public helpdesk needs. For an internal helpdesk, keep port 443 to your own networks, or set HELPDESK_SUBMIT_A_TICKET_PUBLIC=false in /opt/django-helpdesk/helpdesk.env and restart the service so only signed in staff can raise tickets.

Step 7: Sign in as staff and work a ticket

Browse to <HELPDESK_URL>login/ and sign in as helpdeskadmin with the password from Step 4, then change it under your user menu. All Tickets shows every request across every queue, with its status, age, owner and submitter; the table can be searched, sorted, filtered and saved as a query.

The staff ticket list showing tickets with their status, creation time, owner and submitter

Open a ticket. The Respond area is where the work happens: write a follow up, set the new status, decide whether the update is public (public updates are what the person who raised the ticket sees on its page), attach a file and optionally record time spent. The follow up and the status change are written into the ticket's history.

A ticket in the staff interface with its details, follow ups and the respond form

Step 8: Dashboard, reports and knowledge base

Dashboard summarises the tickets you own, unassigned tickets waiting to be picked up and the age of everything open. Reports breaks tickets down by queue, status, priority and month. Knowledgebase holds articles grouped into categories; the image ships one Getting Started article.

The staff dashboard with ticket counts by age, tickets assigned to the signed in user and unassigned tickets

Queues, knowledge base categories and articles, pre set replies and users are managed in the Django admin at <HELPDESK_URL>admin/. For each queue set a title and a slug (the slug becomes the ticket reference prefix) and tick Allow public submission if the queue should appear on the public form. You can list them from the command line too:

sudo helpdesk-manage shell --no-imports -c "from helpdesk.models import Queue; print([(q.slug, q.title, q.allow_public_submission) for q in Queue.objects.all()])"
[('general-enquiries', 'General Enquiries', True), ('it-support', 'IT Support', True)]

helpdesk-manage runs any Django management command as the helpdesk service user with the instance's settings. For example, to set a new password for a staff account interactively:

sudo helpdesk-manage changepassword helpdeskadmin

Step 9: Attachments

Staff and the people raising tickets can attach files (text, PDF, office documents, PNG and JPEG images and emails; django-helpdesk 2.6.1 refuses .htm and .html). Attachments are stored under /opt/django-helpdesk/site/media/helpdesk/attachments/ in a directory named after the ticket's private key, and nginx always serves them as downloads:

grep -A9 'location /media/' /etc/nginx/sites-available/cloudimg-django-helpdesk
    location /media/ {
        alias /opt/django-helpdesk/site/media/;
        autoindex off;
        access_log off;
        types { }
        default_type application/octet-stream;
        add_header Content-Disposition "attachment" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header Content-Security-Policy "default-src 'none'; sandbox" always;
        add_header Strict-Transport-Security "max-age=31536000" always;

The nginx access log records the path of each request but never its query string, so the private keys in ticket links do not end up in the log.

Step 10: Optional outbound email and email to ticket

Email is optional and switched off by default. The helpdesk works fully without a mail server; until you configure one, notification mails are discarded (never written to a log, because they carry ticket keys and password reset links). To send them through your own SMTP server, add the settings to /opt/django-helpdesk/helpdesk.env:

EMAIL_HOST=smtp.example.com
EMAIL_PORT=587
EMAIL_HOST_USER=helpdesk@example.com
EMAIL_HOST_PASSWORD=<your-smtp-password>
EMAIL_USE_TLS=true
DEFAULT_FROM_EMAIL=helpdesk@example.com

then restart the application:

sudo systemctl restart django-helpdesk

With outbound email on, Forgotten your password? on the sign in page sends a reset link. That link, like every ticket link in a notification, is built from the name the helpdesk is configured with (Step 11), never from the host name a visitor's request claims.

To collect tickets from a mailbox, open the queue in the Django admin, tick Allow email submission, fill in the mailbox type, host, port, user and password, then run the collector on a schedule (a systemd timer or cron):

sudo helpdesk-manage get_email

Step 11: Your own DNS name and certificate

Point a DNS record at the instance, then set the name the helpdesk is served as. This mints a new self signed certificate for the name, updates nginx, Django's allowed host names and trusted origins, the domain used in ticket and password reset links, and the URL in /root/django-helpdesk-credentials.txt, and keeps the name across reboots:

sudo django-helpdesk-set-hostname <your-domain>

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

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

Step 12: Security checks

A request for a host name the instance does not serve gets no answer, port 80 redirects to HTTPS, the staff pages need a sign in, and a ticket cannot be opened without its key:

H=$(sudo sed -nE 's#^HELPDESK_URL=https://([^/]+)/.*#\1#p' /root/django-helpdesk-credentials.txt)
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: evil.example' http://127.0.0.1/ || true
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -sk -o /dev/null -w 'staff list without signing in: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/tickets/"
curl -sk -o /dev/null -w 'ticket without its key: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/view/?ticket=it-support-1&email=someone@example.com"
unknown host name: 000
port 80: 301 -> https://<public-ip>/
staff list without signing in: 302
ticket without its key: 403

The application runs unprivileged in a sandbox, gunicorn, nginx and PostgreSQL cannot reach the instance metadata service, and nginx passes the real client address from the connection:

systemctl show django-helpdesk -p User -p ProtectSystem -p NoNewPrivileges -p IPAddressDeny
systemctl show nginx postgresql@16-main -p IPAddressDeny
grep -E 'proxy_set_header (X-Forwarded-For|X-Real-IP|X-Forwarded-Host)' /etc/nginx/sites-available/cloudimg-django-helpdesk
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
User=helpdesk
ProtectSystem=full
NoNewPrivileges=yes
IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32

IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Host "";

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

H=$(sudo sed -nE 's#^HELPDESK_URL=https://([^/]+)/.*#\1#p' /root/django-helpdesk-credentials.txt)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = <public-ip>, O = cloudimg
X509v3 Subject Alternative Name:
    IP Address:<public-ip>, DNS:ec2-<public-ip-dashed>.compute-1.amazonaws.com, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost

Step 13: Verify the whole workflow yourself

The image ships the check it is built and tested with. Over HTTPS on the configured name it submits a ticket through the public form, opens it with its private key (and proves it is refused without the key), proves a wrong staff password is refused, signs in, finds the ticket in the staff list, adds a follow up with an attachment and resolves it, checks the change reached the database, downloads the attachment and checks it is served as a download, checks an .html attachment is refused, then deletes the ticket so nothing is left behind:

sudo bash /usr/local/sbin/django-helpdesk-verify-workflow.sh
WORKFLOW_OK ticket=<id> status=Resolved followups=2 attachment=download-only key-link=ok no-key=403

The installed versions:

/opt/django-helpdesk/venv/bin/python -c "import django, importlib.metadata as m; print('django-helpdesk', m.version('django-helpdesk'), '| Django', django.get_version(), '| gunicorn', m.version('gunicorn'))"
django-helpdesk 2.6.1 | Django 5.2.<n> | gunicorn <version>

Step 14: Backups

Back up the database and the attachments. The dump goes to a root only directory; never copy /opt/django-helpdesk/helpdesk.env into a backup location, because it holds the Django secret key and the database password:

sudo install -d -o root -g root -m 0700 /var/backups/django-helpdesk
sudo sh -c 'umask 077; runuser -u postgres -- pg_dump helpdesk | gzip > /var/backups/django-helpdesk/helpdesk-$(date -u +%Y%m%d).sql.gz'
sudo tar -C /opt/django-helpdesk/site -czf /var/backups/django-helpdesk/media-$(date -u +%Y%m%d).tar.gz media

Copy the files off the instance to your own backup storage, or snapshot the EBS volume.

Step 15: Server components

Component Version Path
django-helpdesk 2.6.1 /opt/django-helpdesk/venv
Django 5.2 LTS /opt/django-helpdesk/venv
Django project /opt/django-helpdesk/site
gunicorn django-helpdesk.service, 127.0.0.1:8020
PostgreSQL 16 postgresql@16-main.service, 127.0.0.1:5432, /var/lib/postgresql/16/main
nginx 1.24 /etc/nginx/sites-available/cloudimg-django-helpdesk
Settings /opt/django-helpdesk/helpdesk.env (root:helpdesk, 0640)
Attachments /opt/django-helpdesk/site/media/helpdesk/attachments
Certificate and key /etc/ssl/django-helpdesk/
First boot django-helpdesk-firstboot.service, /usr/local/sbin/django-helpdesk-firstboot.sh
Host name helper /usr/local/sbin/django-helpdesk-set-hostname
Management commands /usr/local/sbin/helpdesk-manage
Credentials /root/django-helpdesk-credentials.txt (0600)
Licence BSD-3-Clause /usr/share/doc/django-helpdesk/LICENSE, LICENSE.3RDPARTY

Logs: journalctl -u django-helpdesk (application errors), journalctl -u django-helpdesk-firstboot (first boot), /var/log/nginx/, /var/log/postgresql/.

Troubleshooting

The site does not answer. First boot may still be running, or it failed (then nginx and the application stay stopped by design). Check it:

systemctl show -p ActiveState -p Result django-helpdesk-firstboot.service
Result=success
ActiveState=active

If Result is not success, read sudo journalctl -u django-helpdesk-firstboot --no-pager.

The browser gets no answer for a name you use. The helpdesk answers only to its configured name, the instance's own EC2 addresses and names, and localhost. Set your name with sudo django-helpdesk-set-hostname <your-domain> (Step 11).

The staff password is lost. Set a new one with sudo helpdesk-manage changepassword helpdeskadmin.

Security recommendations

  • Allow port 22 from your own networks only; open 443 only to the people who need the helpdesk.
  • Change the helpdeskadmin password after your first sign in and give each support agent a named account.
  • Keep /root/django-helpdesk-credentials.txt and /opt/django-helpdesk/helpdesk.env as shipped. The environment file holds the Django secret key; anyone who reads it can forge sessions.
  • Use your own DNS name and a certificate your users trust (Step 11).
  • Keep the operating system patched; unattended security upgrades are on.
  • Back up the database and attachments regularly (Step 14).

Support and licensing

django-helpdesk is open source under the BSD 3-Clause licence (copies of its licence and its third party notices are in /usr/share/doc/django-helpdesk/); there is no per user or per CPU licence fee. The AMI price covers cloudimg's hardening, first boot setup and support. django-helpdesk is a trademark of its respective owners and Django is a registered trademark of the Django Software Foundation; cloudimg is not affiliated with or endorsed by either. Screenshots show demonstration data; the image ships with no tickets.

  • Email: support@cloudimg.co.uk
  • Website: https://www.cloudimg.co.uk
  • Support: 24/7, with a 24 hour response SLA