Skip to content

How it works

Four stages turn a small JSON profile into a proven, named image: Define, Resolve, Build, Prove.

Terms such as lock, seal and smoke test are explained in Key concepts.

%%{init: {'theme':'base','fontFamily':'ui-sans-serif, system-ui, -apple-system, Helvetica, Arial, sans-serif','themeVariables':{'primaryColor':'#ffffff','primaryTextColor':'#2b2d42','primaryBorderColor':'#c9184a','lineColor':'#8d99ae','edgeLabelBackground':'#ffffff','fontSize':'13px'},'flowchart':{'curve':'basis','nodeSpacing':45,'rankSpacing':55,'padding':8,'useMaxWidth':true}}}%%
flowchart TB
    subgraph D[" โ‘  Define  ยท  profiles, bases, packages (JSON) "]
        direction LR
        PR["profiles/<br/>NAME.json"] ~~~ BA["config/bases/<br/>*.json"] ~~~ PK["config/packages/<br/>*.json"]
    end
    subgraph R["&nbsp;โ‘ก Resolve &amp; review &nbsp;ยท&nbsp; the Mac, or any host with gpg&nbsp;"]
        direction LR
        RS["resolve.py<br/>resolve"] --> LK[("locks/<br/>NAME.lock.json")] --> RV{{"you review<br/>&amp; commit"}}
    end
    subgraph B["&nbsp;โ‘ข Build &nbsp;ยท&nbsp; Apple silicon Mac&nbsp;"]
        direction LR
        VF["verify<br/>cache โ†” lock"] --> IN["install from the<br/>vendor IPSW / ISO"] --> GU["guest re-verifies,<br/>hardens, seals"]
    end
    subgraph P["&nbsp;โ‘ฃ Prove &nbsp;ยท&nbsp; a throwaway clone&nbsp;"]
        direction LR
        UV["โ€ฆ-unverified"] --> ST{{"smoke test<br/>from outside"}}
        ST -->|"pass"| OK["rbt-NAME-sha โœ…"]
        ST -->|"fail"| KEEP["kept for<br/>inspection"]
    end
    D ==> R ==> B ==> P

    classDef cfg fill:#fff0f3,stroke:#c9184a,stroke-width:1.5px,color:#2b2d42
    classDef step fill:#ffffff,stroke:#2b2d42,stroke-width:1.5px,color:#2b2d42
    classDef gate fill:#ffd6de,stroke:#c9184a,stroke-width:1.5px,color:#2b2d42
    classDef good fill:#c9184a,stroke:#800f2f,stroke-width:1.5px,color:#ffffff
    classDef bad fill:#2b2d42,stroke:#2b2d42,color:#ffffff
    class PR,BA,PK cfg
    class RS,VF,IN,GU,UV step
    class LK,RV,ST gate
    class OK good
    class KEEP bad
    style D fill:#f7f7f9,stroke:#d9a5b3,color:#6b6b76
    style R fill:#f7f7f9,stroke:#d9a5b3,color:#6b6b76
    style B fill:#f7f7f9,stroke:#d9a5b3,color:#6b6b76
    style P fill:#f7f7f9,stroke:#d9a5b3,color:#6b6b76
  1. Define. A profile names an OS base and a list of tools. Bases and packages declare where each input comes from and how it is verified.
  2. Resolve. resolve.py finds the current versions, downloads them, verifies them, and writes a lock. macOS profiles resolve on the Mac; NixOS and Kali resolve anywhere with gpg. A person reviews the lock diff before it's committed.
  3. Build. build.sh re-checks the cache against the lock and installs from the vendor image. Inside the guest, every staged file is checked again before install. The guest then hardens itself, removes build residue and identity, and powers off.
  4. Prove. smoke-test.sh boots a disposable clone and tests it from outside. For a key-SSH image (RHUBARB_SSH_PUBKEYS set), password login must be refused and key login must work. It then logs in and checks the hardening: passwordless sudo, auto-login or a disabled firewall fails the test. A key-SSH Linux image is also rebooted once, and on both boots / must be read-write with its fstab mounts up. For an SSH-disabled image, port 22 must refuse connections; nothing inside can be checked, because there is no way in. Every image must keep Screen Sharing closed, and any port a package declares (Juice Shop's 3000) must answer. Only then is the image renamed to rbt-<profile>-<inputs-sha>. The name is derived from the inputs, so identical inputs give an identical name, and out/<vm>.provenance.json records exactly what went in.