How to Password-Protect a Generated PDF in Ruby
Use HexaPDF for strong PDF encryption in Ruby, understand Prawn’s limits, and avoid common password and reader-compatibility mistakes.

Password-protecting a generated PDF in Ruby means encrypting the document before writing it to disk. For new confidential documents, use HexaPDF’s HexaPDF::Document#encrypt API and supply the password from a secret store or environment variable. HexaPDF documents AES 128-bit as its default and compatibility-minded choice. Prawn also has an encrypt_document method, but the versioned Prawn 2.5.0 API documents a weak, 40-bit password-derived key, so it should not be treated as equivalent for sensitive files.
Choose the Ruby PDF library first
| Library | Encryption entry point | Best fit | Important limitation |
|---|---|---|---|
| HexaPDF | HexaPDF::Document#encrypt |
Confidential PDFs and workflows that need modern AES options | Check current licensing terms for your deployment model |
| Prawn | encrypt_document |
Simple PDF generation where its documented encryption limits are acceptable | Prawn 2.5.0 documents a 40-bit password-derived key; readers may not enforce permissions |
HexaPDF is the safer default when encryption strength matters. Its guide says RC4 is old and insecure and should be avoided, and describes AES 128-bit as the default and the best option for broad reader compatibility. AES 256-bit is standardized with PDF 2.0, but you should verify that every recipient’s PDF reader supports the chosen mode. Read HexaPDF’s encryption guide for the options supported by your installed version.
Encrypt a PDF with HexaPDF
1. Install the gem
gem install hexapdf
In an application, add HexaPDF to your Gemfile and run bundle install:

gem "hexapdf"
2. Generate and encrypt the document
This complete example creates a page, writes text, encrypts the document, and saves it. The password is read from PDF_USER_PASSWORD rather than embedded in source code.
require "hexapdf"
password = ENV.fetch("PDF_USER_PASSWORD")
pdf = HexaPDF::Document.new
page = pdf.pages.add
page.canvas.text("Confidential report", at: [50, 750])
pdf.encrypt(user_password: password)
pdf.write("report.pdf")
The user password is the password a recipient enters to open the file. HexaPDF’s standard security handler also supports an owner password, which has broader authority under the PDF security model. Exact option names and permission controls can vary by installed release, so consult the Standard Security Handler API before adding owner passwords or restrictions.
3. Run it safely
PDF_USER_PASSWORD='use-a-secret-from-your-secret-manager' ruby generate_report.rb
Do not commit passwords to Git, put them in command history, or print them in logs. In production, load the value from your platform’s secret manager and rotate it according to your access policy. A password-protected PDF is still exposed if the password is stored beside the file or sent through an unprotected channel.
Encryption choices and compatibility
AES 128-bit
HexaPDF documents AES 128-bit as its default and recommends it when broad reader compatibility matters. It is a practical choice for files opened by a mixed set of desktop, browser and mobile PDF applications.
AES 256-bit
AES 256-bit was standardized with PDF 2.0. Use it when your security requirements call for it and you control, or can test, the recipient reader environment. Do not describe it as universally compatible. Generate a sample file and open it in the exact applications your users rely on before switching a production workflow.
RC4
Avoid RC4. HexaPDF’s encryption guide describes it as old and insecure. If an old integration requires RC4, treat migration to AES as a security task rather than accepting the legacy setting indefinitely.
User passwords, owner passwords and permissions
A user password controls opening the document. An owner password can open the file without the user-level restrictions defined by the PDF security handler. Permissions can express whether printing, copying or editing is allowed, but those flags depend on the reader application honoring them. They are not a replacement for authorization, data-loss prevention or access controls around the file itself.
For a confidential report, protect the delivery path as well as the PDF: restrict who can download it, avoid putting the password in the same email as the attachment, expire links where your storage system supports it, and keep an audit trail of access. Encryption does not prevent screenshots or a recipient from sharing a password after opening the file.
Encrypt a generated PDF with Prawn
Prawn’s project manual documents encrypt_document. The following is runnable and reads the password from the environment:
require "prawn"
Prawn::Document.generate("report.pdf") do
text "Confidential report"
encrypt_document(user_password: ENV.fetch("PDF_USER_PASSWORD"))
end
Prawn’s manual says a user password is required to read the encrypted output. Without one, the file can still be encrypted but does not require a password to open. The Prawn 2.5.0 API documentation warns that its encryption is weak and limited to a 40-bit password-derived key because of historical PDF restrictions. It also cautions that reader applications may not enforce permissions. Scope that warning to the documented Prawn 2.5.0 API and verify the current release documentation before relying on Prawn for confidential material. See the Prawn encryption manual and versioned API.
Existing PDFs: generate first, then encrypt
Encryption must be configured before HexaPDF writes the output. If another process already created an unencrypted PDF, use a HexaPDF workflow that opens or imports that document, calls encrypt, and writes a new output file. Keep the original outside public storage until the encrypted file has been validated.
require "hexapdf"
password = ENV.fetch("PDF_USER_PASSWORD")
doc = HexaPDF::Document.open("input.pdf")
doc.encrypt(user_password: password)
doc.write("encrypted.pdf")
Whether every feature of a complex input PDF survives a read-and-write cycle depends on the document structure and the library version. Preserve a copy of the source, compare page count and important content, and validate the result in your target readers.
Automate verification
A useful verification step is to open the resulting file in a clean process that does not already have cached credentials. Confirm that:
- The viewer prompts for the user password.
- The correct password opens every page.
- An incorrect password is rejected.
- Fonts, images, links, forms and metadata required by your workflow remain intact.
- The file opens in the desktop, browser and mobile readers used by recipients.
For CI, keep a small encrypted fixture and test the expected failure when a wrong password is supplied. Never place a real production password in test output.
Or skip the browser setup
If your workflow also needs clean captures of web pages before creating a report or PDF, ScreenshotNeo provides a website screenshot API and MCP server. It is separate from Ruby PDF encryption: you still encrypt the generated PDF with HexaPDF or another PDF library. ScreenshotNeo can supply the source capture without requiring you to maintain a browser worker.

