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
adminpassword 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 arounddocker compose up -d) andbloodhound-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/tcpfrom your management CIDR for SSH, and80/tcpfrom the CIDR that needs the BloodHound web UI and API. Add443/tcpif 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 (
azureuserin 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 900Restrict the source address ranges on these rules to the networks that need them. In the Azure Portal, open the VM, then Networking.
Connect over SSH
Replace <vm-ip> with the public IP address of your VM. The default login user is azureuser.
ssh azureuser@<vm-ip>Confirm the services are active
sudo systemctl is-active docker.service bloodhound.service docker --version docker compose versionThe output shows active printed twice, the Docker Engine version, and the Docker Compose v2 plugin version.
Confirm the BloodHound containers are running
cd /opt/bloodhound && sudo docker compose -f docker-compose.yml --env-file .env psExactly two services should be in the Up state:
bloodhoundis the API server and UI, published as0.0.0.0:80->8080/tcp.app-dbis PostgreSQL 18, reported as5432/tcpwith no host mapping, because the database is deliberately not published.
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.
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.txtThis 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.
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.
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/loginBoth should report HTTP 200.
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.
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.
Managing the service
sudo systemctl restart bloodhound.service
sudo systemctl status bloodhound.service --no-pager | head -12The 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 bloodhoundBack 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.gzCopy 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 theadminpassword 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/tcpin 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
Overview
BloodHound Community Edition uses graph theory to reveal the hidden and unintended privilege relationships in an Active Directory, Entra ID or Azure environment. You feed it the output of the SharpHound and AzureHound collectors, and it maps every principal, group, computer, certificate template and trust into an interactive graph. You can then trace the exact attack paths an adversary could walk to reach Domain Admins or any other high-value target.
A built-in query library and pathfinding surface the shortest routes to compromise, so red teams can prove impact and blue teams can find and cut the edges that matter.
This SecureAnyCloud image runs BloodHound CE as the official upstream container (the Go API server plus the bundled React UI), published on host port 80 and orchestrated by docker compose under systemd. It is backed by a PostgreSQL 18 graph database.
What you get:
- BloodHound Community Edition 9.4.0, the Apache-2.0 licensed Active Directory and Entra ID attack path analysis platform, shipped as the official upstream container
- The BloodHound Go API and React UI on host port 80
- A bundled PostgreSQL 18 graph database (the application store and the attack-path graph), reachable only inside a private container network
- Docker Engine and the docker-compose plugin from the official Docker APT repository
- A unique PostgreSQL password, JWT signing key and single-use administrator password generated on each instance's first boot, on a fresh database
- The Docker data root and security database on a dedicated data volume at
/var/lib/bloodhound - A genuine SharpHound sample dataset staged at
/opt/bloodhound/sample-data/for immediate exploration docker.service,bloodhound-firstboot.serviceandbloodhound.serviceas systemd units- 24/7 SecureAnyCloud support
Why there is no Neo4j
This image deliberately runs BloodHound's first-party PostgreSQL graph backend rather than the end-of-life Neo4j 4.4 line that the upstream example uses. The shipped stack therefore carries no GPLv3 component and no end-of-life database.
PostgreSQL is reachable only inside the private container network, and it is never published to a host port. The Docker data root, which holds the pre-pulled image and the whole security database, lives on a dedicated data volume mounted at /var/lib/bloodhound. Your ingested data survives OS-disk changes, and the volume is resizable independently.
Security from first boot
BloodHound upstream ships build-time defaults for its PostgreSQL password (bloodhoundcommunityedition) and administrator password (admin). This image never ships those defaults to a customer.
Before the stack you see ever starts, a unique PostgreSQL password, JWT signing key and administrator password are generated for your instance on a fresh database. The initial admin password is deliberately single-use. BloodHound issues it already expired, so you must set a new password at your first sign-in.
Prerequisites
- An AWS account, an EC2 key pair in the target region, and a VPC with a public subnet.
- m5.large (2 vCPU / 8 GiB RAM) is the recommended instance type. BloodHound's own guidance is a floor of 8 GiB RAM.
- Security group inbound rules: allow
22/tcpfrom your management network for SSH, and80/tcpfor the BloodHound UI and API from wherever your analysts browse.
Step 1 - Launch the AMI
Subscribe to the listing in AWS Marketplace, then launch an instance from the AMI into your VPC with the security group described above. Choose the m5.large instance type and attach your EC2 key pair.
The image keeps the pre-pulled container images on the dedicated data volume. The BloodHound stack therefore comes up within a few minutes of first boot, with no image re-pull while it rotates its secrets and initialises a fresh database.
Step 2 - Connect to your instance
Connect over SSH as the default login user for the operating system variant you launched. The login user differs per OS, so use the entry for your variant:
- Ubuntu 24.04: SSH login user ubuntu. Example: ssh -i your-key.pem ubuntu@<instance-public-ip>
ssh -i your-key.pem ubuntu@<public-ip>Step 3 - Read your unique credentials
On first boot the instance generates a unique administrator password, seeds a fresh administrator on a clean database, and arms forced rotation so the password is single-use. It writes the credentials, along with the resolved web URL, to a root-only file.
View the URL and username. The password line is kept off screen here:
sudo grep -E '^(bloodhound\.url|bloodhound\.admin\.user)=' /root/bloodhound-credentials.txtbloodhound.url=http://<instance-public-ip>/ui/login bloodhound.admin.user=adminThe same file also contains the line BLOODHOUND_ADMIN_PASSWORD=<unique-per-instance-password>. Read the whole file with sudo cat /root/bloodhound-credentials.txt when you are ready to sign in. There is no default or shared login anywhere in the image. Every secret is unique to this instance, and you must change the administrator password at your first sign-in.
Step 4 - Verify the security model from the command line
Confirm the three systemd units are active:
sudo systemctl is-active docker bloodhound bloodhound-firstbootactive active activeConfirm that only SSH and the BloodHound UI are bound on the host. PostgreSQL (5432) and the Neo4j ports (7474 / 7687) never appear, because PostgreSQL is reachable only on the private container network and no Neo4j is shipped at all:
sudo ss -tlnH | awk '{print $4}' | grep -Eo ':(80|5432|7474|7687)$' | sort -u || true:80Confirm that BloodHound answers its unauthenticated health probe and serves the sign-in page, while the API rejects unauthenticated calls:
curl -s -o /dev/null -w 'health %{http_code}\n' http://127.0.0.1/health
curl -s -o /dev/null -w 'sign-in page %{http_code}\n' http://127.0.0.1/ui/login
curl -s -o /dev/null -w 'unauth API %{http_code}\n' http://127.0.0.1/api/v2/available-domainshealth 200 sign-in page 200 unauth API 401Confirm that the well-known upstream default credentials are rejected, and that the unique per-instance administrator password authenticates. The password is read into a shell variable and never printed:
curl -s -o /dev/null -w 'admin / admin HTTP %{http_code}\n' -X POST http://127.0.0.1/api/v2/login -H 'Content-Type: application/json' -d '{"login_method":"secret","username":"admin","secret":"admin"}'
curl -s -o /dev/null -w 'admin / bloodhoundcommunityedition HTTP %{http_code}\n' -X POST http://127.0.0.1/api/v2/login -H 'Content-Type: application/json' -d '{"login_method":"secret","username":"admin","secret":"bloodhoundcommunityedition"}'
PASS=$(sudo grep '^BLOODHOUND_ADMIN_PASSWORD=' /root/bloodhound-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'admin / per-instance password HTTP %{http_code}\n' -X POST http://127.0.0.1/api/v2/login -H 'Content-Type: application/json' --data-binary "$(python3 -c 'import json,sys;print(json.dumps({"login_method":"secret","username":"admin","secret":sys.argv[1]}))' "$PASS")"admin / admin HTTP 401 admin / bloodhoundcommunityedition HTTP 401 admin / per-instance password HTTP 200Finally, confirm that no Neo4j image is present in the appliance. The graph runs entirely on PostgreSQL:
sudo docker ps -a --format '{{.Image}}' | grep -ci neo4j || echo 00Step 5 - Sign in to the web UI
Open the BloodHound UI in a browser at http://<instance-public-ip>/ui/login and sign in as admin with the password from /root/bloodhound-credentials.txt.
The initial password is issued already expired, so BloodHound requires you to choose a new password at this first sign-in. Keep the new password safe.
Step 6 - Explore attack paths
Once you sign in, you land on the Explore view. Open the Cypher tab and its saved-query library to run BloodHound's prebuilt queries and pathfinding, or write your own Cypher against the graph:
Running a query such as All Domain Admins or Shortest paths to Domain Admins renders the attack-path graph, which shows every principal and the relationships that lead to a high-value target.
The image below shows the bundled SharpHound sample dataset, with Active Directory users mapped by their MemberOf relationships into the Domain Admins groups across three trusted domains:
Step 7 - Review the ingested data
Open the Administration area's Data Quality view. It summarises the objects collected in each environment (users, groups, computers, OUs and GPOs), so you can confirm that a collection landed correctly before you start hunting:
Step 8 - Ingest your own collector data
A genuine SharpHound sample dataset is staged read-only on the instance so you can explore immediately. You can re-upload it, or your own collector output, at any time from the UI's Quick Upload or File Ingest:
ls -1 /opt/bloodhound/sample-data/ad_sampledata.zipDownload the SharpHound and AzureHound collectors from the official SpecterOps releases (github.com/SpecterOps/SharpHound and github.com/SpecterOps/AzureHound). Run SharpHound against your own Active Directory, or AzureHound against your Entra ID tenant. Then upload the resulting JSON or ZIP through the BloodHound UI.
BloodHound ingests the data and runs its analysis pass, and the new attack paths become queryable in Explore. The collector binaries are not bundled in this image. They are offensive-security tools that you download and run against environments you are authorised to assess.
Step 9 - Confirm the dedicated data volume
The Docker data root and the whole security database live on a dedicated EBS volume mounted at /var/lib/bloodhound. They survive OS-disk changes and can be grown independently:
findmnt -no SOURCE,TARGET,FSTYPE /var/lib/bloodhound/dev/nvme1n1 /var/lib/bloodhound ext4To grow the volume, expand the EBS volume in the AWS console. Then run sudo resize2fs /dev/nvme1n1, using the device shown by the command above.
Step 10 - Point the instance at your own domain and TLS
BloodHound is served over plain HTTP on port 80 out of the box. For production, put the instance behind an Application Load Balancer, or behind a reverse proxy such as nginx or Caddy that terminates TLS with your own certificate and domain. Restrict the security group so that only your load balancer reaches port 80.
The BloodHound UI and API are origin-agnostic, so no application reconfiguration is needed when you place a proxy in front.
Support
SecureAnyCloud provides 24/7 technical support for this product by email and live chat at sales@secureanycloud.com. Support covers:
- Deployment
- Re-pointing the instance at your own domain or DNS name
- Reverse-proxy TLS termination
- Ingesting SharpHound and AzureHound collector data
- Backing up the security database with EBS snapshots or AWS Backup
Deploy on AWS
Launch BloodHound CE with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com