Deployment guide

BloodHound CE

Videos

Overview: BloodHound CE on Ubuntu 24.04 LTS on Azure: overview (watch on YouTube)

Launch and log in: BloodHound CE on Ubuntu 24.04 LTS on Azure: launch and log in (watch on YouTube)

Deep Dive: BloodHound CE on Ubuntu 24.04 LTS on Azure: deep dive (watch on YouTube)

Overview

BloodHound Community Edition uses graph theory to reveal the hidden and often unintended privilege relationships inside an Active Directory, Entra ID or Azure environment. Attackers use it to find complex attack paths that would be impractical to spot by hand, and defenders use it to find and cut those same paths.

You feed it collector output from SharpHound or AzureHound, and it computes the reachability graph. The graph shows who can ultimately reach Domain Admin, which principals hold DCSync rights, which computers expose LAPS passwords, and more.

The SecureAnyCloud image installs BloodHound CE 9.4.0 as a pinned Docker Compose stack managed by systemd, so a working attack path analysis platform is online within minutes of launch.

What you get:

  • BloodHound Community Edition 9.4.0 (Apache-2.0), from the official published image docker.io/specterops/bloodhound:9.4.0, pinned so the appliance never silently upgrades
  • Docker Engine (Docker CE) and the Docker Compose plugin, from the official Docker package repository
  • PostgreSQL 18 (docker.io/library/postgres:18) holding both the application state and the attack path graph. BloodHound's first-party PostgreSQL graph driver is used, so there is no Neo4j service to run, tune or license
  • The BloodHound web UI and REST API published on port 80. The database publishes no host port at all and is reachable only on the private container network
  • All stateful data on a dedicated 64 GiB data disk mounted at /var/lib/bloodhound (Docker's data-root is relocated there), independently resizable and re-provisioned on every VM
  • A per-VM admin password generated at first boot, which BloodHound requires you to change the first time you sign in
  • A genuine SharpHound sample dataset staged at /opt/bloodhound/sample-data/ so you can explore real attack paths immediately
  • Two systemd units: bloodhound.service (a oneshot wrapper around docker compose up -d) and bloodhound-firstboot.service
  • 24/7 SecureAnyCloud support

Why there is no Neo4j

Upstream's example Compose file runs BloodHound against Neo4j. This image does not. BloodHound CE ships a first-party PostgreSQL graph backend, the same one that SpecterOps' own Debian package uses. The appliance therefore runs the application and the graph on a single PostgreSQL 18 instance.

That means one less database to size and secure, a smaller memory footprint with no JVM, and a stack whose components are Apache-2.0 and the PostgreSQL License. Nothing about attack path analysis is reduced: the built-in query library, path finding and the Cypher API all work exactly as upstream intends.

Prerequisites

  • An active Azure subscription, an SSH key, and a VNet and subnet.
  • Standard_B4ms (4 vCPU / 16 GB RAM) is the recommended size. SpecterOps documents a floor of 8 GB RAM and 4 cores for a standard BloodHound environment and warns about out-of-memory errors during startup below that, so a 2 vCPU / 4 GB size is not sufficient.
  • NSG inbound rules: allow 22/tcp from your management CIDR for SSH, and 80/tcp from the CIDR that needs the BloodHound web UI and API. Add 443/tcp if you put your own TLS in front.

Deploy the Virtual Machine

Option A: Azure Portal

  • Open the offer in the Azure Marketplace and select Create.
  • Choose your subscription, resource group, VM name and region, and set Size to Standard_B4ms.
  • Under Administrator account, choose your username (azureuser in the examples below) and select SSH public key.
  • Under Inbound port rules, allow SSH (22), then add HTTP (80) for the BloodHound web UI and API.
  • Select Review + create, then Create.

Option B: Azure CLI

az vm image terms accept --urn sac1647283583153:bloodhound-ce-ubuntu-24-04:default:latest
az vm create \
  --resource-group myResourceGroup \
  --name my-bloodhound-ce-vm \
  --image sac1647283583153:bloodhound-ce-ubuntu-24-04:default:latest \
  --size Standard_B4ms \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard
az vm open-port --resource-group myResourceGroup --name my-bloodhound-ce-vm --port 80 --priority 900

Restrict the source address ranges on these rules to the networks that need them. In the Azure Portal, open the VM, then Networking.

1

Connect over SSH

Replace <vm-ip> with the public IP address of your VM. The default login user is azureuser.

ssh azureuser@<vm-ip>
2

Confirm the services are active

sudo systemctl is-active docker.service bloodhound.service docker --version docker compose version

The output shows active printed twice, the Docker Engine version, and the Docker Compose v2 plugin version.

3

Confirm the BloodHound containers are running

cd /opt/bloodhound && sudo docker compose -f docker-compose.yml --env-file .env ps

Exactly two services should be in the Up state:

  • bloodhound is the API server and UI, published as 0.0.0.0:80->8080/tcp.
  • app-db is PostgreSQL 18, reported as 5432/tcp with no host mapping, because the database is deliberately not published.
4

Confirm the data disk is mounted

All BloodHound state, including the application database and the whole attack path graph, lives on a dedicated data disk mounted at /var/lib/bloodhound. Docker's data-root points to this location.

df -h /var/lib/bloodhound findmnt -no SOURCE,TARGET,FSTYPE /var/lib/bloodhound
sudo docker info --format 'Docker data-root: {{.DockerRootDir}}'

To grow the volume later, resize the disk in the Azure portal and then run sudo resize2fs /dev/disk/azure/scsi1/lun0.

5

Retrieve your per-VM admin password

The image ships with no default or shared credential. On first boot, bloodhound-firstboot.service generates a fresh PostgreSQL password, a fresh JWT signing key and a unique admin password for this VM. It then writes them to a root-only file.

sudo cat /root/bloodhound-credentials.txt

This password is deliberately single use. The appliance marks the initial credential as expired, so BloodHound requires you to choose a new password the moment you sign in. Keep the new password safe, because this file is not updated when you change it.

6

Sign in to the BloodHound web UI

Browse to http://<vm-ip>/ui/login and sign in as admin with the password from Step 5.

Because the initial credential is issued already expired, BloodHound immediately asks you to set a new one. Enter the password from Step 5 as the Expired password, then choose and confirm your new password.

BloodHound enforces its own complexity policy on the new password: it must contain at least one uppercase character and at least one special character.

7

Check the API is healthy

BloodHound exposes an unauthenticated health endpoint. Point a load balancer or Azure health probe at this endpoint.

curl -s -o /dev/null -w 'health: HTTP %{http_code}\n' http://127.0.0.1/health
curl -s -o /dev/null -w 'ui: HTTP %{http_code}\n' http://127.0.0.1/ui/login

Both should report HTTP 200.

8

Authenticate against the REST API

Everything the UI does is also available over the REST API. This block reads the initial per-VM password straight from the credentials file, so it works as-is on a freshly launched VM.

Once you have changed your password in Step 6, set BH_PASS to your new password instead, because the credentials file is not updated when you rotate.

BH_PASS="$(sudo grep '^BLOODHOUND_ADMIN_PASSWORD=' /root/bloodhound-credentials.txt | cut -d= -f2-)" curl -s -X POST http://127.0.0.1/api/v2/login \
  -H 'Content-Type: application/json' \
  -d "{\"login_method\":\"secret\",\"username\":\"admin\",\"secret\":\"${BH_PASS}\"}" \
  | python3 -c 'import json,sys; d=json.load(sys.stdin).get("data"); print("login OK auth_expired:", d["auth_expired"], " token length:", len(d["session_token"])) if d else sys.exit("login failed")'

auth_expired reports whether BloodHound still wants a password change. While it is true, the rest of the API deliberately refuses the session token. That refusal is the forced rotation doing its job.

Rotate your password (Step 6) and re-authenticate to get a token that can drive ingest and queries.

9

Ingest the sample dataset and explore real attack paths

A genuine SharpHound collection is staged on the image, so you can see BloodHound working against real data before you point it at your own environment.

ls -la /opt/bloodhound/sample-data/

Upload the sample data through the UI. Sign in, choose Quick Upload in the left-hand navigation, and drop in the JSON files from /opt/bloodhound/sample-data/ad_sampledata.zip (unzip it locally first). BloodHound ingests the files and then runs its analysis pass to compute derived edges.

Once analysis finishes, open Explore, switch to the CYPHER tab and expand Saved Queries. BloodHound CE ships a library of prebuilt attack path queries.

Select All Domain Admins to run the query and render the graph. The graph shows every principal with a transitive MemberOf path into a Domain Admins group, across each domain in the dataset.

To collect from your own environment, use Download Collectors in the left-hand navigation. Get the SharpHound (Active Directory) or AzureHound (Entra ID and Azure) collector that matches this server version, run it against your environment, and upload the output the same way.

10

Managing the service

sudo systemctl restart bloodhound.service
sudo systemctl status bloodhound.service --no-pager | head -12

The unit is a oneshot wrapper around Docker Compose, so restart brings the whole stack down and back up. To read the application logs, run:

cd /opt/bloodhound && sudo docker compose -f docker-compose.yml --env-file .env logs --tail 20 bloodhound
11

Back up the database

PostgreSQL holds both the application state and the graph, so a single dump captures everything.

cd /opt/bloodhound
sudo docker compose -f docker-compose.yml --env-file .env exec -T app-db \
  sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB"' | gzip > /tmp/bloodhound-backup.sql.gz ls -lh /tmp/bloodhound-backup.sql.gz

Copy the resulting file off the VM and keep it with your other backups.

Security notes

  • No shared credential ships in the image. Upstream's example Compose file defaults both the PostgreSQL and Neo4j passwords to the published literal bloodhoundcommunityedition, and the upstream Debian package defaults the admin password to admin. None of those values exists anywhere in this image. Every secret (the database password, the JWT signing key and the admin password) is generated on your VM at first boot.
  • The initial admin password is single use. BloodHound issues it already expired, so you must change it at first sign-in before the account can do anything.
  • The database is not exposed. PostgreSQL declares no host port, so it is reachable only from the BloodHound container over the private Docker network, and authentication there is scram-sha-256.
  • Only port 80 is published. Before you expose the VM beyond a trusted network, put it behind an Azure Application Gateway, a load balancer with TLS termination, or your own reverse proxy. Restrict 80/tcp in the NSG to the CIDRs that need it.
  • BloodHound holds a complete map of privilege relationships in your directory. Treat the VM, its backups and its NSG rules as you would a tier-zero asset.

Support

SecureAnyCloud provides 24/7 support for this image. For questions about BloodHound itself, see the SpecterOps documentation.

Deploy on Azure

Launch BloodHound Community Edition 9.4.0 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure