Media & Entertainment AWS

Black Candy 3.2.1 on AWS User Guide

| Product: Black Candy 3.2.1 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying Black Candy 3.2.1 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened SRS Streaming Media Server & Media Servers AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other media and streaming products in the listing are described on the listing's guide hub.

Black Candy is a free, self-hosted music streaming server (MIT licence). It scans a folder of music files into a library of artists, albums and songs, and plays it in a responsive web player with playlists, a play queue, favourites and search. Songs in formats a browser cannot play are transcoded on the fly with ffmpeg. The same server also backs Black Candy's iOS and Android apps through its token-authenticated API.

What is on the image:

  • Black Candy 3.2.1, built from the upstream v3.2.1 tag source archive (commit 93cfdca9), on Ruby 4.0.9 compiled from source
  • Rails gems from a cloudimg lockfile that raises every gem with a published security advisory to a fixed release within its line (Rails 8.1.4, Nokogiri 1.19.4, Ransack 4.4.4, HTTParty 0.24.3 and others; see Security posture)
  • The app server (puma) on 127.0.0.1:3000 only, behind nginx serving HTTPS on port 443 with a certificate created on your instance; port 80 redirects to HTTPS
  • A per-instance administrator account created on first boot; the upstream default account is never created
  • ffmpeg for transcoding and libvips for album cover art, both from Ubuntu
  • Two short generated demo tracks (cloudimg Demo Album), so the library is not empty on first sign-in
  • black-candy-set-hostname, a helper that serves Black Candy under your own DNS name, and black-candy-selftest, an end-to-end check

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

Security posture

  • No default account. Upstream's database seed creates an administrator admin@admin.com with the password foobar. This image never runs that seed. First boot creates exactly one account, admin@blackcandy.local, with a 32-character password generated on your instance, and writes it to a root-only file. The password is handed to Rails through a temporary root-only file, never on a command line or in a logged environment.
  • Per-instance secrets. The Rails SECRET_KEY_BASE (which signs sessions and API tokens), the administrator password and the TLS key are all generated on your instance. The image ships without any database: the four SQLite databases (library, cache, cable and job queue) are created on first boot, so no two instances share a secret or a session.
  • Fails closed. The app (black-candy.service) starts only after first boot has written its secret, databases and administrator, and nginx only after first boot has finished. If first boot cannot finish, nothing is served.
  • Only nginx faces the network. Exactly ports 22, 80 and 443 listen beyond loopback; puma listens on 127.0.0.1:3000. The app runs as the unprivileged blackcandy user under a systemd sandbox (read-only system, no home directories, no capabilities, its own /tmp).
  • HTTPS from first boot. nginx serves TLS 1.2 and 1.3 with a self-signed certificate created on your instance (TLS 1.1 is refused). Rails runs with FORCE_SSL, so session cookies are Secure and HttpOnly and responses carry Strict-Transport-Security.
  • The host name comes from configuration, never from a request. First boot reads the instance's public DNS name (else its public or private IPv4 address) from EC2 instance metadata (IMDSv2) and pins nginx to it. Requests for any other host name get their connection closed; requests to the instance's IP addresses are redirected to that name; the app is always handed the configured name, and client-sent X-Forwarded-Host headers are dropped.
  • Client address from the connection. nginx replaces X-Forwarded-For and X-Real-IP with the real peer address and drops Forwarded and Client-IP; Rails trusts forwarded headers from loopback only. At build time a sign-in carrying a forged X-Forwarded-For: 127.0.0.1 was recorded with the real peer address.
  • The app cannot read instance metadata. The app, the first-boot unit and nginx are blocked from the instance metadata service (169.254.169.254, fd00:ec2::254) by systemd IPAddressDeny; only the small unit that looks up the host name at first boot may use it. ffmpeg, started by the app for transcoding, runs inside the same block.
  • Cross-site protection. nginx refuses state-changing requests and WebSocket upgrades from other sites. The transcoding stream (/transcoded_stream), a signed-in GET that starts ffmpeg, is also refused unless the browser shows it is a same-site request. The /api/v1 routes accept only an API token in the Authorization header, never the browser's session cookie.
  • Dependencies. The Black Candy 3.2.1 lockfile pins gems with published advisories (among them an Active Storage file-read issue, CVE-2026-66066). This image raises each one within its release line, with no change to Black Candy's own code; the details are in /opt/black-candy/SECURITY-NOTES.txt. Two advisories remain and do not apply here: puma 6.6.1's PROXY-protocol parsing (CVE-2026-47736 and CVE-2026-47737), which puma does only when told to (this image does not), and net-ssh, a development-only gem that is not installed. Ruby is 4.0.9 rather than the 4.0.2 the release names, which is affected by CVE-2026-46727. libvips runs with VIPS_BLOCK_UNTRUSTED=1, so its less-tested image loaders are disabled.
  • Residual: ffmpeg and libvips updates. Some Ubuntu universe packages this image needs (ffmpeg, libvips and their dependencies) receive certain security fixes only through Ubuntu Pro (ESM). Attaching the instance to Ubuntu Pro delivers them. Black Candy cannot work without these packages, so they stay installed; pro security-status --esm-apps lists the fixes, and Black Candy uses ffmpeg only on your own music files and libvips only on cover art.
  • No outbound calls by default. The server contacts nothing on its own. Cover art lookup through the Discogs API stays off until you enter a Discogs token in Settings. The web player shows account avatars from Gravatar, so the viewer's browser requests www.gravatar.com with a hash of the account e-mail address.
  • Hardened SSH and system. SSH allows keys only, no root login and no X11 forwarding. The AMI requires IMDSv2. SSH host keys and the machine ID are generated fresh on every instance.

