Developer Tools AWS

ByteStash 1.6.0 on AWS User Guide

| Product: ByteStash 1.6.0 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying ByteStash 1.6.0 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened Node.js & Developer and API Tools AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other products in the listing are described on the listing's guide hub.

ByteStash is a self-hosted code-snippet manager. You store reusable snippets, each with one or more files, a description in Markdown and categories, and find them again with full-text search, syntax highlighting (the Monaco editor) and Markdown and Mermaid rendering. Snippets are private to their owner unless the owner marks one public or creates a share link. Every account can create API keys for the REST API and for ByteStash's remote MCP endpoint, so AI assistants such as Claude can search, read and save snippets.

The image runs ByteStash 1.6.0, built by cloudimg from the official 1.6.0 source with Node.js 22.23.4. ByteStash 1.6.0 and the releases before it fixed three security problems that earlier images carried: 1.5.14 stops a signed-in user from overwriting another user's snippet, and 1.6.0 stops recycled (deleted) public snippets from being served by the public API and marks the session cookie Secure behind HTTPS. The build also updates the libraries npm audit flags in the production tree, so the server has no known advisory (details below). The base is a fully patched Ubuntu 24.04 LTS, and unattended security upgrades stay on for your instance.

What is on the image:

  • ByteStash 1.6.0 (bytestash.service) running as the unprivileged bytestash user on 127.0.0.1:5000 only, with its SQLite database in /var/lib/bytestash
  • nginx publishing ByteStash over HTTPS on port 443 with a certificate created on your instance's first boot; port 80 redirects to HTTPS
  • One administrator account, admin, with a password created on your instance's first boot, and a JSON Web Token (JWT) signing secret unique to your instance
  • bytestash-set-hostname, a helper to serve ByteStash under your own DNS name, and cloudimg-bytestash-probe, a sign-in round-trip test
  • The build record (BUILD-INFO.txt), the npm audit results, the licences and the complete ByteStash 1.6.0 source archive in /usr/share/doc/cloudimg-bytestash/

Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3

Security posture

  • No open sign-up and no first-visitor race. A fresh ByteStash install lets anybody who reaches it register the first account. On this image first boot creates the database and the admin account before ByteStash is allowed to start for the first time, so registration is already closed when the instance first answers. Sign-up stays closed (ALLOW_NEW_ACCOUNTS=false), anonymous mode is off, and admin is the only account until you add more (see Maintenance). A check before every start refuses to run ByteStash or nginx until first boot has finished.
  • Per-instance secrets. The admin password, the JWT signing secret that protects every session and API sign-in, and the TLS key are generated on your instance's first boot. No database, password, secret or key ships in the image. The password is in /root/bytestash-credentials.txt (mode 0600, root only); ByteStash stores passwords only as bcrypt hashes.
  • Loopback only. ByteStash listens on 127.0.0.1. nginx on 443 is the only way in.
  • HTTPS on first boot. nginx serves ByteStash with a TLS key and self-signed certificate created on your instance (TLS 1.2 and 1.3 only). Port 80 only redirects to HTTPS. The session cookie is Secure, HttpOnly and SameSite=Lax.
  • Fixed host name. ByteStash is served under a name taken from the instance metadata at first boot (or one you set), never from the request. Requests for any other host name are dropped, and nginx refuses cross-site form posts and WebSocket upgrades. API and MCP clients, which send no browser Origin header, are not affected.
  • Real client addresses. nginx passes the connecting address to ByteStash and ignores any X-Forwarded-For header a client sends, and ByteStash trusts forwarded headers only from nginx on the same instance.
  • No metadata access. ByteStash fetches an identity provider's configuration when you turn on single sign-on, so bytestash.service and nginx.service are blocked from reaching the instance metadata service (169.254.169.254 and fd00:ec2::254). Amazon Time Sync keeps working.
  • Least privilege. ByteStash runs with no Linux capabilities and a read-only system (ProtectSystem=strict) except its own data directory; the application files are owned by root.
  • No scheduled firmware-metadata refresh. The fwupd firmware updater stays installed, but its LVFS metadata refresh (fwupd-refresh.timer and .service) is masked, so an LVFS outage never marks your instance unhealthy at boot; run sudo fwupdmgr refresh yourself if you use it.
  • IMDSv2 required. The AMI is registered with ImdsSupport v2.0, so instances launched from it refuse token-less (IMDSv1) metadata requests.
  • Per-instance identity. SSH host keys and the machine ID are generated fresh on every instance.

