BentoPDF 2.8.10 on AWS User Guide
Overview
This guide covers launching and using the BentoPDF 2.8.10 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg self-hosted personal apps listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
BentoPDF is an open source PDF toolkit with more than a hundred tools: merge and split, compress, organise, rotate, delete and extract pages, convert to and from images and office formats, add watermarks, page numbers and bookmarks, fill forms, sign, redact, compare, and change permissions and metadata. It is privacy first by design: every PDF operation runs inside your own web browser, so your documents are processed on your own machine and are never uploaded to the server. The server only delivers the web application.
Upstream BentoPDF has no login of its own. The cloudimg image turns it into a private PDF toolkit for your team:
- HTTPS from the first boot. nginx serves BentoPDF on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
- A password unique to your instance. HTTP Basic Auth is required for the user
admin. The password is generated on first boot and written to/var/lib/cloudimg/bentopdf-credentials.txt(root only); nginx stores only a bcrypt hash of it. No password is baked into the image. - Fail closed. nginx refuses to start until first boot has generated the certificate and the password, so the toolkit is never served without a login.
- Hardened defaults. Requests for any host name the instance does not serve are dropped, only
GETandHEADare accepted, cross site requests are refused, nginx is blocked from the instance metadata service and the AMI requires IMDSv2. BentoPDF's own Content Security Policy is served, which limits where the page may load code from and connect to. - Stock upstream release. The official BentoPDF 2.8.10 self hosted release bundle, checked against a pinned SHA-256 and unmodified, with its complete source code on the image.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: BentoPDF 2.8.10 (the static release in /var/www/bentopdf, with the source and licence in /usr/share/bentopdf), nginx, 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 (BentoPDF) and TCP 80 (redirects to HTTPS) from your own networks only.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick BentoPDF 2.8.10 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (BentoPDF itself needs very little on the server, because the PDF work happens in your users' browsers), 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=bentopdf}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| BentoPDF 2.8.10 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot generates the certificate and the password, then nginx starts:
systemctl is-active nginx bentopdf-firstboot
sudo test -f /var/lib/cloudimg/bentopdf-firstboot.done && echo FIRSTBOOT_DONE
Both units are active and the first boot marker is present:
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on the network:
sudo ss -lntp | awk 'NR>1 && $4 !~ /^127\./ {print $4, $6}' | sort
0.0.0.0:22 users:(("sshd",...),("systemd",...))
0.0.0.0:443 users:(("nginx",...))
0.0.0.0:80 users:(("nginx",...))
[::]:22 users:(("sshd",...),("systemd",...))
[::]:443 users:(("nginx",...))
[::]:80 users:(("nginx",...))
Step 4: Get your URL and password
sudo cat /var/lib/cloudimg/bentopdf-credentials.txt
The file is readable by root only and holds the URL, the user name and this instance's password:
BENTOPDF_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
BENTOPDF_USERNAME=admin
BENTOPDF_PASSWORD=<BENTOPDF_PASSWORD>
The URL uses the instance's public DNS name from EC2 instance metadata. The certificate also names the public and private IP addresses, so https://<public-ip>/ works too. To serve BentoPDF under your own DNS name, see Step 8.
Step 5: Sign in from your browser
Open the BENTOPDF_URL in your browser. The certificate is self signed, so your browser asks you to confirm it the first time (or install your own certificate, Step 8). Sign in as admin with the password from Step 4. The browser remembers the login for the session.
The home page lists the tools as cards (Popular Tools first, then every category). Use the search box (Cmd+K or Ctrl+K) to jump to a tool:

Step 6: Work with your PDFs
Merge PDF combines several documents into one. Drop the files on the page (or click to select them), drag the handle on a file to change the order, optionally type a page range such as 1-3, 5 for a file, then choose Merge PDFs. The merged file is built in your browser and downloaded straight away:

Compress PDF reduces the size of a document. Pick the algorithm and level (the recommended defaults suit most files) and choose Compress PDF:

When it finishes, BentoPDF reports the method used and the size saved, and downloads the result:

Every tool has its own address, so you can bookmark or share a link such as https://<your-host>/merge-pdf or https://<your-host>/compress-pdf with your team (they sign in with the same password).
What leaves the browser. Your documents never do. Most tools use only the code served by your instance. A few heavy tools (some conversions, compression engines and OCR) download their open source WebAssembly engine from the public jsDelivr CDN the first time a browser uses them, and digital signature timestamping goes through BentoPDF's own proxy. The Content Security Policy served by the instance allows exactly those destinations. If your users' browsers have no internet access, the tools that need those engines will not run; everything else works.
Step 7: Check the security posture from the command line
Without a login the toolkit is refused, port 80 redirects to HTTPS, the health check stays open for load balancers, and a request for a host name the instance does not serve is dropped without an answer:
H=$(sudo grep '^BENTOPDF_URL=' /var/lib/cloudimg/bentopdf-credentials.txt | sed -E 's#^BENTOPDF_URL=https://([^/]+)/.*#\1#')
curl -s -o /dev/null -w 'http:// -> %{http_code} %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -sk -o /dev/null -w 'no login -> HTTP %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'health -> HTTP %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/healthz"
curl -sk -o /dev/null -w 'unknown host -> %{http_code}\n' --resolve "evil.example:443:127.0.0.1" "https://evil.example/" || true
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/" | grep -o -m1 '401 Authorization Required'
http:// -> 301 https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
no login -> HTTP 401
health -> HTTP 200
unknown host -> 000
401 Authorization Required
000 means nginx closed the connection without answering, which is how it treats any host name it does not serve.
With the password, BentoPDF is served. The password is passed to curl on standard input, so it never appears in the process list or your shell history:
sudo bash -c '
F=/var/lib/cloudimg/bentopdf-credentials.txt
P=$(grep "^BENTOPDF_PASSWORD=" $F | cut -d= -f2-)
H=$(grep "^BENTOPDF_URL=" $F | sed -E "s#^BENTOPDF_URL=https://([^/]+)/.*#\1#")
auth() { printf "user = \"admin:%s\"\n" "$P"; }
auth | curl -K - -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w "with login -> HTTP %{http_code}\n" "https://$H/"
auth | curl -K - -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w "merge tool -> HTTP %{http_code}\n" "https://$H/merge-pdf"
auth | curl -K - -skI --resolve "$H:443:127.0.0.1" "https://$H/" | grep -iE "^cross-origin-(opener|embedder)-policy"
'
with login -> HTTP 200
merge tool -> HTTP 200
cross-origin-opener-policy: same-origin
cross-origin-embedder-policy: credentialless
nginx keeps only a bcrypt hash of the password; the plain text is in the root only credentials file:
sudo sed -E 's/\$2y\$12\$.*/$2y$12$<bcrypt-hash>/' /etc/nginx/.bentopdf-htpasswd
sudo stat -c '%a %U:%G %n' /etc/nginx/.bentopdf-htpasswd /var/lib/cloudimg/bentopdf-credentials.txt
admin:$2y$12$<bcrypt-hash>
640 root:www-data /etc/nginx/.bentopdf-htpasswd
600 root:root /var/lib/cloudimg/bentopdf-credentials.txt
The certificate was generated on this instance and names its own addresses:
H=$(sudo grep '^BENTOPDF_URL=' /var/lib/cloudimg/bentopdf-credentials.txt | sed -E 's#^BENTOPDF_URL=https://([^/]+)/.*#\1#')
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost
nginx cannot reach the instance metadata service, and it will not start until first boot has finished:
systemctl show nginx -p IPAddressDeny | grep 169.254.169.254
systemctl cat nginx | grep -E '^ExecStartPre=/usr/bin/test'
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32
ExecStartPre=/usr/bin/test -f /var/lib/cloudimg/bentopdf-firstboot.done
ExecStartPre=/usr/bin/test -s /etc/nginx/.bentopdf-htpasswd
nginx also refuses a request whose Origin is another web site (HTTP 403), because browsers resend Basic Auth credentials on cross site requests:
H=$(sudo grep '^BENTOPDF_URL=' /var/lib/cloudimg/bentopdf-credentials.txt | sed -E 's#^BENTOPDF_URL=https://([^/]+)/.*#\1#')
curl -sk --resolve "$H:443:127.0.0.1" -H 'Origin: https://evil.example' -d x=1 "https://$H/" | grep -o -m1 '403 Forbidden'
403 Forbidden
Step 8: Your own DNS name and certificate
Point a DNS record at the instance, then set the name BentoPDF is served as. This mints a new self signed certificate for the name, updates nginx and the URL in the credentials file, and keeps the name across reboots:
sudo bentopdf-set-hostname <your-domain>
To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running bentopdf-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/ssl/bentopdf/bentopdf.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/bentopdf/bentopdf.key
sudo nginx -t && sudo systemctl reload nginx
Step 9: Change the password or add users
The login is an nginx Basic Auth file. To set a new password for admin, hand it to htpasswd on standard input (printf is a shell builtin, so the password never appears in the process list; start the line with a space so most shells keep it out of your history), then reload nginx:
printf '%s\n' '<new-password>' | sudo htpasswd -i -B -C 12 /etc/nginx/.bentopdf-htpasswd admin
sudo systemctl reload nginx
Running sudo htpasswd -B -C 12 /etc/nginx/.bentopdf-htpasswd <user> without -i prompts for the password instead. Use it with another user name to give a colleague their own login, and sudo htpasswd -D /etc/nginx/.bentopdf-htpasswd <user> to remove one. Then update or delete BENTOPDF_PASSWORD in /var/lib/cloudimg/bentopdf-credentials.txt so it does not hold an old password.
Step 10: Server components
| Component | Path |
|---|---|
| BentoPDF static release | /var/www/bentopdf |
| Source and licence (AGPL-3.0) | /usr/share/bentopdf/ |
| nginx site | /etc/nginx/sites-available/cloudimg-bentopdf |
| Security headers | /etc/nginx/bentopdf-headers.conf |
| Basic Auth file | /etc/nginx/.bentopdf-htpasswd (bcrypt) |
| Certificate and key | /etc/ssl/bentopdf/ |
| First boot script | /usr/local/sbin/bentopdf-firstboot.sh |
| Host name helper | /usr/local/sbin/bentopdf-set-hostname |
| Credentials | /var/lib/cloudimg/bentopdf-credentials.txt (mode 0600) |
ls -l /usr/share/bentopdf/ | awk 'NR>1{print $1,$5,$9}'
du -sh /var/www/bentopdf | awk '{print $1, $2}'
nginx -v 2>&1
-rw-r--r-- 34523 LICENSE
-rw-r--r-- 755 README.source.txt
-rw-r--r-- 151927366 bentopdf-v2.8.10-source.tar.gz
253M /var/www/bentopdf
nginx version: nginx/1.24.0 (Ubuntu)
Step 11: Security recommendations
- Allow ports 22, 80 and 443 in the security group from your own networks only.
- Keep the per instance password secret, and rotate it if it may have been shared (Step 9).
- Use your own DNS name and a certificate your clients trust (Step 8).
- Keep the operating system patched; unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else.
Support and licensing
BentoPDF is open source under the GNU Affero General Public License v3.0; there is no per user or per CPU licence fee for running it under that licence. The image serves the unmodified upstream release, and the complete source of that exact version is in /usr/share/bentopdf/ (also at https://github.com/alam00000/bentopdf/tree/v2.8.10). If you modify BentoPDF and let others use it over a network, the AGPL requires you to offer them your modified source; BentoPDF's authors also sell a commercial licence for deployments that cannot meet that. The AMI price covers cloudimg's hardening, first boot setup and support.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA