django-helpdesk 2.6.1
Overview
This image runs django-helpdesk 2.6.1 on Ubuntu 24.04 on Amazon EC2. It is one version of the SecureAnyCloud Helpdesk, ITSM and Project Management listing on AWS Marketplace, which you pick as the software version when you subscribe and launch. This guide covers launching and using it.
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. A searchable knowledge base answers the questions that come up again and again, and 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.
What you get:
- 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 and data unique to your instance. The Django secret key and the password of the
helpdeskadminstaff account are generated on your instance's first boot and written to root-only files. The PostgreSQL database itself is created on first boot too. The image ships no database at all, so no account, password or password hash from the build can be in it. django-helpdesk connects to PostgreSQL over the local socket as its own system user, so there is no database password to manage or leak. - 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
nosniffand 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.htmand.htmluploads. - 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 a helpdesk that only your own staff use.
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 select your Region, then choose Launch through EC2. Choose m5.large, your key pair and a security group as described in Prerequisites.
Alternatively, launch with the AWS CLI, replacing 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}]'Connect to your instance
Connect over SSH as the ubuntu user with your key pair:
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
First boot creates the PostgreSQL database and the helpdesk schema, so allow about 30 seconds after the instance reaches Running before you sign in. First boot measured 25 to 26 s on fresh t3.medium instances launched from this AMI, with about 14 s of it creating the PostgreSQL cluster, schema and queues. Then check that the units are active:
systemctl is-active postgresql@16-main django-helpdesk nginx django-helpdesk-firstbootactive active active activeTo see how long first boot took on your instance, run:
sudo journalctl -u django-helpdesk-firstboot -o cat | grep -E 'schema \+ seed took|first boot complete'schema + seed took <n> s first boot complete in <n> s; django-helpdesk will be served at https://<public-ip>/gunicorn and PostgreSQL listen on loopback only, and nginx serves ports 80 and 443. Check the listeners:
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 [::]:443Get your URL and staff password
Print the credentials file:
sudo cat /root/django-helpdesk-credentials.txtThe file holds the address the helpdesk is served as (your instance's public IP address by default), the staff user name and the password generated on this instance:
HELPDESK_URL=https://<public-ip>/ HELPDESK_ADMIN_USER=helpdeskadmin HELPDESK_ADMIN_PASSWORD=<24 letters and digits>The database has no password. The helpdesk PostgreSQL role can only log in over the local socket as the helpdesk system user. To open the database yourself, use sudo -u postgres psql helpdesk.
Only root can read the credentials file, and only root and the helpdesk service can read the environment file that holds the Django secret key. 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.sh600 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 lineOpen 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.
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.
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 a helpdesk that only your own staff use, 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.
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. You can search, sort and filter the table, and save a filter as a query.
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.
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.
Queues, knowledge base categories and articles, preset 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 the queues 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, set a new password for a staff account interactively:
sudo helpdesk-manage changepassword helpdeskadminAttachments
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. Check the nginx rule:
grep -A9 'location /media/' /etc/nginx/sites-available/sac-django-helpdesklocation /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.
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, and 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.comThen restart the application:
sudo systemctl restart django-helpdeskWith 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, and fill in the mailbox type, host, port, user and password. Then run the collector on a schedule, using a systemd timer or cron:
sudo helpdesk-manage get_emailYour 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. It also 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 nginxSecurity 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: 403The 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. Check each setting:
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/sac-django-helpdeskIPAddressDeny=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. Check it:
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 subjectAltNamesubject=CN = <public-ip>, O = sac 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:localhostVerify the whole workflow yourself
The image ships the check it is built and tested with. Over HTTPS on the configured name, the check does the following:
- 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, and finds the ticket in the staff list.
- Adds a follow-up with an attachment and resolves the ticket, then checks the change reached the database.
- Downloads the attachment and checks it is served as a download, and checks an
.htmlattachment is refused. - Deletes the ticket so nothing is left behind.
Run it:
sudo bash /usr/local/sbin/django-helpdesk-verify-workflow.shWORKFLOW_OK ticket=<id> status=Resolved followups=2 attachment=download-only key-link=ok no-key=403To print the installed versions, run:
/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>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 mediaCopy the files off the instance to your own backup storage, or snapshot the EBS volume.
Server components
These are the components, their versions and their locations:
- 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 and the local socket, with data in /var/lib/postgresql/16/main (created on first boot)
- nginx 1.24: /etc/nginx/sites-available/sac-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 and LICENSE.3RDPARTY
Logs: journalctl -u django-helpdesk (application errors), journalctl -u django-helpdesk-firstboot (first boot), /var/log/nginx/ and /var/log/postgresql/.
Troubleshooting
The site does not answer. First boot may still be running, because it creates the database, so give it about 30 seconds. Or first boot failed, in which case nginx and the application stay stopped by design. Check it:
systemctl show -p ActiveState -p Result django-helpdesk-firstboot.serviceResult=success ActiveState=activeIf 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, and open 443 only to the people who need the helpdesk.
- Change the
helpdeskadminpassword after your first sign-in and give each support agent a named account. - Keep
/root/django-helpdesk-credentials.txtand/opt/django-helpdesk/helpdesk.envas shipped. The environment file holds the Django secret key, and 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 SecureAnyCloud'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. SecureAnyCloud is not affiliated with or endorsed by either. The image ships with no tickets.
- Email: sales@secureanycloud.com
- Website: https://secureanycloud.com
- Support: 24/7, with a 24 hour response SLA
Deploy on AWS
Launch django-helpdesk 2.6.1 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