What anyone who can reach the instance can read

ByteStash serves a few pages without signing in, by design. Keep them in mind when you decide who may reach port 443:

  • Public snippets. /public/snippets in the web UI and /api/public/snippets in the API list and serve the snippets a user has explicitly marked public, including each file's raw text. Private snippets are never listed there, and since 1.6.0 neither are snippets in the recycle bin.
  • Share links. A share link (/s/<id>, and its embed /embed/<id> for other web pages) shows one snippet to whoever has the link, unless the owner ticked Require authentication when creating it. Share links can expire.
  • API reference. /api-docs serves ByteStash's OpenAPI documentation.

Everything else (your snippets, API keys, settings and the admin pages) needs a signed-in session or an API key.

Known advisories and how the build handles them

  • Server dependencies. npm audit on the upstream 1.6.0 lockfile lists five advisories in the server's production tree. This build updates, within their own release lines, body-parser (a denial-of-service on every JSON request), zod (used by the MCP tool definitions), and hono, fast-uri and ip-address (used by the MCP library). After the changes npm audit --omit=dev reports no advisory for the server; the result ships in server-npm-audit-production.json.
  • Web interface. The browser bundle is built with dompurify 3.4.17, which fixes a critical sanitiser bypass that the upstream lockfile's version has. One low advisory remains: KaTeX, which renders maths in Mermaid diagrams, can be steered by an existing prototype-pollution bug elsewhere (GHSA-238p-pmpm-9mq7); the fix needs Mermaid 12, a major release ByteStash has not adopted yet. It runs only in the browser, never on the server; the result ships in client-npm-audit-production.json.
  • Build tools. The web interface is compiled at build time; its development tools (including vite) and the compiler used for the native modules are removed afterwards and never ship.

Prerequisites

  1. An AWS account and permission to launch EC2 instances
  2. An EC2 key pair in the Region you launch in
  3. A subnet that gives the instance a public IPv4 address (or private connectivity from where you will use ByteStash)

Recommended instance type: m5.large. ByteStash is a single Node.js process with SQLite storage; a smaller burstable type also works for a small team.

Connecting to your instance

Version Operating system SSH user
ByteStash 1.6.0 on Ubuntu 24.04 Ubuntu 24.04 LTS ubuntu

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. Open the cloudimg Hardened Node.js & Developer and API Tools AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  2. Once the subscription is active, choose Launch your software.
  3. Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose ByteStash 1.6.0 on Ubuntu 24.04, then choose your Region.
  4. Pick an instance type (m5.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  5. Security group: allow SSH (22), HTTP (80) and HTTPS (443). Restrict SSH to your own IP address and 80/443 to the people and API clients that use ByteStash.
  6. Choose your VPC and a subnet that gives the instance a public IP, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.

Option B: AWS CLI

Run this on your own workstation. It creates a security group that allows SSH and HTTPS from your management address only, then launches the instance. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDR with your own:

SG=$(aws ec2 create-security-group --group-name bytestash \
  --description "ByteStash" --vpc-id vpc-0123456789abcdef0 \
  --query GroupId --output text)
for p in 22 80 443; do
  aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port "$p" --cidr <your-mgmt-cidr>
done
aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type m5.large \
  --key-name my-key-pair \
  --security-group-ids "$SG" \
  --subnet-id subnet-0123456789abcdef0 \
  --associate-public-ip-address \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=bytestash}]'

You do not need --metadata-options: the AMI already makes IMDSv2 required.

Step 2: Connect and read your credentials

Connect as ubuntu with the key pair you chose:

ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>

First boot finishes within a minute of launch. It creates the certificate, the JWT signing secret, the database and the admin account, then lets ByteStash and nginx start. Your credentials are in a root-only file:

