Deployment guide

Pwndoc

Overview

This image runs Pwndoc 1.6.0, the open-source penetration test reporting platform, on Ubuntu 24.04 LTS. Pwndoc replaces the copy-and-paste report workflow with a shared workspace. A reusable vulnerability knowledge base holds your standard findings with their descriptions, remediation advice and CVSS scoring, and each engagement draws from that base instead of starting from a blank document. Testers write findings together with live updates between everyone in the same audit, attach evidence images, and record scope and network hosts. They then export the finished engagement to a Word document generated from your own DOCX template.

Upstream Pwndoc ships only as a Docker Compose stack. This image is a native systemd install with no container runtime. The Node.js backend runs from /opt/pwndoc/backend as an unprivileged pwndoc system account. The Vue/Quasar single-page app is pre-built and served as static files, and a local MongoDB 8.0 Community database holds every audit, finding, template and evidence image.

nginx terminates TLS on port 443 and is the only listener exposed to the network. It serves the single-page app and reverse-proxies /api and /socket.io to the backend on 127.0.0.1:4242, which in turn talks to MongoDB on 127.0.0.1:27017. The backend and the database are bound to loopback and are never directly reachable. Port 80 issues a permanent redirect to HTTPS. Pwndoc sets its session cookies secure and sameSite=strict, so authentication cannot complete over plain HTTP.

What you get:

  • Pwndoc 1.6.0, built from a pinned upstream release commit and run by systemd as the unprivileged pwndoc user.
  • The Pwndoc web UI on :443 over HTTPS, with :80 redirecting to it.
  • A per-VM admin account whose password is generated on first boot and recorded in a root-only file.
  • A per-VM 4096-bit TLS certificate, also generated on first boot, which replaces the private keys that upstream commits to its public repository.
  • Per-VM JWT signing secrets and a per-VM MongoDB credential. None of them is baked into the image.
  • MongoDB 8.0 Community, bound to loopback with authentication enabled, with its data directory on a dedicated Azure data disk at /var/lib/pwndoc.
  • A starter dataset so you can create an audit immediately: the English language, the default DOCX report template, a Security Audit audit type and four vulnerability categories.
  • mongod.service, pwndoc.service and nginx.service as systemd units, enabled and active.
  • An unauthenticated /healthz endpoint for Azure Load Balancer health probes.
  • 24/7 SecureAnyCloud support.

Prerequisites

  • An active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region.
  • Standard_B2s (2 vCPU / 4 GiB RAM) is a good starting point. Size up for larger teams and for audits with many findings and evidence images.
  • NSG inbound rules: allow 22/tcp from your management network and 443/tcp for the web interface. Optionally allow 80/tcp so the HTTP-to-HTTPS redirect works for people who type the bare address.
  • Because Pwndoc holds client engagement data, restrict 443/tcp to your own networks or a VPN rather than opening it to the internet.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Pwndoc by SecureAnyCloud, and select Create. On the Basics tab, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22) and HTTPS (443). Review the dedicated data disk on the Disks tab, then select Review + create and then Create.

Step 2 - Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:pwndoc-ubuntu-24-04:default:latest
az vm create \
  --resource-group <your-rg> \
  --name pwndoc \
  --image sac1647283583153:pwndoc-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name pwndoc --port 443 --priority 1010

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4 - Confirm the services are running

nginx on port 443 is the only public listener. It serves the single-page app and proxies the API to the backend on 127.0.0.1:4242, which talks to MongoDB on 127.0.0.1:27017. Confirm that the services are active and check which sockets are exposed:

systemctl is-active mongod pwndoc nginx pwndoc-firstboot
sudo ss -tlnp | grep -E ':80 |:443 |:4242 |:27017 '

Expected output:

active active active active LISTEN 0 4096 127.0.0.1:27017 0.0.0.0:* users:(("mongod",pid=...)) LISTEN 0 511 127.0.0.1:4242 0.0.0.0:* users:(("node",pid=...)) LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=...)) LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=...))

Ports 4242 and 27017 are bound to 127.0.0.1, not 0.0.0.0. The Pwndoc API and the database are reachable only from the VM itself, so nginx is the single point of ingress.

