Deployment guide

PrivateBin 2.0.5

Overview

This guide shows how to launch and use the PrivateBin 2.0.5 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud 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. The key that decrypts the paste is the part of the link after #, which browsers never send to a server, so the server only ever holds ciphertext.

Teams use PrivateBin 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 SecureAnyCloud image makes it your team's private service.

What you get:

  • HTTPS from 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 HTTPS is required for PrivateBin to work 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 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 the paste with the key in the link. Without the full link, there is nothing to read.
  • Fail-closed start. 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 that the instance does not serve are dropped, and 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, on fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
  • PrivateBin 2.0.5 (the official release in /var/www/privatebin, license 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, x86_64 architecture and a recommended instance type of m5.large
  • 24/7 SecureAnyCloud support

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 that 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 that your team and the people you share pastes with use
1

Launch the instance

In the AWS Marketplace listing, choose Continue to Subscribe, and then choose Continue to Configuration. Pick PrivateBin 2.0.5 on Ubuntu 24.04 as the software version and your Region, and then choose 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 described in Prerequisites. The AMI already carries the 10 GiB paste store volume. You can make it larger on the launch page.

To launch with the AWS CLI instead, 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}]'
2

Connect to your instance

Connect over SSH as the ubuntu user, which is the default user for PrivateBin 2.0.5 on Ubuntu 24.04:

ssh -i <your-key.pem> ubuntu@<public-ip>
3

Check the services

First boot generates the certificate and the password, and then nginx starts in front of PHP 8.3 FPM. Check the services:

systemctl is-active nginx php8.3-fpm
sudo test -f /var/lib/sac/data/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",...))
4

Get your URL and password

Read the credentials file as root:

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://secureanycloud.com/stacks/privatebin-2-0-5/

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.

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. Alternatively, 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, set the following options:

  • Expires sets when the paste expires (5 minutes to 1 year, or never).
  • Burn after reading deletes the paste as soon as it is opened once.
  • Open discussion lets readers add comments. Posting a comment needs the login too.
  • Password is an optional password that readers must also enter.
  • Format is 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, and Delete data removes the paste from the server straight away.

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. A fresh browser with no credentials can open the link and read the paste, with a notice of when it expires.

Anyone who holds the full link can read the paste until it expires, so treat the link like the secret itself. Send it over a channel that 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.

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 that the instance does not serve is dropped without an answer. Run the following checks:

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

The value 000 means that nginx closed the connection without answering, which is how it treats any host name that 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 that the page can only talk to your own instance. Upstream PrivateBin allows any site, for optional third-party URL shorteners. script-src 'self' means that the page only runs the scripts that 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. Inspect it:

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 = sac 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. Confirm both:

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/sac/data/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
8

Your own DNS name and certificate

Point a DNS record at the instance, and then set the name that PrivateBin is served as. The command creates 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 that 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 certificate:

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
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, which it writes to /root/privatebin-credentials.txt.

To choose the password yourself, pass it on standard input. printf is a shell builtin, so the password never appears in the process list. Start the line with a space so that 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 the login 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
10

Configure PrivateBin

PrivateBin reads /var/www/privatebin/cfg/conf.php on every request, so changes apply immediately without a restart. To see the settings that the image ships with, run:

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

The main settings are:

  • fileupload in [main] ships as false. Set it to true to allow file attachments, which are encrypted in the browser like the text.
  • sizelimit in [main] ships as 10000000. It is the largest paste in bytes (10 MB). nginx and PHP accept requests up to 16 MB.
  • discussion in [main] ships as true. It offers the Open discussion option on new pastes.
  • default in [expire] ships as 1week. It is the preselected expiry. The choices are listed under [expire_options].
  • limit in [traffic] ships as 0. It is the number of seconds between pastes from one IP address. It is off, because only signed-in users can create pastes. Set it to 10 to rate-limit them.
  • limit in [purge] ships as 300. It is 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 from being read as a web page. cfg/conf.sample.php documents every option:

sudo nano /var/www/privatebin/cfg/conf.php
11

The paste store and backups

Pastes, comments and PrivateBin's small housekeeping files live on the separate EBS volume. The volume is owned by the PHP user only and is mounted without device files, set-uid programs or execution. Inspect it:

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. Alternatively, archive the store 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, and then run sudo resize2fs on its device (shown by findmnt above).

12

Server components

The server components are installed at these paths:

  • 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
  • License, source note and file checksums: /usr/share/doc/privatebin/
  • nginx site: /etc/nginx/sites-available/sac-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-sac-privatebin.ini
  • First boot script: /usr/local/sbin/privatebin-firstboot.sh
  • Host name and password helpers: /usr/local/sbin/privatebin-set-hostname and /usr/local/sbin/privatebin-set-password
  • Credentials: /root/privatebin-credentials.txt (mode 0600)

To list the release files, the documentation files and the nginx and PHP versions, run:

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.sac.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.

13

Updating PrivateBin

You can check the release in /var/www/privatebin against /usr/share/doc/privatebin/SHA256SUMS to confirm that 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 and 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.

14

Security recommendations

  • Allow port 22 in the security group from your own networks only, and allow ports 80 and 443 only from the networks that 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 that your readers' browsers trust (Step 8), so that nobody has to click through a certificate warning.
  • Use Burn after reading and short expiry times for secrets, and send a paste Password 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 upgrade applies everything else.

Support and licensing

PrivateBin is open source under the zlib/libpng license. The bundled libraries are under their own permissive licenses, listed in the same file. There is no per-user or per-CPU license fee. The license is in /usr/share/doc/privatebin/.

SecureAnyCloud is not affiliated with or endorsed by the PrivateBin project. The AMI price covers SecureAnyCloud's hardening, first-boot setup and support.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com
  • Support: 24/7, with a 24-hour response SLA

Deploy on AWS

Launch PrivateBin 2.0.5 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

Deploy on AWS