Black Candy 3.2.1 on AWS User Guide
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.1tag source archive (commit93cfdca9), 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:3000only, 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, andblack-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.comwith the passwordfoobar. 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 unprivilegedblackcandyuser 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 areSecureandHttpOnlyand responses carryStrict-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-Hostheaders are dropped. - Client address from the connection. nginx replaces
X-Forwarded-ForandX-Real-IPwith the real peer address and dropsForwardedandClient-IP; Rails trusts forwarded headers from loopback only. At build time a sign-in carrying a forgedX-Forwarded-For: 127.0.0.1was 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/v1routes accept only an API token in theAuthorizationheader, 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 withVIPS_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-appslists 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.comwith 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
- An AWS account and permission to launch EC2 instances
- An EC2 key pair in the Region you launch in
- 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
- 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.
- Once the subscription is active, choose Launch your software.
- 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.
- Pick an instance type (
m5.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - 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.
- 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.

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

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

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.

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-requiredexists; Black Candy and nginx start again by themselves. - Black Candy updates: Black Candy is installed from source under
/opt/black-candy/appwith its gems invendor/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-candyfor the app,/var/log/nginx/for web requests,/var/log/cloudimg-firstboot.logfor 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,ec2messagesand 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 -fshows 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 runsudo 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 50shows the sync job. - A song does not play: formats a browser cannot play are transcoded;
sudo journalctl -u black-candy -n 50shows ffmpeg errors for unreadable files. - The end-to-end check:
sudo black-candy-selftestsigns in to the API, streams a song and printsround-trip OKwhen 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.