diff --git a/.forgejo/workflows/lint-governance.yml b/.forgejo/workflows/lint-governance.yml new file mode 100644 index 0000000..490702e --- /dev/null +++ b/.forgejo/workflows/lint-governance.yml @@ -0,0 +1,27 @@ +name: Lint Governance + +on: + push: + branches: + - main + pull_request: + branches: + - "**" + workflow_dispatch: + +jobs: + governance: + name: BEP Metadata + runs-on: [self-hosted, linux, x86_64, burrow-forge] + steps: + - name: Checkout + uses: https://code.forgejo.org/actions/checkout@v4 + with: + token: ${{ github.token }} + fetch-depth: 0 + + - name: Validate BEP metadata + shell: bash + run: | + set -euo pipefail + python3 Scripts/check-bep-metadata.py diff --git a/.github/workflows/lint-governance.yml b/.github/workflows/lint-governance.yml new file mode 100644 index 0000000..08b665c --- /dev/null +++ b/.github/workflows/lint-governance.yml @@ -0,0 +1,23 @@ +name: Governance Lint + +on: + pull_request: + branches: + - "*" + +jobs: + governance: + name: BEP Metadata + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Validate BEP metadata + shell: bash + run: | + set -euo pipefail + python3 Scripts/check-bep-metadata.py diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0ca7ced --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,14 @@ +# instructions for agents + +1. Spell the project name as `Burrow` in user-facing copy and `burrow` in code, package, and protocol identifiers unless an existing integration requires a different literal. +2. Read [CONSTITUTION.md](CONSTITUTION.md) before changing Apple clients, the daemon, the control plane, forge infrastructure, identity, or security-sensitive code. +3. Anchor non-trivial changes in a Burrow Evolution Proposal (BEP) under [evolution/](evolution/README.md) so future contributors can inherit the rationale, safeguards, and rollout shape. +4. Before touching the Apple app, daemon IPC, or Tailnet flows, review: + - [evolution/proposals/BEP-0002-control-plane-bootstrap-and-local-auth.md](evolution/proposals/BEP-0002-control-plane-bootstrap-and-local-auth.md) + - [evolution/proposals/BEP-0003-connect-ip-and-negotiation-roadmap.md](evolution/proposals/BEP-0003-connect-ip-and-negotiation-roadmap.md) + - [evolution/proposals/BEP-0005-daemon-ipc-and-apple-boundary.md](evolution/proposals/BEP-0005-daemon-ipc-and-apple-boundary.md) + - [evolution/proposals/BEP-0006-tailnet-authority-first-control-plane.md](evolution/proposals/BEP-0006-tailnet-authority-first-control-plane.md) +5. Apple clients must talk only to the daemon over gRPC. Do not add direct HTTP, control-plane, or helper-process calls from Swift UI code. +6. Treat Tailnet as one protocol family. Tailscale-managed and self-hosted Headscale-style deployments differ by authority, policy, and auth details, not by a separate user-facing protocol surface. +7. Maintain canonical identity and operator metadata in [contributors.nix](contributors.nix). If Burrow forge, Authentik, Headscale, or admin/group mappings need to change, edit that registry first and derive runtime configuration from it. +8. When process or architecture is unclear, stop and draft or update a BEP instead of improvising durable behavior in code. diff --git a/Makefile b/Makefile index f927f5f..1a0488c 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,12 @@ check: build: @cargo build +bep-check: + @python3 Scripts/check-bep-metadata.py + +bep-list: + @Scripts/bep list + daemon-console: @$(sudo_cargo_console) daemon diff --git a/README.md b/README.md index b8684c3..ba4f50c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Routine verification now runs unprivileged with `cargo test --workspace --all-fe The repository now carries its own design and deployment record: - [Constitution](./CONSTITUTION.md) +- [Agent Instructions](./AGENTS.md) - [Burrow Evolution](./evolution/README.md) - [WireGuard Rust Lineage](./docs/WIREGUARD_LINEAGE.md) - [Protocol Roadmap](./docs/PROTOCOL_ROADMAP.md) @@ -19,6 +20,8 @@ The repository now carries its own design and deployment record: Burrow is fully open source, you can fork the repo and start contributing easily. For more information and in-depth discussions, visit the `#burrow` channel on the [Hack Club Slack](https://hackclub.com/slack/), here you can ask for help and talk with other people interested in burrow. Checkout [GETTING_STARTED.md](./docs/GETTING_STARTED.md) for build instructions and [GTK_APP.md](./docs/GTK_APP.md) for the Linux app. Forge and deployment scaffolding live in [`flake.nix`](./flake.nix), [`nixos/`](./nixos), and [`.forgejo/workflows/`](./.forgejo/workflows/). Hosted mail backup operations live in [`docs/FORWARDEMAIL.md`](./docs/FORWARDEMAIL.md) and [`Tools/forwardemail-custom-s3.sh`](./Tools/forwardemail-custom-s3.sh). +Agent and governance-sensitive work should start with [AGENTS.md](./AGENTS.md), [CONSTITUTION.md](./CONSTITUTION.md), and the relevant BEPs under [`evolution/proposals/`](./evolution/proposals/). Identity and bootstrap metadata now live in [`contributors.nix`](./contributors.nix). + The project structure is divided in the following folders: ``` diff --git a/Scripts/bep b/Scripts/bep new file mode 100755 index 0000000..1c6bd64 --- /dev/null +++ b/Scripts/bep @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(git rev-parse --show-toplevel) +proposals_dir="$repo_root/evolution/proposals" + +auto_browse() { + if command -v wisu >/dev/null 2>&1; then + exec wisu -i -g --icons "$repo_root/evolution" + fi + exec ls -la "$repo_root/evolution" +} + +usage() { + cat <<'USAGE' +Usage: bep [command] + +Commands: + list [--status ] List BEPs, optionally filtered by status. + open Open a BEP in $EDITOR. + help Show this help. + +If no command is provided, bep launches a simple browser for evolution/. +USAGE +} + +normalize_id() { + local raw="$1" + if [[ "$raw" =~ ^BEP-[0-9]+$ ]]; then + printf '%s' "$raw" + return 0 + fi + if [[ "$raw" =~ ^[0-9]+$ ]]; then + printf 'BEP-%04d' "$raw" + return 0 + fi + return 1 +} + +read_status() { + local file="$1" + awk -F ': ' '/^Status:/ {print $2; exit}' "$file" +} + +read_title() { + local file="$1" + local line + line=$(head -n 1 "$file" || true) + printf '%s' "$line" | sed -E 's/^# `[^`]+`[[:space:]]+//; s/^[^A-Za-z0-9]+//' +} + +list_bep() { + local filter="${1:-}" + local filter_lower="" + if [[ -n "$filter" ]]; then + filter_lower=$(printf '%s' "$filter" | tr '[:upper:]' '[:lower:]') + fi + + printf '%-10s %-18s %s\n' "BEP" "Status" "Title" + local file + local entries=() + for file in "$proposals_dir"/BEP-*.md; do + [[ -e "$file" ]] || continue + local base + base=$(basename "$file") + local id + id=$(printf '%s' "$base" | cut -d- -f1-2) + local status + status=$(read_status "$file") + local status_lower + status_lower=$(printf '%s' "$status" | tr '[:upper:]' '[:lower:]') + if [[ -n "$filter_lower" && "$status_lower" != "$filter_lower" ]]; then + continue + fi + local title + title=$(read_title "$file") + entries+=("$(printf '%-10s %-18s %s' "$id" "$status" "$title")") + done + if [[ ${#entries[@]} -gt 0 ]]; then + printf '%s\n' "${entries[@]}" | sort + fi +} + +open_bep() { + local raw="$1" + local id + if ! id=$(normalize_id "$raw"); then + echo "Unknown BEP id: $raw" >&2 + exit 1 + fi + local matches + matches=("$proposals_dir"/"$id"-*.md) + if [[ ${#matches[@]} -eq 0 || ! -e "${matches[0]}" ]]; then + echo "No proposal found for $id" >&2 + exit 1 + fi + if [[ ${#matches[@]} -gt 1 ]]; then + echo "Multiple proposals match $id:" >&2 + printf ' %s\n' "${matches[@]}" >&2 + exit 1 + fi + local editor="${EDITOR:-vi}" + exec "$editor" "${matches[0]}" +} + +command=${1:-} +case "$command" in + "") + auto_browse + ;; + list) + if [[ ${2:-} == "--status" && -n ${3:-} ]]; then + list_bep "$3" + else + list_bep + fi + ;; + open) + if [[ -z ${2:-} ]]; then + echo "bep open requires an id" >&2 + exit 1 + fi + open_bep "$2" + ;; + help|-h|--help) + usage + ;; + *) + echo "Unknown command: $command" >&2 + usage + exit 1 + ;; +esac diff --git a/Scripts/check-bep-metadata.py b/Scripts/check-bep-metadata.py new file mode 100755 index 0000000..d054934 --- /dev/null +++ b/Scripts/check-bep-metadata.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import pathlib +import re +import sys + + +REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent +PROPOSALS_DIR = REPO_ROOT / "evolution" / "proposals" +ALLOWED_STATUSES = { + "Pitch", + "Draft", + "In Review", + "Accepted", + "Implemented", + "Rejected", + "Returned for Revision", + "Superseded", + "Archived", +} +REQUIRED_FIELDS = [ + "Status", + "Proposal", + "Authors", + "Coordinator", + "Reviewers", + "Constitution Sections", + "Implementation PRs", + "Decision Date", +] + + +def text_block_lines(path: pathlib.Path) -> list[str]: + content = path.read_text(encoding="utf-8") + match = re.search(r"```text\n(.*?)\n```", content, re.DOTALL) + if not match: + raise ValueError("missing leading ```text metadata block") + return [line.rstrip() for line in match.group(1).splitlines() if line.strip()] + + +def validate(path: pathlib.Path) -> list[str]: + errors: list[str] = [] + proposal_id = path.name.split("-", 2)[:2] + expected_id = "-".join(proposal_id).removesuffix(".md") + + try: + lines = text_block_lines(path) + except ValueError as exc: + return [f"{path}: {exc}"] + + field_names = [line.split(":", 1)[0] for line in lines] + if field_names != REQUIRED_FIELDS: + errors.append( + f"{path}: metadata fields must appear in order {', '.join(REQUIRED_FIELDS)}" + ) + return errors + + fields = dict(line.split(":", 1) for line in lines) + fields = {key.strip(): value.strip() for key, value in fields.items()} + + if fields["Status"] not in ALLOWED_STATUSES: + errors.append(f"{path}: invalid Status {fields['Status']!r}") + + if fields["Proposal"] != expected_id: + errors.append( + f"{path}: Proposal field {fields['Proposal']!r} does not match filename id {expected_id!r}" + ) + + if fields["Status"] in {"Accepted", "Implemented", "Superseded", "Rejected", "Archived"} and fields["Decision Date"] == "Pending": + errors.append( + f"{path}: Decision Date must not be Pending once status is {fields['Status']}" + ) + + return errors + + +def main() -> int: + errors: list[str] = [] + for path in sorted(PROPOSALS_DIR.glob("BEP-*.md")): + errors.extend(validate(path)) + + if errors: + for error in errors: + print(error, file=sys.stderr) + return 1 + + print(f"checked {len(list(PROPOSALS_DIR.glob('BEP-*.md')))} BEPs") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/contributors.nix b/contributors.nix new file mode 100644 index 0000000..f6cc014 --- /dev/null +++ b/contributors.nix @@ -0,0 +1,47 @@ +{ + groups = { + users = "burrow-users"; + admins = "burrow-admins"; + }; + + identities = { + contact = { + displayName = "Burrow"; + canonicalEmail = "contact@burrow.net"; + sourceEmail = "net.burrow@gmail.com"; + isAdmin = true; + forgeAuthorized = true; + bootstrapAuthentik = true; + sshPublicKeyPath = ./nixos/keys/contact_at_burrow_net.pub; + roles = [ + "operator" + "forge-admin" + ]; + }; + + conrad = { + displayName = "Conrad Kramer"; + canonicalEmail = "conrad@burrow.net"; + sourceEmail = "ckrames1234@gmail.com"; + isAdmin = true; + forgeAuthorized = false; + bootstrapAuthentik = true; + roles = [ + "operator" + "founder" + ]; + }; + + agent = { + displayName = "Burrow Agent"; + canonicalEmail = "agent@burrow.net"; + isAdmin = false; + forgeAuthorized = true; + bootstrapAuthentik = false; + sshPublicKeyPath = ./nixos/keys/agent_at_burrow_net.pub; + roles = [ + "automation" + ]; + }; + }; +} diff --git a/evolution/README.md b/evolution/README.md index e55a347..794b1fe 100644 --- a/evolution/README.md +++ b/evolution/README.md @@ -58,3 +58,17 @@ evolution/ ``` Use ASCII Markdown. Keep metadata at the top of each proposal so tooling and future agents can parse it quickly. + +## BEP Helper + +Use the `bep` helper under `Scripts/` to browse or list proposals: + +- `Scripts/bep` opens a quick browser for `evolution/`. +- `Scripts/bep list --status Draft` lists proposals by status. +- `Scripts/bep open BEP-0005` opens a proposal in `$EDITOR`. + +Validate proposal metadata with: + +```bash +python3 Scripts/check-bep-metadata.py +``` diff --git a/evolution/proposals/BEP-0005-daemon-ipc-and-apple-boundary.md b/evolution/proposals/BEP-0005-daemon-ipc-and-apple-boundary.md new file mode 100644 index 0000000..1227444 --- /dev/null +++ b/evolution/proposals/BEP-0005-daemon-ipc-and-apple-boundary.md @@ -0,0 +1,78 @@ +# `BEP-0005` - Daemon IPC and Apple Boundary + +```text +Status: Draft +Proposal: BEP-0005 +Authors: gpt-5.4 +Coordinator: gpt-5.4 +Reviewers: Pending +Constitution Sections: II, III, IV, V +Implementation PRs: Pending +Decision Date: Pending +``` + +## Summary + +Burrow should formalize one Apple/runtime boundary: Apple clients speak only to the daemon over gRPC on the app-group Unix socket, and the daemon owns all external control-plane, helper-process, and runtime coordination work. This prevents UI code from accreting side HTTP paths or ad hoc control-plane integrations that bypass the system Burrow is supposed to own. + +## Motivation + +- The current Tailnet work already showed the failure mode: Swift UI code started reaching around the daemon boundary to talk to helper HTTP endpoints directly. +- Apple-specific process ownership is easy to blur between the app, the network extension, and helper daemons unless the contract is explicit. +- If Burrow wants a durable multi-runtime architecture, the daemon must remain the only orchestration boundary between clients and control/data-plane behavior. + +## Detailed Design + +- Apple UI and Apple support libraries may call only daemon gRPC methods over the declared Burrow Unix socket. +- Direct Swift calls to external control-plane HTTP APIs, localhost helper HTTP servers, or runtime-specific subprocesses are forbidden. +- The daemon is responsible for: + - discovery of Tailnet authorities and related metadata + - control-plane session setup and tracking + - login/session lifecycle brokering + - runtime start/stop/reconcile + - translating helper or bridge processes into stable daemon RPCs +- `burrow/src/control/` owns transport-neutral control-plane semantics such as discovery, authority normalization, and request/response shaping. +- Apple UI owns presentation only: + - forms + - local state + - presenting returned auth URLs or statuses + - surfacing daemon availability and errors +- Any new Apple-facing runtime capability requires a daemon RPC first. + +## Security and Operational Considerations + +- Keeping control-plane I/O out of Swift UI reduces accidental secret, token, and callback sprawl across app code. +- The daemon boundary makes testing and kill-switch behavior tractable because runtime integration is localized. +- Apple daemon lifecycle ownership must be explicit: either the app ensures the daemon is running before RPC or the extension owns it and the UI surfaces daemon-unavailable state clearly. + +## Contributor Playbook + +- Before adding a new Apple-side workflow, identify the daemon RPC that should own it. +- If the RPC does not exist, add the protocol shape in `proto/burrow.proto`, implement it in the daemon, and only then wire Swift UI. +- Verify that no Swift UI or support code calls external control-plane HTTP endpoints directly. +- For Tailnet and similar flows, test: + - daemon unavailable behavior + - successful RPC path + - error propagation through the UI + +## Alternatives Considered + +- Let Apple UI call control-plane endpoints directly for convenience. Rejected because it creates parallel orchestration paths and breaks the daemon contract. +- Allow one-off exceptions for login helpers. Rejected because those exceptions become the architecture. + +## Impact on Other Work + +- Governs the Tailnet refactor and future Apple runtime work. +- Interacts with BEP-0002 control-plane bootstrap and BEP-0003 transport refactoring. + +## Decision + +Pending. + +## References + +- `Apple/UI/` +- `Apple/Core/` +- `Apple/NetworkExtension/` +- `burrow/src/daemon/` +- `burrow/src/control/` diff --git a/evolution/proposals/BEP-0006-tailnet-authority-first-control-plane.md b/evolution/proposals/BEP-0006-tailnet-authority-first-control-plane.md new file mode 100644 index 0000000..fea4aba --- /dev/null +++ b/evolution/proposals/BEP-0006-tailnet-authority-first-control-plane.md @@ -0,0 +1,71 @@ +# `BEP-0006` - Tailnet Authority-First Control Plane + +```text +Status: Draft +Proposal: BEP-0006 +Authors: gpt-5.4 +Coordinator: gpt-5.4 +Reviewers: Pending +Constitution Sections: I, II, IV, V +Implementation PRs: Pending +Decision Date: Pending +``` + +## Summary + +Burrow should treat Tailnet as one protocol family. Tailscale-managed and self-hosted Headscale-style deployments differ by authority, policy, and auth details, not by a distinct user-facing protocol. Burrow’s config and UI should therefore be authority-first rather than provider-first. + +## Motivation + +- Splitting Tailscale and Headscale into separate user-facing providers causes fake architectural divergence. +- Discovery already naturally returns an authority and optional issuer; that is the stable contract users actually need. +- Future managed or enterprise deployments should fit the same model without requiring another protocol picker. + +## Detailed Design + +- Tailnet configuration is centered on: + - account + - identity + - authority/login server URL + - optional tailnet name + - optional hostname + - auth method/material +- User-facing surfaces should not force a protocol choice between Tailscale and Headscale. +- Provider inference may remain internal metadata for compatibility and diagnostics: + - default managed Tailscale authority + - custom self-hosted authority + - Burrow-owned authority when explicitly applicable +- Discovery returns authority and related metadata; editing the authority is the mechanism that moves a configuration from managed default to custom control server. +- The daemon and control layer own provider inference; the UI should primarily present “Tailnet” plus the selected authority. + +## Security and Operational Considerations + +- Authority-first config reduces UI complexity and makes misconfiguration easier to reason about. +- Provider-specific assumptions must not leak into packet or control-plane semantics unless the authority actually requires them. +- Auth material must remain authority-scoped and identity-scoped in daemon storage. + +## Contributor Playbook + +- Remove provider pickers from Tailnet UI unless a concrete protocol difference requires one. +- Store the authority explicitly in payloads and infer provider internally only when needed. +- Prefer tests that validate authority normalization and discovery behavior over UI-provider branching. + +## Alternatives Considered + +- Keep separate user-facing providers for Tailscale and Headscale. Rejected because it models deployment shape as protocol shape. +- Collapse all control planes into one opaque Burrow provider. Rejected because the authority still matters operationally and diagnostically. + +## Impact on Other Work + +- Refines BEP-0002’s Tailscale-shaped control-plane work. +- Constrains the Tailnet Apple refactor and future daemon control-plane storage. + +## Decision + +Pending. + +## References + +- `burrow/src/control/` +- `Apple/UI/Networks/` +- `proto/burrow.proto` diff --git a/evolution/proposals/BEP-0007-identity-registry-and-operator-bootstrap.md b/evolution/proposals/BEP-0007-identity-registry-and-operator-bootstrap.md new file mode 100644 index 0000000..1fde0fb --- /dev/null +++ b/evolution/proposals/BEP-0007-identity-registry-and-operator-bootstrap.md @@ -0,0 +1,73 @@ +# `BEP-0007` - Identity Registry and Operator Bootstrap + +```text +Status: Draft +Proposal: BEP-0007 +Authors: gpt-5.4 +Coordinator: gpt-5.4 +Reviewers: Pending +Constitution Sections: II, III, IV, V +Implementation PRs: Pending +Decision Date: Pending +``` + +## Summary + +Burrow should maintain one canonical registry for project identities, aliases, bootstrap users, SSH keys, and admin-group mappings. Forgejo, Authentik, and related bootstrap configuration should derive from that registry instead of hardcoding overlapping identity facts in multiple modules. + +## Motivation + +- Burrow currently hardcodes operator and admin/bootstrap user facts directly in host configuration. +- Multi-account and self-hosted identity are becoming core architecture, not incidental infra details. +- A single registry reduces drift across Forgejo, Authentik, Headscale, SSH authorization, and future control-plane bootstrap. + +## Detailed Design + +- Add a root-level identity registry (`contributors.nix`) as the canonical source of truth for: + - usernames + - display names + - canonical emails + - external source emails or aliases + - admin scope + - bootstrap eligibility + - forge authorized SSH keys + - named roles +- Consume that registry from host configuration for: + - Forgejo authorized keys + - Forgejo bootstrap admin defaults + - Authentik bootstrap users + - Burrow user/admin group names +- Future work may derive contributor docs, OIDC bootstrap, and additional runtime configuration from the same registry. + +## Security and Operational Considerations + +- Identity drift is a security bug when it affects admin groups, bootstrap accounts, or SSH authorization. +- The registry stores metadata only; secrets remain in agenix or other declared secret paths. +- Changes to the registry should receive explicit review because they affect access and governance. + +## Contributor Playbook + +- Edit `contributors.nix` first when changing operator, admin, alias, or bootstrap identity state. +- Derive runtime configuration from the registry instead of duplicating the same facts elsewhere. +- Keep secret references separate from identity metadata. + +## Alternatives Considered + +- Continue hardcoding users in module options. Rejected because drift is inevitable once Forgejo, Authentik, and Headscale all depend on the same identities. +- Create separate per-service user lists. Rejected because it duplicates governance facts and weakens review. + +## Impact on Other Work + +- Supports forge auth, Authentik group sync, and future multi-account Burrow control-plane work. +- Creates the basis for stronger contributor and operator provenance later. + +## Decision + +Pending. + +## References + +- `contributors.nix` +- `nixos/hosts/burrow-forge/default.nix` +- `nixos/modules/burrow-authentik.nix` +- `nixos/modules/burrow-forge.nix` diff --git a/nixos/hosts/burrow-forge/default.nix b/nixos/hosts/burrow-forge/default.nix index d612ea8..fb5b8ae 100644 --- a/nixos/hosts/burrow-forge/default.nix +++ b/nixos/hosts/burrow-forge/default.nix @@ -1,4 +1,23 @@ -{ config, self, ... }: +{ config, lib, self, ... }: + +let + contributors = import ../../../contributors.nix; + identities = contributors.identities; + bootstrapUsers = lib.mapAttrsToList + ( + username: identity: { + inherit username; + name = identity.displayName; + email = identity.canonicalEmail; + sourceEmail = identity.sourceEmail or null; + isAdmin = identity.isAdmin or false; + } + ) + (lib.filterAttrs (_: identity: identity.bootstrapAuthentik or false) identities); + forgeAuthorizedKeys = map + (username: builtins.readFile identities.${username}.sshPublicKeyPath) + (builtins.attrNames (lib.filterAttrs (_: identity: identity.forgeAuthorized or false) identities)); +in { imports = [ @@ -59,12 +78,14 @@ services.burrow.forge = { enable = true; + contactEmail = identities.contact.canonicalEmail; + adminUsername = "contact"; + adminEmail = identities.contact.canonicalEmail; adminPasswordFile = "/var/lib/burrow/intake/forgejo_pass_contact_at_burrow_net.txt"; + oidcAdminGroup = contributors.groups.admins; + oidcRestrictedGroup = contributors.groups.users; oidcClientSecretFile = config.age.secrets.burrowForgejoOidcClientSecret.path; - authorizedKeys = [ - (builtins.readFile ../../keys/contact_at_burrow_net.pub) - (builtins.readFile ../../keys/agent_at_burrow_net.pub) - ]; + authorizedKeys = forgeAuthorizedKeys; }; services.burrow.forgeRunner = { @@ -92,22 +113,9 @@ googleClientIDFile = config.age.secrets.burrowAuthentikGoogleClientId.path; googleClientSecretFile = config.age.secrets.burrowAuthentikGoogleClientSecret.path; googleLoginMode = "redirect"; - bootstrapUsers = [ - { - username = "contact"; - name = "Burrow"; - email = "contact@burrow.net"; - sourceEmail = "net.burrow@gmail.com"; - isAdmin = true; - } - { - username = "conrad"; - name = "Conrad Kramer"; - email = "conrad@burrow.net"; - sourceEmail = "ckrames1234@gmail.com"; - isAdmin = true; - } - ]; + userGroupName = contributors.groups.users; + adminGroupName = contributors.groups.admins; + bootstrapUsers = bootstrapUsers; }; services.burrow.headscale = {