This document covers how to build OAK, set up a GPG signing key, publish the companion website to Cloudflare Pages, and publish release archives to Cloudflare R2.
- Go 1.22 or later
gpg(GnuPG) — for signing and verificationrsync— for mirroring Tor Browser and Tailsgit— for cloning onion-sites and the Tor Browser Manualwrangler(Cloudflare CLI) — for Pages deploymentss5cmd— for uploading large release artifacts to R2 (wrangler caps at ~300 MB)
Install Wrangler:
npm install -g wrangler
wrangler loginInstall s5cmd:
brew install peak/tap/s5cmd # macOS
# or download a release binary from https://github.com/peak/s5cmd/releasesgit clone https://github.com/stazelabs/open-access-kit.git
cd open-access-kit
go build -o oak ./cmd/oakOr install globally:
go install github.com/stazelabs/open-access-kit/cmd/oak@latestVerify the build:
./oak versionOAK signs release archives with GPG. You need a key pair before running a signed build.
If you do not already have a suitable key:
gpg --full-generate-keyRecommended settings: RSA 4096, no expiry (or a multi-year expiry), your release identity as the name and email.
gpg --list-secret-keys --keyid-format longThe key ID is the 16-character hex string after rsa4096/, for example 8F3DA1B2C4E5F6A7.
gpg --armor --export YOUR_KEY_ID > keys/oak-signing.pubCommit keys/oak-signing.pub to the repository. This key is bundled into every OAK image so offline recipients can verify archives without internet access.
Set signing.key_id in oak.yaml to your key ID:
signing:
enabled: true
key_id: "8F3DA1B2C4E5F6A7"
public_key: keys/oak-signing.pubAlternatively, pass --sign-key at build time to avoid committing the key ID.
1Password can store the GPG private key and its passphrase, keeping them out of dotfiles and off the filesystem between uses.
Install the 1Password CLI and desktop app, then enable CLI integration in Settings → Developer → Integrate with 1Password CLI:
brew install 1password-cli # macOS
op --versionExport your private key and store it as a 1Password document:
gpg --armor --export-secret-keys YOUR_KEY_ID > /tmp/oak-signing.asc
op document create /tmp/oak-signing.asc \
--title "OAK Signing Key" \
--vault "Private"
rm /tmp/oak-signing.ascStore the passphrase as a password item:
op item create \
--category login \
--title "OAK Signing Key Passphrase" \
--vault "Private" \
--field "password=your-passphrase-here"Store the reference paths (adjust vault/title to match):
op://Private/OAK Signing Key/
op://Private/OAK Signing Key Passphrase/password
Use an ephemeral keyring so the private key is never written to your default ~/.gnupg:
export GNUPGHOME=$(mktemp -d)
trap "rm -rf $GNUPGHOME" EXIT
# Retrieve and import the private key
op document get "OAK Signing Key" | gpg --batch --import
# Retrieve the passphrase and pre-seed the agent
PASSPHRASE=$(op read "op://Private/OAK Signing Key Passphrase/password")
echo "$PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
--pinentry-mode loopback \
--quick-set-expire YOUR_KEY_ID 0 # no-op refresh to load the key into agentThen run the build as normal. The EXIT trap removes the temporary keyring automatically.
Pass the passphrase via stdin using GPG's loopback pinentry mode so the build is non-interactive:
PASSPHRASE=$(op read "op://Private/OAK Signing Key Passphrase/password")
echo "$PASSPHRASE" | gpg --batch --yes \
--passphrase-fd 0 \
--pinentry-mode loopback \
--armor --detach-sign dist/OAK-Q126-M.zipoak sign invokes gpg directly, so you can pre-load the passphrase into the running agent before calling oak build --sign and GPG will pick it up without prompting.
#!/usr/bin/env bash
set -euo pipefail
RELEASE=Q126
# Ephemeral keyring
export GNUPGHOME=$(mktemp -d)
trap "rm -rf $GNUPGHOME" EXIT
# Load key and passphrase from 1Password
op document get "OAK Signing Key" | gpg --batch --import
PASSPHRASE=$(op read "op://Private/OAK Signing Key Passphrase/password")
# Pre-seed GPG agent so oak sign is non-interactive
echo "$PASSPHRASE" | gpg --batch --passphrase-fd 0 \
--pinentry-mode loopback --list-secret-keys > /dev/null
# Build and sign all tiers
for tier in S M L; do
./oak build --tier "$tier" --sign
done
echo "Build complete. Artifacts in dist/"./oak build --tier M --signThis runs: download → verify → stage → annotate → package → sign.
Output artifacts land in dist/:
dist/OAK-Q126-M.zip
dist/OAK-Q126-M.zip.sha256
dist/OAK-Q126-M.zip.asc
| Flag | Purpose |
|---|---|
--tier S|M|L |
Target tier (default: M) |
--sign |
GPG-sign the output ZIP |
--sign-key KEY_ID |
Override the signing key |
--skip-download |
Skip step 1 when the mirror is already current |
--dry-run |
Show what would happen without doing it |
--release Q126 |
Override the release name (default: from oak.yaml) |
for tier in S M L; do
./oak build --tier $tier --sign
doneOnce mirror/ is populated you can skip re-downloading on subsequent runs:
./oak build --tier M --skip-download --sign./oak download # Step 1: populate mirror/
./oak verify # Step 2: GPG-check mirrored content
./oak stage --tier M # Step 3: assemble image/
./oak annotate # Step 4: generate VERSION/MANIFEST/README
./oak package # Step 5: create dist/ ZIP + SHA256
./oak sign dist/OAK-Q126-M.zip # Step 6: detached GPG signatureoak site renders content/guides/ to docs/ as static HTML:
./oak sitePreview locally by opening docs/index.html in a browser.
To point links at a custom base URL (useful for staged deployments):
./oak site --base-url https://your-project.pages.devThe docs/ directory is the static site deployed to Cloudflare Pages.
- Log in to the Cloudflare dashboard and go to Workers & Pages.
- Create a new Pages project connected to the
open-access-kitGitHub repository. - Set the build configuration:
- Framework preset: None
- Build command: leave blank (site is pre-rendered; see below)
- Build output directory:
docs
- Under Environment variables, set none (the site has no build-time dependencies).
Because oak must run to render the site, the recommended workflow is to render locally and push the output rather than using Cloudflare's build workers:
- Render:
./oak site - Commit
docs/and push to the main branch. - Cloudflare Pages deploys automatically on push.
Alternatively, use wrangler pages deploy for a direct upload without a git push:
./oak site
wrangler pages deploy docs --project-name open-access-kitIn the Pages project settings, add a custom domain under Custom domains. Cloudflare manages the DNS record automatically for domains on Cloudflare.
Release archives are stored in a Cloudflare R2 bucket, giving recipients a stable download URL independent of GitHub.
wrangler r2 bucket create oak-releasesOr create it in the Cloudflare dashboard under R2 Object Storage.
Enable public access on the bucket so that download URLs work without authentication: in the dashboard, go to the bucket settings and turn on Public access, or bind a custom domain to the bucket.
s5cmd uses the S3-compatible R2 API. Create an R2 API token in the Cloudflare dashboard
(R2 → Manage R2 API Tokens) with Object Read & Write permissions on oak-releases.
Export credentials before uploading (or store them in ~/.aws/credentials under a named profile):
export AWS_ACCESS_KEY_ID=<your-r2-access-key-id>
export AWS_SECRET_ACCESS_KEY=<your-r2-secret-access-key>
export AWS_ENDPOINT_URL=https://<your-cloudflare-account-id>.r2.cloudflarestorage.comYour Cloudflare account ID is shown in the dashboard URL (dash.cloudflare.com/<account-id>)
and on the R2 overview page.
After a successful oak build --sign, upload the dist/ artifacts with s5cmd.
Use --acl public-read only if your bucket has public access enabled via R2's S3 API;
if you enabled public access through the dashboard instead, omit the flag.
Upload a single tier:
RELEASE=Q126
TIER=M
for ext in .zip .zip.sha256 .zip.asc; do
file="dist/OAK-${RELEASE}-${TIER}${ext}"
[ -f "$file" ] || continue
s5cmd cp "$file" "s3://oak-releases/${RELEASE}/${TIER}/OAK-${RELEASE}-${TIER}${ext}"
doneUpload all tiers at once:
RELEASE=Q126
for tier in S M L; do
for ext in .zip .zip.sha256 .zip.asc; do
file="dist/OAK-${RELEASE}-${tier}${ext}"
[ -f "$file" ] || continue
s5cmd cp "$file" "s3://oak-releases/${RELEASE}/${tier}/OAK-${RELEASE}-${tier}${ext}"
done
dones5cmd uploads in parallel by default and has no file-size limit, making it suitable for the multi-gigabyte L-tier archive.
With public access enabled, objects are reachable at:
https://pub-<your-account-hash>.r2.dev/oak-releases/Q126/M/OAK-Q126-M.zip
If you bind a custom domain to the bucket (e.g., releases.openaccess.tools), the URL becomes:
https://releases.openaccess.tools/oak-releases/Q126/M/OAK-Q126-M.zip
Update the download links in the GitHub README and companion website to point at these URLs for each quarterly release.
- Update
release:inoak.yaml(e.g.,Q126→Q226) - Run
./oak build --tier all --signfor all tiers - Run
./oak siteto refresh the companion website - Upload
dist/artifacts to R2:s5cmd cp dist/... s3://oak-releases/... - Deploy the updated site:
wrangler pages deploy docs --project-name open-access-kit - Tag the release commit:
git tag Q226 && git push --tags - Update download links in the README to point at the new R2 objects