sudo cat /root/bytestash-credentials.txt
# ByteStash - generated on this instance's first boot by bytestash-firstboot.service.
# Unique to this instance; nothing here shipped in the image. Store it somewhere safe.
BYTESTASH_URL=https://ec2-54-81-216-2.compute-1.amazonaws.com/
BYTESTASH_USERNAME=admin
BYTESTASH_ADMIN_PASSWORD=<BYTESTASH_ADMIN_PASSWORD>
#
# Open BYTESTASH_URL and sign in as admin (self-signed certificate minted on this instance; port 80 redirects).
# Sign-up is closed: nobody can register an account. After first login change this password in the user menu
# (your account name, top right) -> Change password. REST API and MCP clients use an API key: user menu -> API Keys.
# Change the name ByteStash is served as:  sudo bytestash-set-hostname <your-dns-name>
# Restrict 80/443 in the security group to the people and clients that need them.
# Guide: https://www.cloudimg.co.uk/guides/bytestash-1-6-0-on-ubuntu-24-04-aws/

BYTESTASH_ADMIN_PASSWORD holds your instance's own password. Keep it somewhere safe, then change it after your first sign-in (see Maintenance).

Step 3: Confirm the services are running

First boot, ByteStash and nginx are active, ByteStash runs as its own unprivileged user, and the start guard (which runs before every start of ByteStash and nginx) passes:

systemctl is-active bytestash-firstboot.service bytestash.service nginx.service
ps -o user:12=,comm= -p "$(systemctl show -p MainPID --value bytestash.service)"
sudo /usr/local/sbin/bytestash-guard
active
active
active
bytestash    node
GUARD_OK

If a service ever refuses to start, run the guard: it names the exact reason.

These are the TCP ports the instance listens on:

sudo ss -Hltn | awk '{print $4}' | sort -u
0.0.0.0:22
0.0.0.0:443
0.0.0.0:80
127.0.0.1:5000
127.0.0.53%lo:53
127.0.0.54:53
[::]:22
[::]:443
[::]:80

ByteStash (5000) listens on loopback only; nginx publishes it on 443. The loopback port 53 entries are Ubuntu's local stub resolver (systemd-resolved).

The versions and the build record:

node --version
grep -E '^(ByteStash|source|tag commit|audit|database)' /usr/share/doc/cloudimg-bytestash/BUILD-INFO.txt
v22.23.4
ByteStash 1.6.0 on Ubuntu 24.04 - cloudimg build record
source           https://github.com/jordan-dalby/ByteStash/archive/refs/tags/v1.6.0.tar.gz
source sha256    6e534ea3b82e01e043ab55d042cb1ebab78e204d63ac35f5822bfd414c96f8f6
tag commit       da3a7afe93cf06c7c01d3cd09e4a2ea52aff4e6a
audit            server: 0 advisories; web UI bundle: katex + mermaid LOW only (GHSA-238p-pmpm-9mq7)
database         SQLite (/var/lib/bytestash/snippets/snippets.db); PostgreSQL mode off

Step 4: Sign in

Browse to the BYTESTASH_URL from Step 2, for example https://ec2-54-81-216-2.compute-1.amazonaws.com/. The certificate is self-signed, so your browser warns once; accept it, or install your own certificate (Step 8). ByteStash shows its sign-in page, without a link to register:

The ByteStash sign-in page with username and password fields, served over HTTPS

Sign in as admin with BYTESTASH_ADMIN_PASSWORD. Your snippet list opens; a new instance has none yet. The screenshot below is from an instance with a few snippets added. The Private and Public switch under the name toggles between your own snippets and the public ones, and the small vdev label next to the name is ByteStash's version tag, which upstream fills in only for its own container builds (the version is in the build record above):

The ByteStash snippet list with search, category filters and three snippet cards showing their languages and categories

The image ships a round-trip test that signs in through nginx as a browser would. It proves that the API refuses anonymous requests, that sign-up and a wrong password are refused, and that your admin password returns an administrator session that can read your snippets:

sudo /usr/local/sbin/cloudimg-bytestash-probe
PROBE_OK anonymous API 401, sign-up 403, wrong password 401, admin JWT (is_admin), verify valid, authenticated API 200 at https://ec2-54-81-216-2.compute-1.amazonaws.com/

You can confirm sign-up is closed from the command line too. Port 80 only redirects to HTTPS, ByteStash reports that new accounts are not allowed, a registration attempt is refused, and the account table holds only admin. The block stops with an error if any answer is not the expected one:

