πΊοΈ Blueprints
A blueprint is a range config bundled with its dependencies: pinned role versions, copies of any local roles, and template build configs.
# Save your range config as a blueprint
ludus blueprint create --id ad-lab --name "AD Lab"
# Share it with your team
ludus blueprint share group ad-lab sec-team
# A teammate creates a new range from it
ludus range create -r ad-lab -n "AD Lab" --from-blueprint ad-lab
# Deploy the range
ludus range deploy
Create blueprints from your own ranges with ludus blueprint create --from-range, or register them in bulk from a source: a git repo or tarball that ships blueprints, roles, and templates together.
Creating a Blueprintβ
Blueprint IDsβ
Every blueprint has a unique identifier and must follow these rules:
- Start with a letter (A-Z or a-z)
- Followed by any number of: alphanumeric,
_, or-characters - Optionally up to 2
/-separated segments, each containing alphanumeric,_, or-
Examplesβ
- `my-blueprint`
- `team/windows`
- `org/team/prod-lab`
From Scratchβ
By default, blueprint create seeds the new blueprint with the same example range config that range create uses (a small AD lab) so you start with a working layout to edit:
ludus blueprint create --id <blueprintID>
ludus blueprint config edit <blueprintID>
Override the seed with your own YAML in one shot:
ludus blueprint create --id <blueprintID> --config ./range-config.yml
The metadata flags (--name, --description, --version, --tag, --min-ludus-version) are accepted inline β no follow-up update needed.
From a Rangeβ
ludus blueprint create --id <blueprintID> --from-range <rangeID>
Copying an Existing Blueprintβ
ludus blueprint create --from-blueprint <sourceBlueprintID>
The copy is owned by you and is shared with no one. --id is optional and if it is omitted, the copy's blueprint ID will be {source}-copy, then {source}-copy-2, etc.
Listing Blueprintsβ
ludus blueprint list
+--------------------+------------------+---------+--------+--------------+---------------+------------------+
| BLUEPRINT ID | NAME | OWNER | ACCESS | SHARED USERS | SHARED GROUPS | UPDATED |
+--------------------+------------------+---------+--------+--------------+---------------+------------------+
| ad-lab | AD Lab | JD-1 | owner | 2 | 1 | 2026-02-10 09:30 |
| team/windows | Windows Range | JD-1 | owner | 0 | 0 | 2026-02-12 14:15 |
| org/sec/malware | Malware Lab | admin | group | 3 | 2 | 2026-02-15 11:00 |
+--------------------+------------------+---------+--------+--------------+---------------+------------------+
The ACCESS column shows your relationship to each blueprint: admin, owner, direct (shared with you), group (shared via a group), or source (inherited from a shared source).
Viewing a Blueprint Configβ
ludus blueprint config get <blueprintID>
This will print the YAML config to stdout.
Applying a Blueprintβ
To create a new range from a blueprint, pass --from-blueprint to range create:
ludus range create -r ad-lab -n "AD Lab" --from-blueprint ad-lab
ludus range deploy --range-id ad-lab
If the apply step fails after the range is created, the range still exists; the error includes the retry command.
To apply a blueprint to a range that already exists:
# Apply to your default range
ludus blueprint apply ad-lab
# Apply to a specific range
ludus blueprint apply ad-lab --target-range JD-1-range-2
Applying a blueprint overwrites the target range configuration, and does not redeploy the range for you. Please run ludus range deploy --range-id <your range> to deploy the applied configuration.
If testing mode is enabled on the target range, apply will fail. Use --force to override it.
Sharing Blueprintsβ
Share with Usersβ
ludus blueprint share user <blueprintID> <userID...>
# Single user
ludus blueprint share user ad-lab JD-1
# Multiple users (space-separated or comma-separated)
ludus blueprint share user ad-lab JD-1 AS-2 BW-3
ludus blueprint share user ad-lab JD-1,AS-2,BW-3
Share with Groupsβ
ludus blueprint share group <blueprintID> <groupName...>
# Single group
ludus blueprint share group ad-lab sec-team
# Multiple groups
ludus blueprint share group ad-lab sec-team,dev-team
Unshareβ
ludus blueprint unshare user <blueprintID> <userID...>
ludus blueprint unshare group <blueprintID> <groupName...>
View Accessβ
ludus blueprint access users <blueprintID>
+---------+---------------+---------------+-----------+
| USERID | NAME | ACCESS | GROUPS |
+---------+---------------+---------------+-----------+
| JD-1 | John Doe | owner | - |
| AS-2 | Alice Smith | direct, group | sec-team |
| BW-3 | Bob Williams | group | sec-team |
+---------+---------------+---------------+-----------+
ludus blueprint access groups <blueprintID>
+-----------+----------+---------+
| GROUP | MANAGERS | MEMBERS |
+-----------+----------+---------+
| sec-team | AS-2 | BW-3 |
+-----------+----------+---------+
Deleting a Blueprintβ
ludus blueprint rm <blueprintID>
Only the owner or an admin can delete a blueprint. You will be prompted for confirmation, unless you use --no-prompt to skip it.
Permissionsβ
You may only see blueprints you have access to. Admins have access to all blueprints. When a blueprint is shared with a group, all members and managers of that group gain access to the blueprint.
| Action | Admin | Owner | Group Manager | Shared User | Group Member |
|---|---|---|---|---|---|
| View / list / apply / copy | β | β | β | β | β |
| Edit config | β | β | β | β | β |
| Share / unshare | β | β | β * | β | β |
| Delete | β | β | β | β | β |
* Group managers can only share with groups they manage, or with users in their groups.
CLI Referenceβ
| Command | Description |
|---|---|
ludus blueprint list | List all accessible blueprints; --tag <tag> filters |
ludus blueprint create | Create from scratch, a range, an existing blueprint, or an exported tarball |
ludus blueprint info <id> | Show metadata and dependency status |
ludus blueprint apply <id> | Apply a blueprint to a range |
ludus blueprint install <id> | (Re-)install a blueprint's role dependencies |
ludus blueprint update <id> | Update name, description, version, tags, etc. |
ludus blueprint config get <id> | Print the YAML config |
ludus blueprint config edit <id> | Edit the YAML config (built-in TUI or $LUDUS_EDITOR) |
ludus blueprint config set <id> -f <file> | Replace the YAML config from a file |
ludus blueprint export <id> | Export the bundle as a .tar.gz |
ludus blueprint access users <id> | List users with access |
ludus blueprint access groups <id> | List groups with access |
ludus blueprint share user <id> <userID...> | Share with users |
ludus blueprint share group <id> <groupName...> | Share with groups |
ludus blueprint unshare user <id> <userID...> | Unshare from users |
ludus blueprint unshare group <id> <groupName...> | Unshare from groups |
ludus blueprint rm <id> | Delete a blueprint |
Directory Structureβ
Each blueprint is stored on disk as a small directory holding its range config and dependency manifest. Applying it always produces the same range.
<ludus_install_path>/blueprints/<record-id>/
βββ blueprint.yml # display metadata (imported blueprints only)
βββ range-config.yml # the range config
βββ requirements.yml # galaxy roles and collections, and license-gated roles
βββ thumbnail.png # optional display thumbnail
blueprint.yml holds display metadata. Locally created blueprints keep this in the database; the file appears in exports and is hand-written when authoring a blueprint for a source.
manifest_version: 1
id: my-lab
name: "My Lab"
description: "Short tagline" # shown in the catalog and picker
version: 1.0.0
tags: [ad, workshop]
min_ludus_version: 2.1.2
config: range-config.yml
range-config.yml is a standard Ludus range config β the same format ludus range config get returns. Ludus reads it to surface which templates and roles the blueprint references on the blueprint detail page.
requirements.yml declares the galaxy roles and collections the blueprint depends on, so any instance it lands on can install them. When Ludus creates a blueprint from a range config, it writes this file for you β pinning each galaxy role at its installed version and recording subscription roles by name. When editing by hand, every role referenced under roles: in range-config.yml must be declared here.
roles:
- name: geerlingguy.docker
version: 7.4.4 # pin a galaxy role
- name: badsectorlabs.ludus_adcs # off-galaxy: name + src
src: https://github.com/badsectorlabs/ludus_adcs
version: v1.2.0
collections: # required for any 3-part
- name: community.crypto # FQCN role like
version: 2.16.0 # community.crypto.openssl_certificate
- name: my.collection # off-galaxy collection
source: https://github.com/foo/my-collection.git # NOTE: collections use
type: git # `source:` not `src:`
version: main
subscription_roles: # license-gated roles served
- ludus_ghosts_client # from the Ludus catalog
- name: ludus_adcs
Export and importβ
Export a blueprint and move it to another instance:
ludus blueprint export my-lab -o my-lab.tar.gz
Import it elsewhere:
ludus blueprint create --import my-lab.tar.gz
ludus blueprint apply my-lab
ludus range deploy
Blueprint export is a config snapshot, not a full installable artifact. The tarball carries blueprint.yml (display metadata), range-config.yml, requirements.yml, and the blueprint's thumbnail if one is set. Galaxy role and collection pins in requirements.yml are re-resolved on the importer's instance via ansible-galaxy. Custom local roles and Packer templates do not travel with a single-blueprint export β if you need to distribute those alongside a blueprint, package them as a source instead.
Subscription rolesβ
Subscription role bytes are never carried; only the names declared under subscription_roles: in requirements.yml. At deploy time, an instance without a valid license (or whose catalog doesn't cover one of the names) will fail when ansible-galaxy tries to install the role. See the Private Role Catalog for the list of subscription roles.
Recovering from a failed installβ
Dependencies install automatically when a blueprint is created, imported, or its source is synced. (Copies inherit the source bundle's installed deps.) If an install fails partway, retry with:
ludus blueprint install <id>