Prerequisites

  1. An AWS account and permission to launch EC2 instances
  2. An EC2 key pair in the Region you launch in
  3. Your music files, which you copy onto the instance (or onto an extra EBS volume you attach)

Recommended instance type: m5.large. Black Candy uses about 500 MiB of memory at rest; transcoding is the main CPU load. Music lives on the root volume by default: increase it at launch to fit your library.

Connecting to your instance

Version Operating system SSH user
Black Candy 3.2.1 on Ubuntu 24.04 Ubuntu 24.04 LTS ubuntu

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. Open the cloudimg Hardened SRS Streaming Media Server & Media Servers 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 Black Candy 3.2.1 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: the listing's default group opens SSH (22), HTTP (80) and HTTPS (443). Restrict SSH to your own IP address, and restrict 80 and 443 to the networks your listeners use.
  6. Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.

Option B: AWS CLI

Run this on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDRs with your own:

SG=$(aws ec2 create-security-group --group-name black-candy \
  --description "Black Candy music streaming" --vpc-id vpc-0123456789abcdef0 \
  --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 80 --cidr <your-mgmt-cidr>
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 \
  --block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=50,VolumeType=gp3}' \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=black-candy}]'

The block device mapping grows the root volume to 50 GiB for a music library; leave it out to keep 12 GiB. You do not need --metadata-options: the AMI already makes IMDSv2 required. Associate an Elastic IP if you want the address to survive a stop and start.

Step 2: Connect and find your login

Connect as ubuntu with the key pair you chose:

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

