Discovery examples
Every snippet is a complete config file: try it with bifrost config check <file>. For DNS, see
DNS records.
Static machines
Section titled “Static machines”A machine with one mount (the short form), and a machine with two mounts, one of them read-only through rclone. Both listen for SSH on port 2222:
[[machines]]name = "nas"host = "192.168.1.20"port = 2222user = "admin"remote = "/volume1/share"read_only = true
[[machines]]name = "build"host = "build.lan"port = 2222user = "ci"tags = ["ci"]
[[machines.mounts]]remote = "~"local = "build"
[[machines.mounts]]remote = "/srv/artifacts"local = "build-artifacts"driver = "rclone"read_only = true$ bifrost config checkok: /home/sami/.config/bifrost/config.toml (2 machines, 0 providers, 3 mounts, root /home/sami/machines)That is ~/machines/nas, ~/machines/build and ~/machines/build-artifacts. A machine’s port and user apply
to all its mounts. Static machines are allowed unless a [policy.deny] rule matches.
Tailscale
Section titled “Tailscale”Tags, excluded names and a mount template
Section titled “Tags, excluded names and a mount template”[[discovery]]type = "tailscale"
[discovery.filter]include_tags = ["dev", "agent"] # ACL tags tag:dev and tag:agentexclude_names = ["*-old", "laptop-*"]
[discovery.mount]user = "sami"remote = "/srv/work"read_only = true| Peer | Tags | Verdict |
|---|---|---|
devbox |
tag:dev |
allowed (tailscale.filter.include) |
agent-03 |
tag:agent |
allowed (tailscale.filter.include) |
devbox-old |
tag:dev |
denied (tailscale.filter.exclude names=*-old) |
laptop-sami |
tag:dev |
denied (tailscale.filter.exclude names=laptop-*) |
gpu-box |
none | discover-only |
The allowed peers mount read-only, as sami, from /srv/work.
Tag your machines
Section titled “Tag your machines”Tags are defined in your tailnet policy file. Let admins apply them:
"tagOwners": { "tag:dev": ["autogroup:admin"], "tag:agent": ["autogroup:admin"],},Then tag each machine in the admin console (Machines → ⋯ → Edit ACL tags), or when it joins with
sudo tailscale up --advertise-tags=tag:dev. Bifröst only reads tailscale status; it never changes tags.
Exactly one machine, by node ID
Section titled “Exactly one machine, by node ID”A node ID stays the same when the machine is renamed. Look it up:
$ tailscale status --json | jq -r '.Peer[] | select(.HostName == "gpu-box") | .ID'nQ2mX7Yr4R11CNTRL[[discovery]]type = "tailscale"
[discovery.filter]include_ids = ["nQ2mX7Yr4R11CNTRL"]Only gpu-box is allowed; every other peer is discover-only.
HTTP inventory
Section titled “HTTP inventory”A file served on your machine
Section titled “A file served on your machine”-
Write the document:
~/inventory/machines.json {"machines": [{ "name": "web-01", "host": "10.20.0.4", "user": "deploy", "metadata": { "tags": ["dev"], "env": "dev" } },{ "name": "web-02", "addresses": ["10.20.0.5"], "port": 2222, "metadata": { "tags": ["dev"] } }]} -
Serve it on the loopback address only:
Terminal window python3 -m http.server 8765 --bind 127.0.0.1 --directory ~/inventory -
Point Bifröst at it. Plain
http://is accepted only for127.0.0.1,[::1]andlocalhost:[[discovery]]type = "http"name = "inventory"url = "http://127.0.0.1:8765/machines.json"[discovery.filter]include_tags = ["dev"]
Both machines are tagged dev, so both are allowed. web-01 connects to 10.20.0.4. web-02 has no host, so
it uses its first address, on port 2222. user is a hint, used only with honor_hints = true. Plain http:// to
any other address fails the check:
$ bifrost config checkerror: discovery[0].url: must be https://, or http:// only to 127.0.0.1, [::1] or localhostAn authenticated endpoint
Section titled “An authenticated endpoint”[[discovery]]type = "http"name = "inventory"url = "https://inventory.example.com/bifrost/v1/machines"headers = { Authorization = "Bearer ${BIFROST_INVENTORY_TOKEN}" }interval = "60s"The token comes from the environment, for bifrost config check as for the daemon. An unset variable is an error,
never an empty header:
$ bifrost config checkerror: discovery[0].headers."Authorization": undefined variable $BIFROST_INVENTORY_TOKENUnder systemd, keep the token in a file only you can read, and load it into the unit:
[Service]EnvironmentFile=%h/.config/bifrost/inventory.env(umask 077; printf 'BIFROST_INVENTORY_TOKEN=%s\n' "$TOKEN" > ~/.config/bifrost/inventory.env)systemctl --user daemon-reload && systemctl --user restart bifrostGenerate the document from a list
Section titled “Generate the document from a list”agent-01 10.20.0.4 dev,agentagent-02 10.20.0.5 devbuild-01 10.20.0.18 cijq -Rn '{machines: [inputs | select(length > 0) | split(" ") | {name: .[0], host: .[1], metadata: {tags: (.[2] | split(","))}}]}' hosts.txt > ~/inventory/machines.jsonEach line becomes {"name": "agent-01", "host": "10.20.0.4", "metadata": {"tags": ["dev", "agent"]}}. Rerun it
whenever the list changes; the next refresh picks up new and changed lines, and a removed machine is dropped
after 3 × interval.
All together
Section titled “All together”Static, Tailscale and DNS in one file, with a global deny:
[policy.deny]names = ["*-old"]tags = ["prod"]
[[machines]]name = "build"host = "10.0.0.18"user = "ci"remote = "/srv/build"
[[discovery]]type = "tailscale"
[discovery.filter]include_tags = ["dev"]exclude_names = ["agent-02"] # handed to DNS
[discovery.mount]user = "sami"
[[discovery]]type = "dns"name = "infra"domain = "infra.example.com"
[discovery.filter]include_names = ["agent-*", "db-*"]
[discovery.mount]user = "ubuntu"With these DNS records:
_bifrost IN TXT "v=bf1 node=agent-01 host=10.0.0.11"_bifrost IN TXT "v=bf1 node=agent-02 host=10.0.0.12"_bifrost IN TXT "v=bf1 node=db-01 host=10.0.0.30 tags=prod"| Machine | Reported by | Verdict | Why |
|---|---|---|---|
build |
static | allowed (static) |
static machines are allowed unless denied |
devbox |
Tailscale, tag:dev |
allowed (tailscale.filter.include) |
|
devbox-old |
Tailscale, tag:dev |
denied (policy.deny names=*-old) |
a global deny beats every allow |
laptop |
Tailscale, no tags | discover-only |
no rule allows it |
agent-01 |
Tailscale, no tags; infra |
discover-only |
Tailscale is more trusted, so its report alone decides. infra is listed as shadowed |
agent-02 |
Tailscale, tag:dev; infra |
allowed (infra.filter.include) |
the exclude drops Tailscale’s report (still listed as shadowed), so DNS decides and supplies 10.0.0.12 |
db-01 |
infra, tags=prod |
denied (policy.deny tags=prod) |
See Policy and filtering for the rules behind each verdict.