One GET request returns a PNG, JPEG, WebP or PDF. The API removes cookie and consent banners, newsletter popups and chat widgets before capture; each cleanup step can be disabled. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server lets Claude, Cursor and other MCP clients call take_screenshot, get_page_info and capture_pdf.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
See the ScreenshotNeo API documentation for all options: full-page capture with lazy images, CSS element selection, dark mode, device presets, custom viewports and retina scale; PDF paper size, margins, landscape and page ranges; custom CSS and JavaScript; clicks and wait conditions; blocking ads, trackers, requests or resource types; headers, cookies, user agents and Authorization; timezone and geolocation; transparent backgrounds; resizing; caching with a chosen TTL; signed links; asynchronous jobs and signed webhooks; bulk capture of up to 100 URLs per call; usage reporting; and the OpenAPI specification.
There are 1,000 free screenshots each month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
Troubleshooting
“uninitialized constant HexaPDF”
The gem is not installed or is not loaded by the running bundle. Add gem "hexapdf" to the Gemfile, run bundle install, and execute the program with bundle exec ruby generate_report.rb. Confirm that require "hexapdf" appears before HexaPDF::Document.new.
The output opens without asking for a password
Check that pdf.encrypt runs before pdf.write, and that user_password is non-empty. With Prawn, make sure encrypt_document(user_password: ...) is inside the document-generation block. Test in a fresh viewer session; a viewer that already has a decrypted copy may not prompt again.
The password is rejected
Inspect the environment variable and surrounding shell quoting. Passwords containing spaces, shell metacharacters or non-ASCII characters can be altered before Ruby receives them. Read the value from a secret manager or use a correctly quoted environment assignment. Do not log the password while debugging.
A recipient’s reader cannot open the file
The selected encryption revision may not be supported by that reader. Start with HexaPDF’s AES 128-bit default for broad compatibility, then test AES 256-bit only against the readers you intend to support. Update the reader where possible and keep a compatibility test file in your release checks.
Printing or copying is still possible
PDF permission flags are advisory to the reader application. They are not guaranteed access controls. If the data must not be copied, enforce authorization before download and use a delivery design that limits access to the source system.
Pages or interactive content changed after rewriting
Opening and writing a complex PDF can expose unsupported or unusual structures. Compare the original and encrypted outputs, test forms and annotations, and use a library version whose documentation covers the features you need. Keep the unencrypted source in a protected location while you investigate.
Performance, reliability and cost
Encryption adds a write step and CPU work proportional to the document, but the dominant cost in many workflows is generating images, charts or remote assets before the PDF is written. Build the document once, encrypt once, and stream or write to a controlled temporary location before publishing. Avoid repeatedly opening and rewriting the same file in a loop.
For reliability, fail closed when PDF_USER_PASSWORD is missing, write to a temporary filename, flush the encrypted output, validate it, and rename it atomically. Clean up temporary unencrypted files. If a job retries, use an idempotent output key so a partial file is never mistaken for a completed encrypted artifact.
HexaPDF licensing can matter for commercial deployment. Its project documentation says a commercial license is needed in certain distribution or remote-access cases when application source is not made available under AGPL. Review the current official terms for your exact deployment model before shipping.
FAQ
Is HexaPDF better than Prawn for password protection?
For confidential documents where encryption strength matters, HexaPDF is the better documented choice. Prawn 2.5.0 documents a 40-bit limitation, so do not present the libraries as equivalent.
Can I recover a forgotten PDF password?
Not through the Ruby APIs described here. Treat passwords as secrets, store them in a managed system, and maintain an authorized recovery process outside the PDF file.
Does an owner password make a PDF impossible to copy?
No. Reader applications may ignore permission flags, and a person who can view the document can capture its contents.
Should I use AES 256-bit for every file?
Only when your recipient readers support it and your compatibility testing passes. HexaPDF documents AES 128-bit as the broad-compatibility default.
Does ScreenshotNeo encrypt my generated Ruby PDF?
No. ScreenshotNeo captures web pages and can return images or PDFs. Use HexaPDF or your chosen PDF library to apply password encryption to the final file.