First boot takes under a minute after the instance starts. It sets the name Black Candy is served as (the instance's public DNS name when it has one), creates the TLS certificate, the Rails secret, the databases and the administrator, imports the demo tracks, and writes /var/lib/cloudimg/black-candy-credentials.txt (mode 0600, root). These are the entries in that file (the password is not shown here):

sudo grep -E '^(BLACKCANDY_URL|ADMIN_EMAIL)=' /var/lib/cloudimg/black-candy-credentials.txt
sudo grep -oE '^[A-Z_]+=' /var/lib/cloudimg/black-candy-credentials.txt
sudo stat -c '%a %U:%G %n' /var/lib/cloudimg/black-candy-credentials.txt /etc/black-candy/secret.env
BLACKCANDY_URL=https://ec2-54-159-2-193.compute-1.amazonaws.com/
ADMIN_EMAIL=admin@blackcandy.local
BLACKCANDY_URL=
ADMIN_EMAIL=
ADMIN_PASSWORD=
600 root:root /var/lib/cloudimg/black-candy-credentials.txt
640 root:blackcandy /etc/black-candy/secret.env
Value What it is for
BLACKCANDY_URL The address of this instance's Black Candy
ADMIN_EMAIL Sign-in e-mail of the administrator (admin@blackcandy.local; you can change it in Settings)
ADMIN_PASSWORD Administrator password, generated on this instance

To read the password, run sudo cat /var/lib/cloudimg/black-candy-credentials.txt in your own session and store it in your password manager. /etc/black-candy/secret.env holds the Rails secret, readable only by root and the app.

Step 3: Confirm Black Candy is running

The two first-boot units have finished, the app and nginx are active, and only sshd and nginx listen beyond loopback (puma is on 127.0.0.1:3000, the resolver on 127.0.0.53):

systemctl is-active black-candy-host.service black-candy-firstboot.service black-candy.service nginx.service
sudo ss -Htlnp | awk '{print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u
head -5 /opt/black-candy/VERSION
/opt/ruby/bin/ruby -v
active
active
active
active
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.1:3000 users:(("bundle"),("bundle"),("bundle"),("bundle"))
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"))
black_candy_version=3.2.1
black_candy_tag=v3.2.1
source_commit=93cfdca9857a5e2ae847469c3238a6174515c76f
source_sha256=5afb72e8abfb1424fb377d6c2ea3266df00c4f36448eb5b8cfc2135b314e3c98
ruby_version=4.0.9 (ruby-build v20261023)
ruby 4.0.9 (2026-10-23 revision 3e93b23afc) +PRISM [x86_64-linux]

Step 4: Prove the login and the web defaults

The sign-in page answers, the library and the stream API refuse a signed-out request, plain HTTP redirects to HTTPS, an unknown Host header gets no answer and TLS 1.1 is not offered. The last command is the image's own round-trip check: it signs in to the API with this instance's administrator password, streams a demo track, proves that an unauthenticated stream is refused, and revokes its token:

H=$(sed -n 's/^set \$bc_host "\(.*\)";$/\1/p' /etc/nginx/black-candy-hostvar.conf)
C=(--connect-to ::127.0.0.1:)
curl -sk "${C[@]}" -o /dev/null -w 'sign-in page -> %{http_code}\n' "https://$H/sessions/new"
curl -sk "${C[@]}" -o /dev/null -w 'library, signed out -> %{http_code} %{redirect_url}\n' "https://$H/albums"
curl -sk "${C[@]}" -o /dev/null -w 'stream API, no token -> %{http_code}\n' "https://$H/api/v1/stream/new?song_id=1"
curl -s "${C[@]}" -o /dev/null -w 'http -> %{http_code} %{redirect_url}\n' "http://$H/"
curl -sk -o /dev/null -w 'unknown Host -> %{http_code}\n' -H 'Host: evil.example' https://127.0.0.1/ || true
curl -sk "${C[@]}" -o /dev/null -w 'TLS 1.1 -> %{http_code}\n' --tlsv1.1 --tls-max 1.1 "https://$H/" || true
sudo black-candy-selftest
sign-in page -> 200
library, signed out -> 302 https://ec2-54-159-2-193.compute-1.amazonaws.com/sessions/new
stream API, no token -> 401
http -> 301 https://ec2-54-159-2-193.compute-1.amazonaws.com/
unknown Host -> 000
TLS 1.1 -> 000
round-trip OK: administrator authenticated over https://ec2-54-159-2-193.compute-1.amazonaws.com/, song 1 streamed 198932 bytes of MPEG audio, unauthenticated stream refused (401), token revoked

--connect-to ::127.0.0.1: sends each request to this instance while keeping the real host name in the request, exactly as a browser would send it. 000 means the server closed the connection without an answer.

Step 5: Sign in to Black Candy

Browse to BLACKCANDY_URL (https://<public-dns-name>/). The certificate is self-signed, so your browser warns once; accept it for this host, or install your own certificate (Step 8). Sign in with ADMIN_EMAIL and ADMIN_PASSWORD.

The Black Candy sign-in page asking for the e-mail address and password of the administrator generated on this instance

After signing in, Library lists your albums with their cover art; the demo album is already imported:

The Black Candy library showing the cloudimg Demo Album with its cover art, the empty play queue on the left and the player controls

Open an album and choose Play: the tracks go into the play queue on the left and stream from your instance over HTTPS:

The cloudimg Demo Album playing in Black Candy, with both tracks in the queue and the now-playing panel showing the cover art and elapsed time

The account menu (your avatar, top right) holds Settings, Manage Users, Update Profile and Logout. Open Update Profile first to change the e-mail address and password, and add accounts for other listeners under Manage Users (there is no public sign-up). Settings holds the media path, the Sync button, the transcoding bitrate and the optional Discogs token.

The Black Candy Settings page with the theme, the Library section holding the media path /var/lib/black-candy/media and the Sync button, and the empty Discogs token under Integration

Step 6: Add your music

Black Candy reads music from /var/lib/black-candy/media. Copy your files there (any folder layout; Black Candy reads the artist, album and track tags, and embedded cover art), give the folder back to the blackcandy user, then choose Sync on the Settings page:

scp -r ~/Music/* ubuntu@<public-ip>:/tmp/music/
sudo cp -r /tmp/music/. /var/lib/black-candy/media/
sudo chown -R blackcandy:blackcandy /var/lib/black-candy/media

The demo tracks are generated sine tones; delete them from the media folder and sync again when your own music is in place. Supported formats are MP3, FLAC, OGG/Oga, Opus, WAV, M4A and WMA; formats a browser cannot play (WMA everywhere, Apple Lossless outside Safari, Ogg on Apple devices) are transcoded to MP3 when they are streamed. To keep a large library on its own disk, attach an EBS volume, mount it, and set its path as the media path in Settings.

The library and the demo tracks as Black Candy imports them:

ls -l /var/lib/black-candy/media
cd /opt/black-candy/app && sudo bash -c 'set -a; . /etc/black-candy/black-candy.env; . /etc/black-candy/secret.env; set +a; setpriv --reuid=blackcandy --regid=blackcandy --init-groups -- /opt/ruby/bin/bundle exec rails runner "puts Song.order(:id).map { |s| [s.name, s.album.name, s.artist.name, s.format].join(\" | \") }"'
total 392
-rw-r--r-- 1 blackcandy blackcandy 199994 Oct 27 15:38 01-cloudimg-test-tone.mp3
-rw-r--r-- 1 blackcandy blackcandy 199994 Oct 27 15:38 02-cloudimg-test-tone-high.mp3
cloudimg Test Tone | cloudimg Demo Album | cloudimg | mp3
cloudimg Test Tone (High) | cloudimg Demo Album | cloudimg | mp3

Step 7: Check the security controls

The app, its first-boot unit and nginx carry IPAddressDeny for the metadata addresses, first boot recorded that it could not reach the metadata service from inside its own unit, the app runs sandboxed as blackcandy, and SSH runs with the cloudimg pins:

systemctl show -p IPAddressDeny black-candy.service black-candy-firstboot.service nginx.service
systemctl show -p User -p ProtectSystem -p ProtectHome -p NoNewPrivileges black-candy.service
sudo grep -o 'IMDS-REFUSED-IN-FIRSTBOOT.*' /var/log/cloudimg-firstboot.log
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|x11forwarding) '
IPAddressDeny=168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128

IPAddressDeny=168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128

IPAddressDeny=168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
User=blackcandy
ProtectHome=yes
ProtectSystem=strict
NoNewPrivileges=yes
IMDS-REFUSED-IN-FIRSTBOOT (http 000; IPAddressDeny in effect)
permitrootlogin no
passwordauthentication no
x11forwarding no

Step 8: Serve Black Candy under your own DNS name

Point a DNS record (for example music.example.com) at the instance's public or Elastic IP address, then tell the image to serve that name. It creates a new self-signed certificate for the name, pins nginx and both cross-site guards to it, updates BLACKCANDY_URL in the credentials file and keeps the name across reboots:

sudo black-candy-set-hostname music.example.com

For a trusted certificate, obtain one for that name (for example with certbot's certonly mode using its own temporary web server, or from your organisation's CA), copy the certificate chain and key over /etc/ssl/black-candy/black-candy.crt and /etc/ssl/black-candy/black-candy.key (key mode 0600, root), and run sudo systemctl reload nginx.

Step 9: Change passwords and accounts

  • Administrator: sign in and change the password under Update Profile in the account menu. The credentials file is not updated; remove the old password from it (sudo nano /var/lib/cloudimg/black-candy-credentials.txt) once you have stored the new one.
  • Other listeners: add them under Manage Users; give administrator rights only to people who should manage the library and users.
  • Mobile apps: the Black Candy iOS and Android apps connect to BLACKCANDY_URL. They require a certificate the phone trusts, so set up your DNS name and certificate first (Step 8).

Maintenance

  • Operating system updates: unattended security upgrades are on. Reboot when /var/run/reboot-required exists; Black Candy and nginx start again by themselves.
  • Black Candy updates: Black Candy is installed from source under /opt/black-candy/app with its gems in vendor/bundle. To move to a newer release, launch the newer version of this image (when the listing offers it) and move your media folder and /opt/black-candy/app/storage (the databases) across, or rebuild the app tree from the upstream release following Black Candy's own documentation.
  • Backups: the databases are in /opt/black-candy/app/storage (SQLite) and the music in /var/lib/black-candy/media. Stop the app (sudo systemctl stop black-candy) and copy both, or take an EBS snapshot of the volume.
  • Logs: sudo journalctl -u black-candy for the app, /var/log/nginx/ for web requests, /var/log/cloudimg-firstboot.log for first boot.
  • What the instance contacts: Black Candy itself contacts nothing until you enter a Discogs token. Measured on a new instance from its first boot through the following ten minutes, every outbound connection came from the operating system: the AWS Systems Manager agent (ssm, ec2messages and its S3 bucket), snapd (api.snapcraft.io) and the time service (Amazon Time Sync plus Ubuntu's NTP pool). Black Candy and nginx made none. Disable the SSM agent if you do not use Session Manager.

Troubleshooting

  • The web page does not load right after launch: first boot is still running. sudo journalctl -u black-candy-host.service -u black-candy-firstboot.service -f shows its progress.
  • Browser shows "connection closed": nginx answers only the configured name and redirects the instance's own IP addresses to it. Use BLACKCANDY_URL, or run sudo black-candy-set-hostname <name> for a new name.
  • New music does not appear: check the files are owned by blackcandy (Step 6) and choose Sync in Settings; sudo journalctl -u black-candy -n 50 shows the sync job.
  • A song does not play: formats a browser cannot play are transcoded; sudo journalctl -u black-candy -n 50 shows ffmpeg errors for unreadable files.
  • The end-to-end check: sudo black-candy-selftest signs in to the API, streams a song and prints round-trip OK when everything works.

Licensing

Black Candy is licensed under the MIT licence; the licence text is on the instance in /opt/black-candy/LICENSE, and the source of this release is at github.com/blackcandy-org/blackcandy/tree/v3.2.1. cloudimg is not affiliated with, endorsed by or sponsored by the Black Candy project or its author.

Support

cloudimg provides 24/7 technical support for this image by email and live chat: deployment, first login, HTTPS and DNS setup, upgrades and troubleshooting. Contact support@cloudimg.co.uk.