Step 5 - Retrieve your admin password

On the first boot of every VM, pwndoc-firstboot.service generates a unique administrator password, a unique MongoDB credential and unique JWT signing secrets. It writes the credentials into a root-only file. Confirm that the file exists and is root-only:

sudo ls -l /root/pwndoc-credentials.txt

Expected output:

-rw------- 1 root root 649 ... /root/pwndoc-credentials.txt

To get your admin password, read the file with sudo cat /root/pwndoc-credentials.txt. It contains PWNDOC_URL, PWNDOC_ADMIN_USER, PWNDOC_ADMIN_PASSWORD and MONGO_PASSWORD, all unique to this VM.

The same first-boot service creates the TLS certificate that nginx serves, so the private key on your VM was generated on your VM. This matters because the upstream Pwndoc repository commits working TLS private keys into its source tree, which means every Docker Compose deployment of Pwndoc shares them. This image deletes those keys during the build and generates a fresh 4096-bit key pair per VM instead.

sudo openssl x509 -in /etc/pwndoc/ssl/server.cert -noout -subject -dates

Expected output. The common name is the address your VM resolved for itself, and the dates are one year apart:

subject=O = sac, CN = 10.0.0.21 notBefore=Jul 19 15:25:02 2026 GMT notAfter=Jul 19 15:25:02 2027 GMT

Step 6 - Confirm the stack, the health endpoint and that the API is closed

nginx serves an unauthenticated /healthz endpoint for load balancer probes. Everything else in the API requires a session. Confirm the component versions and that the app answers. Also confirm that an anonymous caller is refused even when it sends a complete, well-formed request body:

node -v mongod --version | head -1 curl -sk -o /dev/null -w 'healthz -> %{http_code}\n' https://127.0.0.1/healthz curl -s -o /dev/null -w 'http redirect -> %{http_code}\n' http://127.0.0.1/ curl -sk -o /dev/null -w 'GET /api/audits -> %{http_code}\n' https://127.0.0.1/api/audits curl -sk -o /dev/null -w 'POST /api/clients -> %{http_code}\n' -X POST https://127.0.0.1/api/clients \
  -H 'Content-Type: application/json' \
  -d '{"company":"Acme","lastname":"Doe","firstname":"Jane","email":"jane@example.com"}'

Expected output:

v22.23.1 db version v8.0.26 healthz -> 200 http redirect -> 301 GET /api/audits -> 401 POST /api/clients -> 401

The POST is deliberately sent with a complete body. Input validation could reject a request that is missing required fields before authentication is ever consulted, which would make an open endpoint look protected. Sending a valid body proves that the 401 came from the authentication layer.

Step 7 - Confirm the administrator bootstrap is closed

Pwndoc creates its first administrator through a one-shot endpoint that stops working as soon as any user exists. First boot has already used it, so an anonymous caller can no longer create an account:

curl -sk https://127.0.0.1/api/users/init
curl -sk -o /dev/null -w 'init replay -> %{http_code}\n' -X POST https://127.0.0.1/api/users/init \
  -H 'Content-Type: application/json' \
  -d '{"username":"someone","password":"Abcdefg1","firstname":"a","lastname":"b"}'

Expected output. The value datas:false means "no longer uninitialized", and the replay is refused:

{"status":"success","datas":false} init replay -> 403

Step 8 - Sign in

Browse to https://<vm-public-ip>/. The certificate is self-signed and unique to your VM, so your browser warns on the first visit. Accept the warning, or replace the certificate as described under Maintenance. Enter admin and the password from Step 5.

You can confirm that the same credential works from the command line. The following block reads the password from the root-only file, checks that a wrong password is rejected, and then checks that the real one is accepted:

PW=$(sudo grep '^PWNDOC_ADMIN_PASSWORD=' /root/pwndoc-credentials.txt | cut -d= -f2-)
curl -sk -o /dev/null -w 'wrong password -> %{http_code}\n' -X POST https://127.0.0.1/api/users/token \
  -H 'Content-Type: application/json' -d '{"username":"admin","password":"Definitely-Wrong-Pw1"}'
curl -sk -X POST https://127.0.0.1/api/users/token -H 'Content-Type: application/json' \
  -d "$(printf '{"username":"admin","password":"%s"}' "$PW")" | grep -o '"token"' | head -1

Expected output:

wrong password -> 401 "token"

Step 9 - Create your first audit

After you sign in, you land on Audits, which is empty on a new VM. Select New Audit, give the engagement a name and pick an assessment type. The image ships with a Security Audit type that is already bound to the default report template, and with English configured as a language, so the dialog has options to choose from immediately.

Inside an audit, fill in the General information and scope, record hosts and services under Network, and add findings. Each finding carries a description, observation, remediation, CVSS vector and category. You can write findings from scratch or pull them from the shared vulnerability knowledge base under Vulnerabilities, so recurring issues keep consistent wording and scoring across every engagement. Several people can edit the same audit at once, with changes appearing live.

Step 10 - Report templates and Word export

Pwndoc generates reports from DOCX templates, so the finished document comes out in your own house style. Open Data and then Templates. The image ships with the upstream Default Template already loaded, which is what lets the Security Audit type export a report out of the box.

To use your own report design, select Create Template and upload your DOCX. Then bind it to an audit type under Data and then Custom Data. Upstream documents the available template variables. To export, open an audit and choose the download action from the audit list.

Step 11 - Confirm engagement data lives on the dedicated disk

Every audit, finding, evidence image and uploaded report template lives under /var/lib/pwndoc, a dedicated Azure data disk that is captured into the image and re-provisioned on every VM. Confirm it with:

df -h /var/lib/pwndoc | tail -1 findmnt -no SOURCE,TARGET,FSTYPE /var/lib/pwndoc grep pwndoc /etc/fstab
sudo ls /var/lib/pwndoc/report-templates

Expected output. The device name varies between VM sizes, which is why the mount is keyed by UUID rather than by device path:

/dev/sdc 30G 202M 28G 1% /var/lib/pwndoc /dev/sdc /var/lib/pwndoc ext4 UUID=8c58cbdc-4ef0-4ef0-8027-9fac61b83a85 /var/lib/pwndoc ext4 defaults,nofail 0 2 Default Template.docx

Maintenance

  • Back up your engagements. Everything Pwndoc stores lives in MongoDB's data files under /var/lib/pwndoc. Snapshot the data disk, or use mongodump/mongorestore with the credential in /root/pwndoc-credentials.txt for logical backups. Pwndoc also has its own Data and then Import / Export page for portable dumps.
  • Add teammates. Sign in as admin and open Data and then Collaborators. There is no self-registration, so accounts exist only when an administrator creates them. Manage roles under Data and then Roles.
  • Replace the TLS certificate. The per-VM certificate is self-signed, which is why browsers warn on the first visit. For production, install your own certificate and key at /etc/pwndoc/ssl/server.cert and /etc/pwndoc/ssl/server.key, and keep the key at 0640 root:pwndoc. Then run sudo systemctl restart nginx pwndoc. Both nginx and the backend read that same pair.
  • Restrict access. Pwndoc holds client findings. Keep 443/tcp limited to your own networks or a VPN in the NSG rather than exposing it to the internet. If it must be public, prefer an Azure Application Gateway or your own reverse proxy with a CA-issued certificate.
  • Rotate credentials. Change the admin password from the profile menu in the UI. The MongoDB credential lives in /etc/pwndoc/pwndoc.env and the JWT signing secrets live in /opt/pwndoc/backend/src/config/config.json. Update those and run sudo systemctl restart pwndoc to apply. Rotating the JWT secrets invalidates all existing sessions.
  • Security updates. Unattended security upgrades are enabled, so the OS keeps itself patched. Reboot when a new kernel is installed.

Support

SecureAnyCloud provides 24/7/365 expert technical support for this image. Contact sales@secureanycloud.com. Pwndoc is licensed under the MIT License. SecureAnyCloud provides this image, and additional charges apply for build, maintenance and 24/7 support.

Deploy on Azure

Launch Pwndoc on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure