Black Candy 3.2.1
Overview
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. It plays the library 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.
This guide covers launching and verifying Black Candy 3.2.1 on Ubuntu 24.04 from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud 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.
What you get:
- Black Candy 3.2.1 from source. It is built from the upstream
v3.2.1tag source archive (commit93cfdca9) on Ruby 4.0.9, compiled from source. - Patched Rails gems. A SecureAnyCloud lockfile raises every gem with a published security advisory to a fixed release within its line. Examples are Rails 8.1.4, Nokogiri 1.19.4, Ransack 4.4.4 and HTTParty 0.24.3 (see Security posture).
- HTTPS in front of a loopback app server. The app server (puma) listens 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 and libvips. ffmpeg handles transcoding and libvips handles album cover art, both from Ubuntu.
- Demo tracks. Two short generated tracks (SecureAnyCloud Demo Album) mean the library is not empty on first sign-in.
- Helpers.
black-candy-set-hostnameserves Black Candy under your own DNS name, andblack-candy-selftestis an end-to-end check. - 24/7 SecureAnyCloud support
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. It writes the password 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. First boot creates the four SQLite databases (library, cache, cable and job queue), 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. nginx starts 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, and puma listens on
127.0.0.1:3000. The app runs as the unprivilegedblackcandyuser under a systemd sandbox: a read-only system, no home directories, no capabilities and its own/tmp. - HTTPS from first boot. nginx serves TLS 1.2 and 1.3 with a self-signed certificate created on your instance, and TLS 1.1 is refused. Rails runs with
FORCE_SSL, so session cookies areSecureandHttpOnly, and responses carryStrict-Transport-Security. - The host name comes from configuration, never from a request. First boot reads the instance's public DNS name from EC2 instance metadata (IMDSv2) and pins nginx to it. If there is no public DNS name, it uses the public or private IPv4 address. Requests for any other host name get their connection closed, and 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. Systemd
IPAddressDenyblocks the app, the first-boot unit and nginx from the instance metadata service (169.254.169.254, fd00:ec2::254). Only the small unit that looks up the host name at first boot may use it. ffmpeg, which the app starts 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) is a signed-in GET that starts ffmpeg. It 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. - Advisories that remain and do not apply. puma 6.6.1's PROXY-protocol parsing (CVE-2026-47736 and CVE-2026-47737) runs only when puma is told to parse it, and this image does not. net-ssh is a development-only gem that is not installed.
- Ruby and libvips. Ruby is 4.0.9 rather than the 4.0.2 that 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). Attach the instance to Ubuntu Pro to receive them. Black Candy cannot work without these packages, so they stay installed.
pro security-status --esm-appslists the fixes. 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 email 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, and transcoding is the main CPU load.
Music lives on the root volume by default. Increase the volume size 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
Launch the instance
Option A: AWS Marketplace console
- Open the SecureAnyCloud Hardened SRS Streaming Media Server & Media Servers AMIs listing on AWS Marketplace. Choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- When 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 and your key pair.
m5.largeis recommended, and the software charge is $0.04 per vCPU per hour. - Review the security group. The listing's default group opens SSH (22), HTTP (80) and HTTPS (443). Restrict SSH to your own IP address, and restrict ports 80 and 443 to the networks your listeners use.
- Choose your VPC and subnet. Leave the metadata defaults, because 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. Replace 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, because the AMI already requires IMDSv2. Associate an Elastic IP if you want the address to survive a stop and start.
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 does the following:
- Sets the name Black Candy is served as, which is 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.
- Writes
/var/lib/sac/data/black-candy-credentials.txt(mode 0600, root).
The following commands show the entries in that file. The password is not shown here:
sudo grep -E '^(BLACKCANDY_URL|ADMIN_EMAIL)=' /var/lib/sac/data/black-candy-credentials.txt
sudo grep -oE '^[A-Z_]+=' /var/lib/sac/data/black-candy-credentials.txt
sudo stat -c '%a %U:%G %n' /var/lib/sac/data/black-candy-credentials.txt /etc/black-candy/secret.envBLACKCANDY_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/sac/data/black-candy-credentials.txt 640 root:blackcandy /etc/black-candy/secret.envValue What it is for
BLACKCANDY_URL The address of this instance's Black Candy
ADMIN_EMAIL Sign-in email 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/sac/data/black-candy-credentials.txt in your own session. Store it in your password manager.
/etc/black-candy/secret.env holds the Rails secret, which only root and the app can read.
Confirm Black Candy is running
Check that the two first-boot units have finished, that the app and nginx are active, and that only sshd and nginx listen beyond loopback. Puma is on 127.0.0.1:3000 and the resolver is 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 -vactive 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]Prove the login and the web defaults
These checks show five results. 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. 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-selftestsign-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. A result of 000 means the server closed the connection without an answer.
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 you sign 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 email address and password.
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.
Add your music
Black Candy reads music from /var/lib/black-candy/media. Copy your files there. Any folder layout works, because Black Candy reads the artist, album and track tags and any 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/mediaThe 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 are transcoded to MP3 when they are streamed. These are WMA everywhere, Apple Lossless outside Safari, and Ogg on Apple devices.
To keep a large library on its own disk, attach an EBS volume and mount it. Then 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-sac-test-tone.mp3 -rw-r--r-- 1 blackcandy blackcandy 199994 Oct 27 15:38 02-sac-test-tone-high.mp3 sac Test Tone | sac Demo Album | sac | mp3 sac Test Tone (High) | sac Demo Album | sac | mp3Check the security controls
This check shows four results. 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. SSH runs with the SecureAnyCloud 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/sac/sac-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 noServe Black Candy under your own DNS name
Point a DNS record at the instance's public or Elastic IP address, for example music.example.com. Then tell the image to serve that name. The helper 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.comFor a trusted certificate, obtain one for that name. For example, use certbot's certonly mode with its own temporary web server, or use 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). Then run sudo systemctl reload nginx.
Change passwords and accounts
- Administrator. Sign in and change the password under Update Profile in the account menu. The credentials file is not updated, so remove the old password from it once you have stored the new one. Use
sudo nano /var/lib/sac/data/black-candy-credentials.txt. - 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/app, with its gems invendor/bundle. To move to a newer release, launch the newer version of this image when the listing offers it. Then move your media folder and/opt/black-candy/app/storage(the databases) across. Alternatively, 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 is in/var/lib/black-candy/media. Stop the app withsudo systemctl stop black-candyand copy both, or take an EBS snapshot of the volume. - Logs. Run
sudo journalctl -u black-candyfor the app. Web requests are logged in/var/log/nginx/and first boot in/var/log/sac/sac-firstboot.log. - What the instance contacts. Black Candy itself contacts nothing until you enter a Discogs token. We measured 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. Run
sudo journalctl -u black-candy-host.service -u black-candy-firstboot.service -fto see its progress. - The 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 that
blackcandyowns the files (Step 6) and choose Sync in Settings. Runsudo journalctl -u black-candy -n 50to see the sync job. - A song does not play. Formats a browser cannot play are transcoded. Run
sudo journalctl -u black-candy -n 50to see 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.
SecureAnyCloud is not affiliated with, endorsed by or sponsored by the Black Candy project or its author.
Support
SecureAnyCloud provides 24/7 technical support for this image by email and live chat. Support covers deployment, first login, HTTPS and DNS setup, upgrades and troubleshooting. Contact sales@secureanycloud.com.
Deploy on AWS
Launch Black Candy 3.2.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need help?
Our support team is available 24/7.
sales@secureanycloud.com