{"id":50860878,"url":"https://github.com/guilledipa/praetor","last_synced_at":"2026-06-14T21:04:34.805Z","repository":{"id":345538935,"uuid":"1186256645","full_name":"guilledipa/praetor","owner":"guilledipa","description":"A Go-based configuration management tool for real-time, mTLS-secured infrastructure orchestration via Message Brokers and gRPC.","archived":false,"fork":false,"pushed_at":"2026-03-20T04:23:57.000Z","size":21030,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-20T06:50:21.453Z","etag":null,"topics":["automation","configuration-management","devops","golang","grpc-go","mtls","nats-jetstream","orchestration","sre","system-administration"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/guilledipa.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-19T12:41:22.000Z","updated_at":"2026-03-20T04:24:01.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/guilledipa/praetor","commit_stats":null,"previous_names":["guilledipa/praetor"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/guilledipa/praetor","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guilledipa%2Fpraetor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guilledipa%2Fpraetor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guilledipa%2Fpraetor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guilledipa%2Fpraetor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/guilledipa","download_url":"https://codeload.github.com/guilledipa/praetor/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guilledipa%2Fpraetor/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34337559,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","configuration-management","devops","golang","grpc-go","mtls","nats-jetstream","orchestration","sre","system-administration"],"created_at":"2026-06-14T21:04:34.047Z","updated_at":"2026-06-14T21:04:34.787Z","avatar_url":"https://github.com/guilledipa.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Praetor - Configuration Management \u0026 Orchestration\n\nPraetor is a lightweight configuration management and orchestration system\ninspired by Puppet and Choria, built in Golang, using NATS JetStream for\nreal-time messaging and gRPC for secure Master-Agent communication.\n\n## Why \"Praetor\"?\n\nIn ancient Rome, a Praetor was a magistrate with significant authority, often\nwith military command and the power to enact laws and judgments. This system\naims to provide similar precise control and command over your infrastructure,\nensuring systems adhere to their intended state and allowing for swift, decisive\nactions.\n\n## Architecture\n\nThe system uses a hybrid model, maintaining a pull-based security posture:\n\n*   **Configuration Plane (NATS Triggered gRPC Pull):** The Master triggers\n    agents to fetch their catalog by sending a message via NATS JetStream to a\n    node-specific subject (e.g., `agent.trigger.getCatalog.agent1`). Upon\n    receiving the trigger, the Agent initiates a gRPC call (secured with mTLS) to\n    the Master to get its catalog. Agents send system facts along with the\n    request. The catalog is compiled by the Master, hydrated with agent facts\n    using Go templates, cryptographically signed, and verified by the Agent.\n*   **Orchestration Plane (Agent-Initiated Pull Subscription to NATS\n    JetStream):** Agents establish a persistent, mTLS secured connection to NATS\n    JetStream and use *pull-based subscriptions* to listen for ad-hoc commands\n    on subjects under `commands.\u003e`. Currently, only `commands.facts.get` is\n    implemented.\n\n```dot\ndigraph PraetorArchitecture {\n  rankdir=LR;\n  stylesheet = \"/frameworks/g3doc/includes/graphviz-style.css\"\n\n  subgraph cluster_user {\n    label = \"User\";\n    User [shape=Mcircle];\n  }\n\n  subgraph cluster_master {\n    label = \"Master Server\";\n    MasterGRPC [label=\"gRPC Server\"];\n    Compiler [label=\"Catalog Compiler / Hydrator\"];\n    MasterNATS [label=\"NATS JetStream Publisher\"];\n    CatalogData [label=\"catalog.yaml\", shape=cylinder];\n    Compiler -\u003e MasterGRPC;\n    CatalogData -\u003e Compiler;\n    MasterNATS -\u003e NATS [label=\" Publish Catalog Trigger\"];\n  }\n\n  subgraph cluster_agent {\n    label = \"Agent Node\";\n    AgentGRPC [label=\"gRPC Client\"];\n    AgentNATS [label=\"NATS JetStream Pull Subscriber\"];\n    FactCollector [label=\"Fact Collector\"];\n    Engine [label=\"State Engine\"];\n    Resources [label=\"Resources\", shape=box3d];\n    FactCollector -\u003e AgentGRPC;\n    AgentGRPC -\u003e Engine;\n    AgentNATS -\u003e Engine;\n    Engine -\u003e Resources;\n    AgentNATS -\u003e NATS [label=\" Sub to Trigger\", dir=back];\n    AgentNATS -\u003e NATS [label=\" Sub to Commands\", dir=back];\n  }\n\n  NATS [label=\"NATS Server (JetStream Enabled)\", shape=Msquare];\n\n  User -\u003e MasterNATS [label=\" Trigger Catalog Fetch / Request Facts\"];\n\n  AgentGRPC -\u003e MasterGRPC [label=\" GetCatalog (mTLS, with Facts)\", dir=back];\n\n  MasterNATS -\u003e NATS [label=\" Publish to commands.facts.get\"];\n}\n```\n\n**Components:**\n\n*   **Master:** Manages configurations, receives agent facts, compiles and signs\n    catalogs, and publishes catalog update triggers (default every 15s) to NATS\n    JetStream.\n*   **Agent:** Runs on managed nodes, collects facts, listens for NATS triggers to\n    fetch catalogs via gRPC, enforces state, and listens for ad-hoc commands (like\n    `commands.facts.get`) via a NATS JetStream pull subscription.\n*   **NATS:** Message broker with JetStream enabled for persistent and reliable\n    real-time command and control, and triggering.\n\n## Setup\n\n1.  **Prerequisites:** Docker, Docker Compose, Go, OpenSSL, NATS CLI, protoc.\n\n2.  **Generate Certificates (Auto-TLS):**\n\n    Praetor now handles PKI out-of-the-box! You do not need to manage certificates manually.\n    When `praetor-master` boots, if it detects a missing `master/certs/ca.crt`, it will **automatically** generate the entire unified CA, NATS server keys, and Master signing credentials cross-platform.\n\n\n3.  **Generate Proto Code:** If you modify `proto/master.proto`, regenerate the\n    Go code:\n\n    ```bash\n    cd proto\n    go mod tidy\n    cd ..\n    protoc --go_out=./proto/gen/master --go-grpc_out=./proto/gen/master proto/master.proto --go_opt=module=github.com/guilledipa/praetor/proto/gen/master --go-grpc_opt=module=github.com/guilledipa/praetor/proto/gen/master\n    cd proto/gen/master\n    go mod tidy\n    cd ../../..\n    ```\n\n4.  **Start NATS Server (with JetStream):**\n\n    ```bash\n    docker compose down -v\n    docker compose build nats\n    docker compose up -d nats\n    ```\n\n    The configuration in `nats/conf/nats-server.conf` and `docker-compose.yml`\n    enables JetStream with persistent storage.\n\n5.  **Start Master:**\n\n    ```bash\n    cd master\n    # Optional: Configure NATS connection and trigger interval\n    # export MASTER_NATS_URL=nats://custom:4222\n    # export MASTER_TRIGGER_INTERVAL=30s\n    go run cmd/master/main.go\n    ```\n\n6.  **Start Agent:**\n\n    ```bash\n    cd agent\n    go run cmd/agent/main.go\n    ```\n\n## Core Resources\n\nPraetor ships with a core set of native Linux resources needed to deploy most applications out-of-the-box. These are compiled directly into the agent for maximum performance and reliability on base Linux distributions:\n\n*   **File:** Manage file contents, existence, permissions, and owners. \n*   **Package:** Manage OS packages using an intelligent, pluggable provider that detects `apt`, `yum`, or `apk` automatically.\n*   **Service:** Control system daemon states (`running`, `stopped`, `enable` or `disable`) with native `systemd` support and a `service` fallback.\n*   **Exec:** Execute arbitrary shell commands, with built-in idempotency logic (`creates`, `onlyif`, `unless`).\n*   **User:** Manage deep configuration of Linux user accounts, mapping primary/secondary groups, homes, shells, and structural UIDs natively via `useradd`/`usermod`.\n*   **Group:** Establish and enforce structural groups directly against `/etc/group` binaries (`groupadd`/`groupmod`).\n*   **Cron:** Isolate, inject, and enforce scheduled cron jobs identically inside a user's `crontab -l` isolated via explicit UUID tagging, protecting manual job edits.\n\n## Catalog Schema \u0026 Hydration\n\nCatalogs now follow a defined schema using Go structs located in the `schema/`\ndirectory. Resources within the catalog are validated against\nthese schemas on the agent side.\n\nThe Master **hydrates** the catalog content based on agent facts. String fields\nwithin the resource `spec` in `master/catalog.yaml` can contain Go\n`text/template` syntax (e.g., `{{ .facts.hostname }}`). The Master renders these\ntemplates before sending the catalog to the agent.\n\nExample `master/catalog.yaml` resource:\n\n```yaml\napiVersion: praetor.io/v1alpha1\nkind: Catalog\nmetadata:\n  name: default-catalog\nspec:\n  resources:\n    - apiVersion: praetor.io/v1alpha1\n      kind: File\n      metadata:\n        name: managed-by-master\n      spec:\n        path: /tmp/managed_by_master.txt\n        content: \"This file is managed by the MASTER (v4).\\nHostname: {{ .facts.hostname }}\\nOS: {{ .facts.os }}\"\n        ensure: present\n        mode: \"0644\"\n    - apiVersion: praetor.io/v1alpha1\n      kind: File\n      metadata:\n        name: to-be-deleted\n      spec:\n        path: /tmp/to_be_deleted.txt\n        ensure: absent\n```\n\n## Usage\n\n### Catalog Management\n\nModify `master/catalog.yaml` to define the desired state. The Master will\nperiodically (every `MASTER_TRIGGER_INTERVAL`, default 15s) send a trigger to the\n`TARGET_NODE_ID` (default `agent1`) to fetch and apply the catalog.\n\nTo manually trigger an update on `agent1` outside the interval:\n\n```bash\nnats --tlscert ./nats/certs/client.crt --tlskey ./nats/certs/client.key --tlsca ./nats/certs/ca.crt --server=nats://localhost:4222 pub agent.trigger.getCatalog.agent1 \"\"\n```\n\n### NATS Commands\n\nCurrently, the only ad-hoc command supported is `commands.facts.get`.\n\nExample: Get facts from `agent1`:\n\n```bash\nnats --tlscert ./nats/certs/client.crt --tlskey ./nats/certs/client.key --tlsca ./nats/certs/ca.crt --server=nats://localhost:4222 req commands.facts.get '{\"facts\": [\"os\", \"hostname\"]}'\n```\n\n### Observability / Metrics\n\nThe Master node natively exposes an OpenMetrics / Prometheus `/metrics` endpoint on port `8080`.\nYou can bind Prometheus or Datadog scrapers to monitor the fleet's orchestration health directly!\n```bash\ncurl http://localhost:8080/metrics\n```\n\n## Key Concepts\n\n*   **mTLS:** Mutual Transport Layer Security is used to secure all gRPC\n    communication between the Master and Agent, and also for connections to the\n    NATS server.\n*   **NATS JetStream:** The persistence layer of NATS, used for the\n    Orchestration Plane and for triggering catalog fetches.\n*   **Fact Management:** Agents collect system facts (e.g., OS, hostname, CPU, memory\n    via `gopsutil`) and send them to the Master with each catalog request. The\n    Master can use these facts to tailor catalog compilation. This is extensible\n    through the `facts.Facter` interface.\n*   **Catalog:** A document (currently generated from `master/catalog.yaml`)\n    conforming to the defined schemas in `schema/` that defines the desired\n    state of resources on a node. The content is hydrated by the Master using\n    agent facts.\n*   **Digital Signatures:** The Master cryptographically signs the catalog using\n    ED25519, and the Agent verifies the signature to ensure authenticity and\n    integrity.\n*   **Resources:** Abstract representations of configurable items on a node\n    (e.g., files, packages, services). Each resource type has a defined schema\n    and implements the `resources.Resource` interface.\n\n## Project Structure\n\nPraetor follows a modular go workspace approach:\n\n*   **`agent/`**: Contains the agent application, including its `main.go` entrypoint, local `facts/` collectors, and the enforcement logic for `resources/`.\n*   **`master/`**: Contains the master application, `main.go` entrypoint, and the `catalog/` compilation logic.\n*   **`pkg/`**: Core shared libraries that are agnostic to the specific binary running them. \n    *   Currently houses the `broker/` package for pluggable messaging (e.g., NATS). \n    *   Future shared utilities (like logging, TLS helpers, or auth libraries) should be placed here.\n*   **`schema/`**: Shared structs and validation logic for the configuration catalog to ensure both Master and Agent agree on resource definitions.\n*   **`proto/`**: gRPC interface definitions and the resulting generated code.\n\n## Developing New Fact Providers\n\nTo add a new source of custom facts:\n\n1.  **Create a Package:** Create a new directory under `agent/facts/` (e.g.,\n    `agent/facts/myfacts`).\n\n2.  **Define the Struct:** Inside your new package, define a struct for your custom\n    facter.\n\n3.  **Implement `facts.Facter` Interface:** Implement the following methods for\n    your struct:\n\n    *   `Name() string`: Return a unique name for your facter (e.g.,\n        `\"myfacts\"`).\n    *   `GetFacts() (map[string]interface{}, error)`: Return a map of fact names\n        to their values. This is where you'll implement the logic to gather\n        your custom facts.\n\n4.  **Register the Facter:** In the same file, add an `init()` function to\n    register your new facter with the central fact registry:\n\n    ```go\n    package myfacts\n\n    import (\n        \"github.com/guilledipa/praetor/agent/facts\"\n    )\n\n    type MyFacter struct{}\n\n    func (f *MyFacter) Name() string { return \"myfacts\" }\n\n    func (f *MyFacter) GetFacts() (map[string]interface{}, error) {\n        myFcts := make(map[string]interface{})\n        // ... gather custom facts ...\n        myFcts[\"my_custom_fact\"] = \"some_value\"\n        return myFcts, nil\n    }\n\n    func init() {\n        facts.RegisterFacter(\u0026MyFacter{})\n    }\n    ```\n\n5.  **Import in Agent:** Add a blank import to `agent/main.go` to ensure the\n    `init()` function of your new facter package is executed:\n\n    ```go\n    import (\n        // ... other imports\n        _ \"github.com/guilledipa/praetor/agent/facts/myfacts\"\n    )\n    ```\n\n6.  **Update `agent/go.mod`:** Run `go mod tidy` in the `agent` directory.\n\n## Developing New Resources\n\nTo add support for a new resource type (e.g., `crontab`):\n\n1.  **Create Schema:** Define the struct for your resource in the `schema/`\n    directory (e.g., `schema/package.go`), including `APIVersion`, `Kind`,\n    `ObjectMeta`, and `Spec` with validation tags.\n\n2.  **Create Resource Package:** Create a new directory under `agent/resources/`\n    (e.g., `agent/resources/package`).\n\n3.  **Implement `resources.Resource` Interface:** In your new package, create a\n    struct that embeds the schema struct. Implement the methods:\n    *   `Type() string`: Return `Kind` from the schema.\n    *   `ID() string`: Return a unique identifier (e.g., from `Metadata.Name`).\n    *   `Get() (resources.State, error)`: Retrieve the current state.\n    *   `Test(currentState resources.State) (bool, error)`: Compare current vs\n        desired.\n    *   `Set() error`: Enforce the desired state.\n\n4.  **Register the Type:** Add an `init()` function to register your new type using\n    `resources.RegisterType`, unmarshalling into your schema struct and\n    validating it.\n\n    ```go\n    package myresource\n\n    import (\n        \"encoding/json\"\n        \"fmt\"\n        \"github.com/guilledipa/praetor/agent/resources\"\n        \"github.com/guilledipa/praetor/schema\"\n        \"github.com/go-playground/validator/v10\"\n    )\n\n    type MyResource struct {\n        schema.MyResource // Embed the schema definition\n    }\n\n    func init() {\n        resources.RegisterType(\"MyResourceKind\", func(spec json.RawMessage) (resources.Resource, error) {\n            var r schema.MyResource\n            if err := json.Unmarshal(spec, \u0026r); err != nil {\n                return nil, fmt.Errorf(\"failed to unmarshal myresource spec: %w\", err)\n            }\n            if err := r.Validate(); err != nil {\n                return nil, fmt.Errorf(\"myresource spec validation failed: %w\", err)\n            }\n            return \u0026MyResource{MyResource: r}, nil\n        })\n    }\n\n    // ... Implement resources.Resource interface for \u0026MyResource ...\n    ```\n\n#### 6. Update `agent/go.mod`\nRun `go mod tidy` or preferably use `make tidy` at the project root.\n\n## Local Development (Build System)\n\nPraetor uses a natively managed **Go Workspace** (`go.work`) linked with a root **Makefile** to orchestrate its multi-module architecture effortlessly.\n\nUsing `go.work` enables IDEs (like VSCode or GoLand) to instantly resolve cross-module imports (e.g., when the `agent` heavily references the `pkg` module).\n\nYou do not need to individually compile components. The project root provides a unified Make Task Runner:\n\n- **`make all`**: Compiles Protobufs, executes all tests, and builds binaries.\n- **`make build`**: Statically compiles both the `agent` and `master` binaries sequentially and exports them into `./bin/`.\n- **`make test`**: Runs the entire matrix of table-driven testing suites simultaneously across `agent/`, `master/`, and `pkg/`.\n- **`make tidy`**: Globally cleans up dependency trees on all 4 isolated `go.mod` files.\n\n#### Proto Generation\n\nIf you modify `proto/master.proto`, you need to regenerate the Go code using `protoc` across the workspace. We have completely automated this via the root `Makefile`. \n\nSimply run:\n```bash\nmake proto\nmake tidy\n```\nThis single command handles compiling the gRPC abstractions and cleanly restructuring all 4 module dependency trees automatically!\n\n## Phase 8 Capabilities Built\n\n- **Persistent State Compliance Reports**: Expand the `ReportState` handler on the Master to implement a `StorageProvider` (NATS JetStream Key/Value) that persistently caches the historical configuration drifts generated across the fleet, paving the way for a compliance dashboard.\n- **Multi-OS Package Manager Support**: Expand the `Package` resource plugin on the agent to dynamically swap `apt` out for `yum`, `dnf`, or `zypper` depending on the enforcing node's `$PATH` binary availability.\n- **Advanced Linux Primitives**: We fully established robust state management over structural Linux boundaries including `user`, `group`, and `cron` components, verified safely under the hood through Go Helper Process mocking abstractions.\n\n## Phase 17 Capabilities Built: Extensible RPC Plugin Engine\n\nWhile Praetor previously compiled its \"Core\" Linux plugins directly into the agent binary, we have decoupled extensibility entirely using a **Hashicorp-style RPC Plugin Architecture**.\n\n### The \"Core\" Plugin vs \"External\" Plugins\n- **The Core Linux Plugin**: The local Linux resources (File, User, Group, Service, Package, Cron, Exec) were successfully extracted into a primary `praetor-plugin-linux` standalone provider. \n- **Ecosystem Expansion**: End users can now write completely isolated plugins in *any programming language* (Go, Python, Rust) without recompiling Praetor! Drop a plugin binary into the `/opt/praetor/plugins/` directory, and the Agent daemon auto-discovers and orchestrates them dynamically over `gRPC`.\n\n\n### How RPC Plugins Work\nWhen Praetor encounters a `mysql_user` resource or any dynamic primitive in its declarative DAG, it doesn't execute it internally:\n1. Praetor daemon spins up the target `praetor-plugin-` binary in the background.\n2. The Agent handshakes with the binary over a local UNIX Domain Socket (UDS) proxy via robust **gRPC**.\n3. It dispatches a `GetState(resource_id)` RPC call.\n4. The plugin securely returns the state evaluation, and Praetor schedules the drifts mathematically. If the plugin crashes, Praetor simply catches the RPC termination and flags the resource as failed without bringing down the orchestrator!\n\n### Physical Host \u0026 VM Deployment Architecture\nBecause plugins operate via isolated binaries communicating over gRPC UNIX Sockets, they act cleanly across typical physical machines or Virtual Machines (VMs) where the primary Praetor Agent is running.\n\nPlugins do not need to be compiled directly into the monolithic Agent installation. \nInstead, they deploy smoothly onto the host OS:\n- **Daemon Native Spawning:** You place a standalone `praetor-plugin-mysql` binary in a known directory (e.g., `/opt/praetor/plugins/`). When the primary `praetor-agent` (running as a `systemd` service) boots up, it discovers these binaries and launches them natively as child background daemons. \n- **Isolated UNIX Sockets:** The agent and its child plugins communicate locally over `/var/run/praetor/` UNIX sockets securely on the host machine.\n- **Resilience:** If a specific workload plugin (like `mysql` or `redis`) panics or crashes, the core OS node agent remains completely stable and simply flags the associated resources as unreachable, continuing to manage everything else!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguilledipa%2Fpraetor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fguilledipa%2Fpraetor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguilledipa%2Fpraetor/lists"}