Deployment guide

PrivateBin 2.1.0

Overview

This guide shows how to launch and use the PrivateBin 2.1.0 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 choose it as the software version when you subscribe and launch.

PrivateBin is a minimalist, open source pastebin in which the server has zero knowledge of what it stores. Your browser encrypts every paste with 256-bit AES in GCM mode before uploading it. 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 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 allow PrivateBin's in-browser encryption only on HTTPS (or localhost), so this is what makes it work on a server.
  • Only you can create pastes. Creating, commenting on and deleting pastes needs HTTP Basic Auth as the user admin. The password is 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 the paste 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, and configuration, libraries and templates answer 404. Requests for host names 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. You can grow, snapshot and back it 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.1.0 (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.
  • Defaults: default user ubuntu, x86_64 architecture, and a recommended instance type of 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 that you use for management).
  • A security group that allows TCP 22 (SSH) from your own networks only. It must also allow TCP 443 (PrivateBin) and TCP 80 (redirects to HTTPS) from the networks used by your team and the people you share pastes with.
1

Launch the instance

In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick PrivateBin 2.1.0 on Ubuntu 24.04 as the software version, select your Region, and choose Launch through EC2.

Choose m5.large, your key pair, and a security group as described above. PrivateBin itself needs very little, because encryption happens in the browser. The AMI already carries the 10 GiB paste store volume, and you can make it larger on the launch page.

Alternatively, launch 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}]'
2

Connect to your instance

Connect to PrivateBin 2.1.0 on Ubuntu 24.04 over SSH as the user ubuntu.

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.

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

sudo cat /root/privatebin-credentials.txt

Only root can read the file. It holds the URL, the user name and this instance's password:

# PrivateBin 2.1.0 - 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-1-0/

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, go to 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. To avoid this, 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 options for the paste:

  • Expires: from 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: an optional password that readers must also enter.
  • 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, 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, and it shows a notice of when the paste 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.

7

Check the security posture from the command line

Run the commands below to check the following behaviour. 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. 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 in 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, to support optional third-party URL shorteners. script-src 'self' means the page only runs scripts that your instance serves.

nginx keeps only a SHA-512 crypt hash of the password. The plain text is in the credentials file, which only root can read.

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 = 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. 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/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, then set the name that PrivateBin is served as. The command mints a new self-signed certificate for the name. It updates nginx, PrivateBin's base address and the URL in the credentials file, and it keeps the name across reboots.

Links you created under the old name keep working only while that name still resolves to the instance. 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. The password is written 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 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
10

Configure PrivateBin

PrivateBin reads /var/www/privatebin/cfg/conf.php on every request, so changes apply immediately without a restart. View the settings that 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

The settings you are most likely to change are listed below, as setting, section, shipped value and effect.

  • fileupload ([main], shipped as false): set it to true to allow file attachments, which are encrypted in the browser like the text.
  • sizelimit ([main], shipped as 10000000): the largest paste in bytes (10 MB). nginx and PHP accept requests up to 16 MB.
  • discussion ([main], shipped as true): offers the Open discussion option on new pastes.
  • default ([expire], shipped as 1week): the preselected expiry. The choices are listed under [expire_options].
  • limit ([traffic], shipped as 0): the 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 ([purge], shipped as 300): how often, in seconds, expired pastes are deleted from the store.

Edit the file with your editor of choice. 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
11

The paste store and backups

Pastes, comments and PrivateBin's small housekeeping files live on the separate EBS volume. Only the PHP user owns it, and it is 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. 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. Then run sudo resize2fs on its device, which findmnt shows above.

12

Server components

The image uses the following components and 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.
  • Licence, 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).
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 19 2026 15:16:58) (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

The release in /var/www/privatebin is checked against /usr/share/doc/privatebin/SHA256SUMS. Run the check 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. Allow ports 80 and 443 only from the networks that your team and your readers use.
  • Keep the per-instance password secret and rotate it if it may have been shared. 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 nobody has to click through a certificate warning.
  • Use Burn after reading and short expiry times for secrets. For anything sensitive, add a paste Password and send it over a different channel.
  • 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 licence. The bundled libraries are under their own permissive licences, which are listed in the same file. There is no per-user or per-CPU licence fee, and the licence 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.1.0 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