H=$(sudo sed -n 's/^set $bs_host "\(.*\)";$/\1/p' /etc/nginx/cloudimg-bytestash-host.conf)
C=(--cacert /etc/ssl/cloudimg/bytestash.crt --resolve "$H:443:127.0.0.1" --resolve "$H:80:127.0.0.1")
R=$(curl -s "${C[@]}" -o /dev/null -w '%{http_code} %{redirect_url}' "http://$H/"); echo "$R"
A=$(curl -s "${C[@]}" "https://$H/api/auth/config" | jq -c '{allowNewAccounts, hasUsers, disableAccounts}'); echo "$A"
G=$(curl -s "${C[@]}" -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' -d '{"username":"someone","password":"Some-Password-123"}' "https://$H/api/auth/register"); echo "register: $G"
U=$(sudo sqlite3 "file:/var/lib/bytestash/snippets/snippets.db?mode=ro" "SELECT username FROM users;"); echo "$U"
[ "$R" = "301 https://$H/" ] && [ "$A" = '{"allowNewAccounts":false,"hasUsers":true,"disableAccounts":false}' ] && [ "$G" = 403 ] && [ "$U" = admin ]
301 https://ec2-54-81-216-2.compute-1.amazonaws.com/
{"allowNewAccounts":false,"hasUsers":true,"disableAccounts":false}
register: 403
admin

Step 5: Create and share a snippet

Choose New snippet, give it a title, add one or more files with their language, optional categories and a Markdown description, and save. Open a snippet to read it with syntax highlighting, copy a file, or edit it:

A ByteStash snippet open in the viewer, with its description, categories and a Python file shown with syntax highlighting

A snippet is private to you unless you tick Make snippet public, which lists it under Public snippets for anybody who can reach the instance. To show one snippet to someone without making it public, open its share menu and choose Create Share Link: the link can Require authentication and can expire (for example 2d). Move to Recycle Bin keeps a snippet for 30 days before it is deleted, and from 1.6.0 a recycled snippet is no longer served publicly.

Step 6: Use the REST API with an API key

Create an API key from the user menu (your account name, top right), API Keys, and copy it; ByteStash shows it once. Scripts send it in an x-api-key header. On your instance you can try it like this (put your key in a file readable only by you first, so it never appears on a command line):

umask 077; printf 'x-api-key: %s\n' '<your-api-key>' > ~/.bytestash-key
curl -s --cacert /etc/ssl/cloudimg/bytestash.crt -H @$HOME/.bytestash-key \
  "https://<your-dns-name>/api/snippets?search=python" | jq '.data[] | {id, title}'

The full API is described at https://<your-dns-name>/api-docs.

Step 7: Connect an AI assistant over MCP

ByteStash serves a remote MCP (Model Context Protocol) endpoint at https://<your-host>/mcp. Add it to an MCP client (Claude, ChatGPT, Perplexity or any client that supports remote MCP servers over HTTP) with your API key as the bearer token, for example in a client configuration file:

{
  "mcpServers": {
    "bytestash": {
      "url": "https://<your-dns-name>/mcp",
      "headers": { "Authorization": "Bearer <your-api-key>" }
    }
  }
}

The assistant can then list, search, read, create, update and recycle your snippets with the six ByteStash tools. The endpoint refuses requests without a valid key. Your client must trust the instance's certificate, so install your own certificate first (Step 8). An assistant has the same access as your account, so give it a key of its own and delete the key under API Keys in the user menu when you no longer need it.

Step 8: HTTPS certificate and host name

The certificate was created on your instance's first boot for the instance's public DNS name, public IP, private IP and localhost. The key, the JWT secret, the credentials file and the data directory are readable only by their owners:

sudo openssl x509 -in /etc/ssl/cloudimg/bytestash.crt -noout -subject -ext subjectAltName
sudo stat -c '%a %U:%G %n' /etc/ssl/cloudimg/bytestash.key /etc/bytestash/jwt-secret /root/bytestash-credentials.txt /var/lib/bytestash
subject=CN = ec2-54-81-216-2.compute-1.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name: 
    DNS:ec2-54-81-216-2.compute-1.amazonaws.com, IP Address:54.81.216.2, IP Address:172.31.81.44, IP Address:127.0.0.1, DNS:localhost
600 root:root /etc/ssl/cloudimg/bytestash.key
640 root:bytestash /etc/bytestash/jwt-secret
600 root:root /root/bytestash-credentials.txt
750 bytestash:bytestash /var/lib/bytestash

To serve ByteStash under your own DNS name (for example snippets.example.com, pointed at this instance), run the helper. It records the name, creates a new self-signed certificate for it, updates nginx and rewrites the URL in the credentials file:

sudo bytestash-set-hostname snippets.example.com

If the instance's public address changes after a stop and start, run the helper again. To use a certificate from your own certificate authority (recommended before others sign in or you connect an AI assistant), replace /etc/ssl/cloudimg/bytestash.key (keep it 0600 root) and /etc/ssl/cloudimg/bytestash.crt, then run sudo systemctl reload nginx.

Step 9: Outbound connections and metadata

ByteStash makes no outbound connection unless you turn on single sign-on. ByteStash and nginx cannot reach the instance metadata service, while the Amazon Time Sync Service at 169.254.169.123 keeps working, and the firmware-metadata refresh is masked:

systemctl show -p IPAddressDeny bytestash.service nginx.service
chronyc -n sources | grep 169.254
FW=$(systemctl is-enabled fwupd-refresh.timer fwupd-refresh.service || true); echo "$FW"
[ "$(sort -u <<<"$FW")" = masked ] && [ "$(systemctl show -p IPAddressDeny --value bytestash.service nginx.service | grep -c 169.254.169.254)" = 2 ]
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128

IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
^* 169.254.169.123               3   4    37    11  +5785ns[  -39us] +/-  522us
masked
masked

Maintenance

  • Your password: change it from the user menu (top right), Change password, then update /root/bytestash-credentials.txt or delete it.
  • More accounts: sign-up is closed. To let colleagues create their own accounts, set ALLOW_NEW_ACCOUNTS=true in /etc/bytestash/bytestash.env, run sudo systemctl restart bytestash, let them register, then set it back to false and restart again. Anyone who can reach the instance can register while it is true, so restrict the security group first. Accounts are administrators only if their name is in ADMIN_USERNAMES (default admin). Administrators manage users, snippets, API keys and share links in the Admin panel (user menu).
  • Single sign-on (OIDC): set OIDC_ENABLED=true and OIDC_ISSUER_URL, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET from your identity provider in /etc/bytestash/bytestash.env, register https://<your-dns-name>/api/auth/oidc/callback as the redirect URL with the provider, and restart ByteStash. The file is readable by root only.
  • Do not set DISABLE_ACCOUNTS=true. It turns every visitor into one shared anonymous user with full access; the start guard refuses it.
  • Backups: ByteStash keeps everything in the SQLite database in /var/lib/bytestash/snippets. Stop ByteStash before you copy it (sudo systemctl stop bytestash), start it again afterwards, and keep the backup as safe as the credentials: it holds your snippets, your API keys and the password hashes. Never write to the database while ByteStash is running.
  • Upgrades: to move to a newer release, launch the newer version from the listing when it is published and restore your backup.
  • Logs: sudo journalctl -u bytestash -n 100 --no-pager, and nginx's access log in /var/log/nginx/access.log.
  • Security patches: unattended-upgrades remains enabled so the OS keeps receiving security updates.

Security recommendations

  • Keep the security group tight. Open 22 only to your management address and 80/443 only to the people and API clients that use ByteStash. Public snippets and share links are readable by anyone who can reach port 443.
  • Use your own certificate before other people sign in or you connect an AI assistant, so they trust it without exceptions.
  • Keep ByteStash on loopback. Reach it through nginx; nginx is what keeps forwarded headers honest and refuses cross-site requests.
  • Give each script and assistant its own API key, and delete keys you no longer use.

Support and licensing

ByteStash is developed by Jordan Dalby and contributors and distributed under the GNU General Public License v3.0. This image ships ByteStash 1.6.0 built from the unmodified 1.6.0 source with two configuration-level edits (ByteStash listens on 127.0.0.1 and trusts forwarded headers only from the local nginx) and the dependency updates listed above; the complete 1.6.0 source archive, the licence and the build record are in /usr/share/doc/cloudimg-bytestash/, and the changed package manifests and lockfile are in /opt/bytestash. Node.js is distributed under the MIT License. cloudimg provides the packaging, the hardening, first-boot HTTPS and per-instance credentials, and 24/7 support.

cloudimg is not affiliated with or endorsed by the ByteStash project or its authors. Product names are marks of their owners and are used here only to identify the software.

For help with deployment or configuration, email support@cloudimg.co.uk or visit cloudimg support.