PrivateBin 2.0.5 on AWS User Guide
Overview
This guide covers launching and using the PrivateBin 2.0.5 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg identity, secrets and PKI listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
PrivateBin is a minimalist, open source pastebin where the server has zero knowledge of what it stores. Your browser encrypts every paste with 256 bit AES in GCM mode before it is uploaded, and the key that decrypts it is the part of the link after #, which browsers never send to a server. The server only ever holds ciphertext. Teams use it to hand over configuration snippets, logs, one time secrets and incident notes without leaving them readable in chat history or on a third party service. Pastes can expire after minutes, days or a year, burn after the first reading, carry an extra password, and be formatted as plain text, source code or Markdown.
Upstream PrivateBin lets anyone who can reach it create pastes. The cloudimg image makes it your team's private service:
- HTTPS from the first boot. nginx serves PrivateBin on port 443 with a certificate generated on your instance's first boot, and port 80 redirects to HTTPS. Browsers only allow PrivateBin's in browser encryption on HTTPS (or localhost), so this is what makes it work at all on a server.
- Only you can create pastes. Creating, commenting on and deleting pastes needs HTTP Basic Auth as the user
admin, with a password generated on the first boot and written to/root/privatebin-credentials.txt(root only). nginx stores only a SHA-512 crypt hash of it. No password is baked into the image, so your instance is never an anonymous paste host on the internet. - Anyone you send a link to can read it. Opening a paste link needs no login: the page, its scripts and the encrypted paste are served by its id, and the reader's browser decrypts it with the key in the link. Without the full link there is nothing to read.
- Fail closed. nginx refuses to start until first boot has generated the certificate and the password.
- Locked down serving. Only the PrivateBin entry point and its static files are served (configuration, libraries and templates answer 404); requests for host names the instance does not serve are dropped; cross site requests are refused; PrivateBin's Content Security Policy only lets the page talk to your own instance; nginx and PHP are blocked from the instance metadata service and the AMI requires IMDSv2.
- Your pastes on their own disk. The paste store is a separate 10 GiB EBS volume mounted at
/var/lib/privatebin-data, outside the web root, that you can grow, snapshot and back up on its own. - The official release, verified, and fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: PrivateBin 2.0.5 (the official release in /var/www/privatebin, licence in /usr/share/doc/privatebin), nginx, PHP 8.3 FPM, 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) from your own networks only, and TCP 443 (PrivateBin) and TCP 80 (redirects to HTTPS) from the networks your team and the people you share pastes with use.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick PrivateBin 2.0.5 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (PrivateBin itself needs very little, because encryption happens in the browser), your key pair, and a security group as above. The AMI already carries the 10 GiB paste store volume; you can make it larger on the launch page.
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=privatebin}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| PrivateBin 2.0.5 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 in front of PHP 8.3 FPM:
systemctl is-active nginx php8.3-fpm
sudo test -f /var/lib/cloudimg/privatebin-firstboot.done && echo FIRSTBOOT_DONE
Both services are active and the first boot marker is present:
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on the network (PHP FPM uses a local socket and the DNS resolver listens on loopback only):
sudo ss -lntp | awk 'NR>1 {print $4, $6}' | sed -E 's/pid=[0-9]+,fd=[0-9]+/.../g' | sort
0.0.0.0:22 users:(("sshd",...),("systemd",...))
0.0.0.0:443 users:(("nginx",...),("nginx",...),("nginx",...))
0.0.0.0:80 users:(("nginx",...),("nginx",...),("nginx",...))
127.0.0.53%lo:53 users:(("systemd-resolve",...))
127.0.0.54:53 users:(("systemd-resolve",...))
[::]:22 users:(("sshd",...),("systemd",...))
[::]:443 users:(("nginx",...),("nginx",...),("nginx",...))
[::]:80 users:(("nginx",...),("nginx",...),("nginx",...))
Step 4: Get your URL and password
sudo cat /root/privatebin-credentials.txt
The file is readable by root only and holds the URL, the user name and this instance's password:
# PrivateBin 2.0.5 - generated on first boot by privatebin-firstboot.service
# These credentials are unique to this instance; none was baked into the image. nginx keeps only a
# SHA-512 crypt hash of the password. Keep this file secret.
PRIVATEBIN_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
PRIVATEBIN_USERNAME=admin
PRIVATEBIN_PASSWORD=<PRIVATEBIN_PASSWORD>
#
# Open PRIVATEBIN_URL in a browser (self-signed certificate minted on this instance's first boot;
# port 80 redirects to https) and sign in with the username and password above to create pastes.
# Anyone you send a paste link to can open it without signing in: the key that decrypts it is in
# the part of the link after '#', which the browser never sends to the server.
# Change the password: sudo privatebin-set-password
# Change the name it serves as: sudo privatebin-set-hostname <your-dns-name>
# User guide: https://www.cloudimg.co.uk/guides/privatebin-2-0-5-on-ubuntu-24-04-aws/
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 PrivateBin under your own DNS name, see Step 8.
Step 5: Create a paste
Open the PRIVATEBIN_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.
Type or paste your text into the Editor. Along the top you choose when the paste Expires (5 minutes to 1 year, or never), Burn after reading (deleted as soon as it is opened once), Open discussion (readers may add comments; posting a comment needs the login too), an optional Password that readers must also enter, and the Format: plain text, source code with syntax highlighting, or Markdown. Preview shows the formatted result.

