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
pwndocuser. - The Pwndoc web UI on
:443over HTTPS, with:80redirecting to it. - A per-VM
adminaccount 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 Auditaudit type and four vulnerability categories. mongod.service,pwndoc.serviceandnginx.serviceas systemd units, enabled and active.- An unauthenticated
/healthzendpoint 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/tcpfrom your management network and443/tcpfor the web interface. Optionally allow80/tcpso the HTTP-to-HTTPS redirect works for people who type the bare address. - Because Pwndoc holds client engagement data, restrict
443/tcpto 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 1010Step 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.txtExpected output:
-rw------- 1 root root 649 ... /root/pwndoc-credentials.txtTo 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 -datesExpected 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 GMTStep 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 -> 401The 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 -> 403Step 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 -1Expected 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-templatesExpected 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.docxMaintenance
- Back up your engagements. Everything Pwndoc stores lives in MongoDB's data files under
/var/lib/pwndoc. Snapshot the data disk, or usemongodump/mongorestorewith the credential in/root/pwndoc-credentials.txtfor 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.certand/etc/pwndoc/ssl/server.key, and keep the key at0640 root:pwndoc. Then runsudo systemctl restart nginx pwndoc. Both nginx and the backend read that same pair. - Restrict access. Pwndoc holds client findings. Keep
443/tcplimited 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.envand the JWT signing secrets live in/opt/pwndoc/backend/src/config/config.json. Update those and runsudo systemctl restart pwndocto 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