Choose Create. Your browser encrypts the text, uploads only the ciphertext, and shows the link to the paste. The part after # is the key: it was generated in your browser and the server never sees it. Copy link copies the whole link; Delete data removes the paste from the server straight away:

Step 6: Share the link
Send the complete link to the person who needs it. They open it in their browser with no login: their browser downloads the ciphertext by its id and decrypts it with the key in the link. Here a fresh browser with no credentials opens the link and reads the paste, with a notice of when it expires:

Anyone holding the full link can read the paste until it expires, so treat the link like the secret itself: send it over a channel you trust, use Burn after reading for one time secrets, and add a Password (sent separately) for anything sensitive. A reader cannot create pastes or post comments on your instance without the login: if they choose New, or try to create a paste or comment, their browser asks for the user name and password.
Step 7: Check the security posture from the command line
Port 80 redirects to HTTPS, the page that creates pastes and the upload itself are refused without a login, the health check stays open for load balancers, PrivateBin's configuration is never served, and a request for a host name the instance does not serve is dropped without an answer:
H=$(sudo grep '^PRIVATEBIN_URL=' /root/privatebin-credentials.txt | sed -E 's#^PRIVATEBIN_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 'create page -> HTTP %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'anonymous create -> HTTP %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'X-Requested-With: JSONHttpRequest' --data '{}' "https://$H/"
curl -sk -o /dev/null -w 'health check -> HTTP %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/healthz"
curl -sk -o /dev/null -w 'config file -> HTTP %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/cfg/conf.php"
curl -sk -o /dev/null -w 'unknown host -> %{http_code}\n' --resolve "evil.example:443:127.0.0.1" "https://evil.example/" || true
http:// -> 301 https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
create page -> HTTP 401
anonymous create -> HTTP 401
health check -> HTTP 200
config file -> HTTP 404
unknown host -> 000
000 means nginx closed the connection without answering, which is how it treats any host name it does not serve.
With the password, the PrivateBin page is served together with its Content Security Policy. 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=/root/privatebin-credentials.txt
P=$(grep "^PRIVATEBIN_PASSWORD=" $F | cut -d= -f2-)
H=$(grep "^PRIVATEBIN_URL=" $F | sed -E "s#^PRIVATEBIN_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 "create page with login -> HTTP %{http_code}\n" "https://$H/"
auth | curl -K - -sk --resolve "$H:443:127.0.0.1" "https://$H/" | grep -o "<title>[^<]*</title>"
auth | curl -K - -sk --resolve "$H:443:127.0.0.1" -o /dev/null -D - "https://$H/" | tr -d "\r" | grep -i "^content-security-policy:" | tr ";" "\n" | sed "s/^ *//" | grep -E "^(content-security-policy: default-src|connect-src|script-src|frame-ancestors)"
'
create page with login -> HTTP 200
<title>PrivateBin</title>
content-security-policy: default-src 'none'
connect-src 'self' blob:
script-src 'self' 'wasm-unsafe-eval'
frame-ancestors 'none'
connect-src 'self' means the page can only talk to your own instance (upstream PrivateBin allows any site, for optional third party URL shorteners), and script-src 'self' means it only runs the scripts your instance serves.
nginx keeps only a SHA-512 crypt hash of the password; the plain text is in the root only credentials file:
sudo sed -E 's/^(admin:\$6\$).*/\1<sha512-crypt-hash>/' /etc/nginx/.privatebin-htpasswd
sudo stat -c '%a %U:%G %n' /etc/nginx/.privatebin-htpasswd /root/privatebin-credentials.txt
admin:$6$<sha512-crypt-hash>
640 root:www-data /etc/nginx/.privatebin-htpasswd
600 root:root /root/privatebin-credentials.txt
The certificate was generated on this instance and names its own addresses:
H=$(sudo grep '^PRIVATEBIN_URL=' /root/privatebin-credentials.txt | sed -E 's#^PRIVATEBIN_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 and PHP cannot reach the instance metadata service, and nginx will not start until first boot has finished and a per instance password is in place:
systemctl show nginx php8.3-fpm -p Id -p IPAddressDeny
systemctl cat nginx | grep -E '^ExecStartPre=/usr/(bin/test|local/sbin)'
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Id=nginx.service
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Id=php8.3-fpm.service
ExecStartPre=/usr/bin/test -f /var/lib/cloudimg/privatebin-firstboot.done
ExecStartPre=/usr/local/sbin/privatebin-auth-guard
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 '^PRIVATEBIN_URL=' /root/privatebin-credentials.txt | sed -E 's#^PRIVATEBIN_URL=https://([^/]+)/.*#\1#')
curl -sk --resolve "$H:443:127.0.0.1" -H 'Origin: https://evil.example' -d x=1 "https://$H/" | grep -o '403 Forbidden' | head -1
403 Forbidden
Step 8: Your own DNS name and certificate
Point a DNS record at the instance, then set the name PrivateBin is served as. This mints a new self signed certificate for the name, updates nginx, PrivateBin's base address and the URL in the credentials file, and keeps the name across reboots. Links you created under the old name keep working only while that name still resolves to the instance, so set the name before you share links widely:
sudo privatebin-set-hostname <your-domain>
To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running privatebin-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/ssl/privatebin/privatebin.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/privatebin/privatebin.key
sudo nginx -t && sudo systemctl reload nginx
Step 9: Change the password or add users
privatebin-set-password manages the Basic Auth file. Run it with no arguments to generate a new random password for admin; it is written to /root/privatebin-credentials.txt. To choose the password yourself, hand it over 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):
printf '%s\n' '<new-password>' | sudo privatebin-set-password --stdin
To give a colleague their own login for creating pastes, add --user <name>; remove it again with --delete <name>. Passwords are 12 to 128 printable characters without spaces or :. nginx reads the file on every request, so no reload is needed:
printf '%s\n' '<new-password>' | sudo privatebin-set-password --user alice --stdin
sudo privatebin-set-password --delete alice
Step 10: Configure PrivateBin
PrivateBin reads /var/www/privatebin/cfg/conf.php on every request, so changes apply immediately without a restart. The settings the image ships with:
sudo grep -E '^(basepath|discussion|fileupload|sizelimit|default|limit) ' /var/www/privatebin/cfg/conf.php
basepath = "https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/"
discussion = true
fileupload = false
sizelimit = 10000000
default = "1week"
limit = 0
limit = 300
| Setting | Section | Shipped value | What it does |
|---|---|---|---|
fileupload |
[main] |
false |
Set to true to allow file attachments (encrypted in the browser like the text). |
sizelimit |
[main] |
10000000 |
Largest paste in bytes (10 MB). nginx and PHP accept requests up to 16 MB. |
discussion |
[main] |
true |
Offers the Open discussion option on new pastes. |
default |
[expire] |
1week |
The preselected expiry. The choices are listed under [expire_options]. |
limit |
[traffic] |
0 |
Seconds between pastes from one IP address. Off, because only signed in users can create pastes; set it to 10 to rate limit them. |
limit |
[purge] |
300 |
How often (in seconds) expired pastes are deleted from the store. |
Edit the file with your editor of choice and keep its first line (;<?php http_response_code(403); /*), which stops the file being read as a web page; cfg/conf.sample.php documents every option:
sudo nano /var/www/privatebin/cfg/conf.php
Step 11: The paste store and backups
Pastes, comments and PrivateBin's small housekeeping files live on the separate EBS volume, owned by the PHP user only and mounted without device files, set-uid programs or execution:
findmnt -no SOURCE,FSTYPE,OPTIONS /var/lib/privatebin-data
df -h --output=size,used,avail,target /var/lib/privatebin-data
sudo stat -c '%a %U:%G %n' /var/lib/privatebin-data
sudo grep -E '^(dir|class) ' /var/www/privatebin/cfg/conf.php
/dev/nvme1n1 ext4 rw,nosuid,nodev,noexec,relatime
Size Used Avail Mounted on
9.8G 24K 9.3G /var/lib/privatebin-data
700 www-data:www-data /var/lib/privatebin-data
class = Filesystem
dir = "/var/lib/privatebin-data"
The store only ever holds ciphertext, but a backup keeps your pastes readable for everyone who still has a link. Take an EBS snapshot of the volume from the EC2 console or with aws ec2 create-snapshot, or archive it on the instance:
sudo tar -C /var/lib/privatebin-data -czf <backup-dir>/privatebin-data-$(date +%F).tar.gz .
To grow the store, modify the volume's size in the EC2 console, then run sudo resize2fs on its device (shown by findmnt above).
Step 12: Server components
| Component | Path |
|---|---|
| PrivateBin release | /var/www/privatebin (read only, owned by root) |
| PrivateBin configuration | /var/www/privatebin/cfg/conf.php |
| Paste store (EBS volume) | /var/lib/privatebin-data |
| Licence, source note and file checksums | /usr/share/doc/privatebin/ |
| nginx site | /etc/nginx/sites-available/cloudimg-privatebin |
| Basic Auth file | /etc/nginx/.privatebin-htpasswd (SHA-512 crypt) |
| Certificate and key | /etc/ssl/privatebin/ |
| PHP settings | /etc/php/8.3/fpm/conf.d/99-cloudimg-privatebin.ini |
| First boot script | /usr/local/sbin/privatebin-firstboot.sh |
| Host name and password helpers | /usr/local/sbin/privatebin-set-hostname, /usr/local/sbin/privatebin-set-password |
| Credentials | /root/privatebin-credentials.txt (mode 0600) |
ls -1 /var/www/privatebin
ls -l /usr/share/doc/privatebin | awk 'NR>1{print $1,$5,$9}'
nginx -v 2>&1
php -v | head -1
CHANGELOG.md
CREDITS.md
LICENSE.md
Procfile
README.md
SECURITY.md
bin
browserconfig.xml
cfg
composer.json
composer.lock
css
i18n
img
index.php
js
lib
manifest.json
robots.txt
tpl
vendor
-rw-r--r-- 27725 LICENSE.md
-rw-r--r-- 493 README.cloudimg.txt
-rw-r--r-- 22791 SHA256SUMS
nginx version: nginx/1.24.0 (Ubuntu)
PHP 8.3.6 (cli) (built: Oct 7 2026 18:06:38) (NTS)
Only index.php, the css, img, js and i18n folders, manifest.json, browserconfig.xml and robots.txt are served; every other file in the folder answers 404.
Step 13: Updating PrivateBin
The release in /var/www/privatebin is checked against /usr/share/doc/privatebin/SHA256SUMS, so you can confirm nothing in it has changed:
cd /var/www/privatebin && sha256sum --quiet --strict -c /usr/share/doc/privatebin/SHA256SUMS && echo RELEASE_FILES_INTACT
RELEASE_FILES_INTACT
To move to a newer release, download it from the PrivateBin releases page, check its checksum, extract it over /var/www/privatebin while keeping your cfg/conf.php, and read the release notes for configuration changes. Pastes in /var/lib/privatebin-data are kept.
Step 14: Security recommendations
- Allow port 22 in the security group from your own networks only, and ports 80 and 443 only from the networks your team and your readers use.
- Keep the per instance password secret, rotate it if it may have been shared, and give each colleague who creates pastes their own login (Step 9).
- Use your own DNS name and a certificate your readers' browsers trust (Step 8), so nobody has to click through a certificate warning.
- Use Burn after reading and short expiry times for secrets, and a paste Password sent over a different channel for anything sensitive.
- Keep the operating system patched; unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else.
Support and licensing
PrivateBin is open source under the zlib/libpng licence (the bundled libraries are under their own permissive licences, listed in the same file); there is no per user or per CPU licence fee. The licence is in /usr/share/doc/privatebin/. cloudimg is not affiliated with or endorsed by the PrivateBin project. 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