diff --git a/dev/certs/localhost/ca.crt b/dev/certs/localhost/ca.crt
index 0f9cb685c4..996b406c72 100644
--- a/dev/certs/localhost/ca.crt
+++ b/dev/certs/localhost/ca.crt
@@ -1,11 +1,11 @@
-----BEGIN CERTIFICATE-----
-MIIBjjCCATOgAwIBAgIUSGDgn+yJYhr4WwJE8/LuHIv8bRMwCgYIKoZIzj0EAwIw
-FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDYwMjIwMTcwNVoXDTM1MDUzMTIw
-MTcwNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0D
+MIIBjTCCATOgAwIBAgIUUbTC/rqeWN6ImaPKvseBPyts3bowCgYIKoZIzj0EAwIw
+FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDMwNTAxMDM0NVoXDTM2MDMwMjAx
+MDM0NVowFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0D
AQcDQgAEZOSlatLzFeE6/9bAaRs2ZGp0tWdVJsApXZSFI+ssNpWVHfl/xbsSg3s8
e/EPGyoaTl52B/7B6cphYZG/XMqfFqNjMGEwHQYDVR0OBBYEFP3XbiEfU2RSQlH9
UtoAy5yNsMqdMB8GA1UdIwQYMBaAFP3XbiEfU2RSQlH9UtoAy5yNsMqdMA8GA1Ud
-EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA0kAMEYCIQCY
-6aZXqaaXc4b0j1TywzNsC9S8fvtGhV9qxASzypmYrQIhAMMcVzTNqWh/hHeAWkgE
-WgMORLUbzlhVYghrE7xdIkZH
+EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA0gAMEUCICKJ
+XR3+kEW/e4jhyGezvLuSFlotUMAPqNgcNTuS3OWZAiEA3pcRA4YgZzlFGXCW/kWj
+YeykmVGg+RezSlkJVgMPCC4=
-----END CERTIFICATE-----
diff --git a/dev/certs/localhost/client.crt b/dev/certs/localhost/client.crt
index 2c6987f430..c72dadee1f 100644
--- a/dev/certs/localhost/client.crt
+++ b/dev/certs/localhost/client.crt
@@ -1,12 +1,13 @@
-----BEGIN CERTIFICATE-----
-MIIBvTCCAWOgAwIBAgIUIL+x94GdtA9AvTeCyIjwNA7rnbYwCgYIKoZIzj0EAwIw
-FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDYwMjIwMTcwNVoXDTI2MDYwMjIw
-MTcwNVowFjEUMBIGA1UEAwwLVGVzdCBDbGllbnQwWTATBgcqhkjOPQIBBggqhkjO
+MIIB4DCCAYWgAwIBAgIUZ7biiH4XMAS9+fPHFMAWX9odf+8wCgYIKoZIzj0EAwIw
+FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDMwNTAxMDM0NVoXDTI3MDMwNTAx
+MDM0NVowFjEUMBIGA1UEAwwLVGVzdCBDbGllbnQwWTATBgcqhkjOPQIBBggqhkjO
PQMBBwNCAATH1LS45MUYeiluQFEeJa2vcgwaiYGhpVLsDuEPdet8bk8K9Ic7PWRv
-uv32g6a+gq3Zchzu1PHoeUiXRaeGsji/o4GQMIGNMAkGA1UdEwQCMAAwCwYDVR0P
-BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAUBgNVHREEDTAL
-gglsb2NhbGhvc3QwHQYDVR0OBBYEFO3x9xTYUVtn7x9hzCNLUODQLmLNMB8GA1Ud
-IwQYMBaAFP3XbiEfU2RSQlH9UtoAy5yNsMqdMAoGCCqGSM49BAMCA0gAMEUCIH30
-DVy+DPR0V/v9T1XJ8HhlUg8UbPmcwTgzavL42syFAiEA/KKtNnVBEZ6+m7fDIulS
-5sD2UZxwVkLBGJTFLRwwxmU=
+uv32g6a+gq3Zchzu1PHoeUiXRaeGsji/o4GyMIGvMAkGA1UdEwQCMAAwCwYDVR0P
+BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA2BgNVHREELzAt
+gglsb2NhbGhvc3SCFGhvc3QuZG9ja2VyLmludGVybmFshwR/AAABhwTAqEH+MB0G
+A1UdDgQWBBTt8fcU2FFbZ+8fYcwjS1Dg0C5izTAfBgNVHSMEGDAWgBT9124hH1Nk
+UkJR/VLaAMucjbDKnTAKBggqhkjOPQQDAgNJADBGAiEAoL14N3nkpHUmnhF7ErRx
+QszmESCUs5WTHFMSy/FfdtUCIQCNKigh/n+TwsYZfyKu2XP9Fn16Crt6JxhUtJZf
+OwFloQ==
-----END CERTIFICATE-----
diff --git a/dev/certs/localhost/gen-certs.sh b/dev/certs/localhost/gen-certs.sh
index db5df39e82..50b58678b3 100755
--- a/dev/certs/localhost/gen-certs.sh
+++ b/dev/certs/localhost/gen-certs.sh
@@ -46,9 +46,12 @@ keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth
subjectAltName = @alt_names
+# host.docker.internal and 192.168.65.254 are docker 'magic' name/ip to communicate with host on macOS.
[ alt_names ]
DNS.1 = localhost
+DNS.2 = host.docker.internal
IP.1 = 127.0.0.1
+IP.2 = 192.168.65.254
EOF
# Generate CA key and self-signed certificate
diff --git a/dev/certs/localhost/localhost.crt b/dev/certs/localhost/localhost.crt
index 7b325b4aed..4123d44a21 100644
--- a/dev/certs/localhost/localhost.crt
+++ b/dev/certs/localhost/localhost.crt
@@ -1,12 +1,13 @@
-----BEGIN CERTIFICATE-----
-MIIBwjCCAWegAwIBAgIUU8w1DlbAnCBW8vM15Ni1vCzk2GQwCgYIKoZIzj0EAwIw
-FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDYxMDE2NDcxNloXDTI2MDYxMDE2
-NDcxNlowFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0D
+MIIB3TCCAYOgAwIBAgIUZ7biiH4XMAS9+fPHFMAWX9odf+4wCgYIKoZIzj0EAwIw
+FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDMwNTAxMDM0NVoXDTI3MDMwNTAx
+MDM0NVowFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0D
AQcDQgAEUcNhl5in7twuxJSVS0PJ9lx/nyChb0gt2t4EqBh0EXirXwOv0UgAlTtU
-ySW+gz6G/fhnvU1VTEZj1R9ROmAUjqOBljCBkzAJBgNVHRMEAjAAMAsGA1UdDwQE
-AwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGgYDVR0RBBMwEYIJ
-bG9jYWxob3N0hwR/AAABMB0GA1UdDgQWBBQGkf9KiSeLoePk7VNsnNpwoLcj+DAf
-BgNVHSMEGDAWgBT9124hH1NkUkJR/VLaAMucjbDKnTAKBggqhkjOPQQDAgNJADBG
-AiEAjApLmLwEvi+oJNmpPZdDuYxg5LDZMyyiHpoTPCQWGiYCIQChuNsE2DfeEOMI
-BM9NCIxPiaiKbzP+Abh3r58TRH0XPg==
+ySW+gz6G/fhnvU1VTEZj1R9ROmAUjqOBsjCBrzAJBgNVHRMEAjAAMAsGA1UdDwQE
+AwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwNgYDVR0RBC8wLYIJ
+bG9jYWxob3N0ghRob3N0LmRvY2tlci5pbnRlcm5hbIcEfwAAAYcEwKhB/jAdBgNV
+HQ4EFgQUBpH/Sokni6Hj5O1TbJzacKC3I/gwHwYDVR0jBBgwFoAU/dduIR9TZFJC
+Uf1S2gDLnI2wyp0wCgYIKoZIzj0EAwIDSAAwRQIgUtrkxbKFr/SxzcE4FbI39ep6
+jQPy7pUoPZGWue1uYkkCIQDeunYP19qD1+k/sEdt/8xwbFL4BA2211+s017BoRwv
+IA==
-----END CERTIFICATE-----
diff --git a/dev/mac-local-dev/README.md b/dev/mac-local-dev/README.md
index eb96e6a085..a4982db13b 100644
--- a/dev/mac-local-dev/README.md
+++ b/dev/mac-local-dev/README.md
@@ -1,246 +1,179 @@
-# Local Carbide API (without machine-a-tron for now)
+# Mac Local Development — Carbide API
-Notes:
-- technically you can start machine-a-tron but it is useless on a Mac since its magic relies on Linux-specific features. It may work in docker on Mac...
-- which is why we should run carbide on Mac in docker too. But for now this run native on Mac.
+Runs `carbide-api` natively on macOS (no Docker for the binary itself).
+Docker Desktop is used only for Vault and Postgres.
+This Carbide API instance is usable by Carbide REST stack.
-## To run carbide from the carbide directory:
-This will setup everything and run carbide-api binary.
+> **Limitations**
+> - TPM / attestation features require Linux and a physical TPM — they are disabled in this setup.
+> - `machine-a-tron` relies on Linux-specific features and is not useful on macOS.
-You can verify carbide-api is running by doing:
-```bash
-grpcurl -plaintext localhost:1079 list
-````
-If you configure carbide to run with TLS , you can do:
-```bash
-grpcurl -insecure localhost:1079 list
-```
+## Prerequisites
-## To run carbide in IntelliJ/RustRover IDE
+| Tool | Notes |
+|------|-------|
+| Docker Desktop | Must be running before the script is invoked |
+| Rust toolchain | `cargo` must be on `$PATH` |
+| `jq` | JSON processing for Vault init output |
+| `curl` | Vault health-check polling |
+| `openssl` | TLS cert generation (pre-installed on macOS) |
-IDE setup is not complete: you may want to configure 'Rust -> External Linters -> Additional Arguments' to include '--no-default-features'.
+---
-First run carbide stand-alone the kill it (it does setup everything).
-Then get some light setup and the needed variables by running:
-```bash
-SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt dev/mac-local-dev/set-env.sh
-```
-It should output something like:
-```bash
-# required variables to run carbide-api:
-export VAULT_PKI_ROLE_NAME=role
-export VAULT_ADDR=http://localhost:8200
-export CARBIDE_WEB_OAUTH2_CLIENT_SECRET=
-export VAULT_PKI_MOUNT_LOCATION=certs
-export VAULT_KV_MOUNT_LOCATION=secrets
-export VAULT_TOKEN=
-export CARBIDE_WEB_AUTH_TYPE=oauth2
-export CARBIDE_WEB_PRIVATE_COOKIEJAR_KEY=
-export DATABASE_URL=postgresql://postgres:admin@localhost
-
-You will need to create/modify a run configuration for carbide-api.
-- cargo command parameters:
-```
-run --package carbide-api
---no-default-features -- run
---config-path dev/mac-local-dev/carbide-api-config-custom.toml
-```
-- environment variables: copy/paste the single line output from above into the 'Environment variables' section.
+## Starting Carbide API
-# Setting a local carbide-api for cloud-local
+Run from **any directory** — the script resolves the repo root automatically:
-BEFORE setting the cloud-local environment you MUST configure auth by replacing 'kas-legacy' in `deploy/kustomize/overlays/local/configmap.yaml` by the following:
-```yaml
-- name: nvidia
- origin: 4 # TokenOriginCustom
- url: https://stg.authn.nvidia.com/pubJWKS
- issuer: "stg.auth.ngc.nvidia.com"
- serviceAccount: True
+```bash
+./dev/mac-local-dev/run-carbide-api.sh
```
-Setup cloud-local (<8m):
+The script is fully self-contained and idempotent. On each run it:
+
+1. Checks prerequisites (`docker`, `cargo`, `jq`, `curl`).
+2. Starts a **Vault** container (`carbide-vault`) on port **8201** and initialises it
+ (KV secrets + PKI) if not already running. The root token is cached at
+ `/tmp/carbide-localdev-vault-root-token`.
+3. Regenerates **TLS certificates** under `dev/certs/localhost/` if they are
+ missing or stale (`gen-certs.sh` is idempotent).
+4. Starts a **Postgres** container (`pgdev`) on port **5432** with SSL if not
+ already running.
+5. Creates `/opt/carbide/firmware` (may prompt for `sudo` once).
+6. Writes a temporary resolved config to `/tmp/carbide-api-config-.toml`
+ with absolute TLS cert paths (the checked-in config uses paths relative to
+ `$CWD`, which would break when launched from an IDE).
+7. Runs **database migrations**.
+8. Starts `carbide-api` (foreground, `Ctrl-C` to stop).
+
+Once running:
+
```bash
-cd cloud-local
-scripts/setup-forge-cloud.sh --clean
+# Verify gRPC is up
+grpcurl -insecure localhost:1079 list
+
+# Web UI
+open https://localhost:1079/admin
```
-Make available the local carbide-api to cloud-local by running:
+### Resetting state
+
```bash
-cd cloud-local
-scripts/setup-local-carbide-service.sh
+# Remove containers (preserves cert files)
+docker rm -f carbide-vault pgdev
+
+# Also regenerate certs from scratch
+rm -f dev/certs/localhost/*.crt dev/certs/localhost/*.key
```
-Start the elektra-site-agent:
-```bash
-cd cloud-local
-scripts/setup-site.sh
-````
+---
+## Using carbide-admin-cli
-Make available the local carbide-api to cloud-local by running:
-- use SSH port forwarding to expose carbide-api running remotely on your local machine.
- Example: `ssh -L 10443:10.217.117.194:443 mydev`
- You can check access by going to
-- configure cloud-local to use your remote carbide-api by running:
-```bash
-cd cloud-local
-scripts/setup-dev-carbide-service.sh
-```
+In a **second terminal**, use the wrapper script to talk to the running API:
-Start the elektra-site-agent:
```bash
-cd cloud-local
-scripts/setup-site.sh
-````
+./dev/mac-local-dev/run-carbide-admin-cli.sh [args...]
+```
-# End-to-end test
+The script:
+- Builds `carbide-admin-cli` automatically if `target/debug/carbide-admin-cli`
+ does not exist.
+- Wires up TLS using the locally-generated certs from `dev/certs/localhost/`
+ (the same CA that `run-carbide-api.sh` configures the server to trust).
+- Can be run from any directory.
-## create a tenant and retrieve the Tenant ID
+### Global flags
-Create and retrieve Tenant by doing a 'Get Current Tenant' request...
+| Flag | Short | Description |
+|------|-------|-------------|
+| `--format ` | `-f` | `ascii-table` (default), `json`, … |
+| `--carbide-api ` | `-c` | Override API URL |
+| `--output ` | `-o` | Write output to file |
+| `--extended` | | Include internal UUIDs and extra fields |
+| `--sort-by ` | | `primary-id` (default) or `state` |
+| `--debug` | `-d` | Increase log verbosity (repeat for trace) |
+| `--internal-page-size N` | `-p` | Paging size for list calls (default 100) |
-Take note of the Tenant ID for subsequent requests.
+### Common subcommands
-## retrieve the Site ID
+```bash
+# List all machines
+./dev/mac-local-dev/run-carbide-admin-cli.sh machine list
-WARNING: you may have to wait for site to be in 'Registered' state before proceeding.
+# Show details for a specific machine
+./dev/mac-local-dev/run-carbide-admin-cli.sh machine show
-Retrieve the Site ID by doing a 'Get All Sites' request...
+# List OS images
+./dev/mac-local-dev/run-carbide-admin-cli.sh os-image list
-Take note of the Site ID for subsequent requests.
+# List network segments
+./dev/mac-local-dev/run-carbide-admin-cli.sh network-segment list
-## create an ip block
+# List tenants (JSON output)
+./dev/mac-local-dev/run-carbide-admin-cli.sh --format json tenant show
-```json
-{
- "name": "allocation-test-super-block",
- "description": "IP Super block for Allocation test",
- "siteId": "{{siteId}}",
- "routingType": "DatacenterOnly",
- "prefix": "100.100.0.0",
- "prefixLength": 19,
- "protocolVersion": "IPv4"
-}
-```
-Take note of the IP Block ID for subsequent requests.
-
-## create an allocation
-
-Use the ID of the above created IP Block as resourceTypeId.
-
-```json
-{
- "name": "allocation-test-dont-use-ip-block",
- "description": "Allocation Test IP Block for Demo Tenant",
- "tenantId": "{{tenantId}}",
- "siteId": "{{siteId}}",
- "allocationConstraints": [
- {
- "resourceType": "IPBlock",
- "resourceTypeId": "3b697bc7-27e2-479c-8152-51b33bfd4c5a",
- "constraintType": "Reserved",
- "constraintValue": 19
- }
- ]
-}
+# Explore all available subcommands
+./dev/mac-local-dev/run-carbide-admin-cli.sh --help
+# Explore sub-subcommands
+./dev/mac-local-dev/run-carbide-admin-cli.sh machine --help
```
-## create a VPC (this will reach out to carbide API)
-Ensure Site is in 'Registered' state before creating VPC: it should happen automatically if there is a proper connection already.
-
-```json
-{
- "name": "capi-test-vpc",
- "description": "VPC for Testing CAPI Integration",
- "siteId": "{{siteId}}"
-}
-```
-
-# PREVIOUS INFORMATION FOR REFERENCE
YOU SHOULD PROBABLY IGNORE FOR NOW
-Running machine-a-tron against carbide API locally.
+### Environment variable overrides
-Requires `sops` and corresponding key in `~/Library/Application Support/sops/age/keys.txt`
-(just like with the normal development environment).
+| Variable | Default | Purpose |
+|----------|---------|---------|
+| `CARBIDE_API_URL` | `https://localhost:1079` | API endpoint |
+| `FORGE_ROOT_CA_PATH` | `dev/certs/localhost/ca.crt` | CA used to verify the server cert |
+| `CLIENT_CERT_PATH` | `dev/certs/localhost/client.crt` | mTLS client certificate |
+| `CLIENT_KEY_PATH` | `dev/certs/localhost/client.key` | mTLS client key |
+### Expired certificate errors
-## Setup Vault and Postgres
+If you see `invalid peer certificate: Expired`, the certs in
+`dev/certs/localhost/` need to be regenerated:
-
-Run vault and add the site-wide secrets that carbide API depends on.
```bash
-# I used vault version Vault v1.20.2 (824d12909d5b596ddd3f34d9c8f169b4f9701a0c), built 2025-08-05T19:05:39Z
-docker run --rm --detach --name vault --cap-add=IPC_LOCK -e 'VAULT_LOCAL_CONFIG={"storage": {"file": {"path": "/vault/file"}}, "listener": [{"tcp": { "address": "0.0.0.0:8200", "tls_disable": true}}], "default_lease_ttl": "168h", "max_lease_ttl": "720h", "ui": true}' -p 8200:8200 hashicorp/vault server
-docker exec -it vault sh
-
-export VAULT_ADDR="http://127.0.0.1:8200"
-vault operator init -key-shares=1 -key-threshold=1 -format=json
-# copy out unseal_keys_b64 and root_token
-# don't forget any trailing '='s
-# save the ROOT_TOKEN for running carbide API later
-export UNSEAL_KEY="base64 encoded data"
-export ROOT_TOKEN="hvs.something"
-vault operator unseal $UNSEAL_KEY
-vault login $ROOT_TOKEN
-vault secrets enable -path=secrets -version=2 kv
-vault kv delete /secrets/machines/bmc/site/root
-vault kv delete /secrets/machines/all_dpus/site_default/uefi-metadata-items/auth
-vault kv delete /secrets/machines/all_hosts/site_default/uefi-metadata-items/auth
-echo '{"UsernamePassword": {"username": "root", "password": "vault-password" }}' | vault kv put /secrets/machines/bmc/site/root -
-echo '{"UsernamePassword": {"username": "root", "password": "vault-password" }}' | vault kv put /secrets/machines/all_dpus/site_default/uefi-metadata-items/auth -
-echo '{"UsernamePassword": {"username": "root", "password": "vault-password" }}' | vault kv put /secrets/machines/all_hosts/site_default/uefi-metadata-items/auth -
-vault secrets enable -path=certs pki
-vault write certs/root/generate/internal common_name=myvault.com ttl=87600h
-vault write certs/config/urls issuing_certificates="http://vault.example.com:8200/v1/pki/ca" crl_distribution_points="http://vault.example.com:8200/v1/pki/crl"
-vault write certs/roles/role allowed_domains=example.com allow_subdomains=true max_ttl=72h require_cn=false allowed_uri_sans="spiffe://forge.local/*"
+rm -f dev/certs/localhost/*.crt dev/certs/localhost/*.key
+(cd dev/certs/localhost && ./gen-certs.sh)
```
-Run postgres with certs.
-```bash
-cd dev/certs/localhost
-./gen-certs.sh
-bash -c 'docker run --rm --detach --name pgdev --net=host -e POSTGRES_PASSWORD="admin" -e POSTGRES_HOST_AUTH_METHOD=trust -v "$(pwd)/localhost.crt:/var/lib/postgresql/server.crt:ro" -v "$(pwd)/localhost.key:/var/lib/postgresql/server.key:ro" postgres:14.5-alpine -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key -c max_connections=300'
-```
+Then restart `run-carbide-api.sh` (the API must load the new server cert).
-## Run Carbide API
+> **Note:** `dev/certs/server_identity.pem` and
+> `dev/certs/forge_developer_local_only_root_cert_pem` are checked-in certs
+> that expired in 2023/2024. Do **not** use them — the scripts default to the
+> locally-generated `localhost/` certs instead.
-```bash
-FORGED_PATH="../forged"
-export CARBIDE_WEB_OAUTH2_CLIENT_SECRET=$(sops -d $FORGED_PATH/bases/carbide/api/secrets/azure-carbide-web-sso-NONPRODUCTION.enc.yaml | sed -En 's/.*client_secret: (.*)/\1/p' | base64 -d)
-export CARBIDE_WEB_AUTH_TYPE=oauth2
-export CARBIDE_WEB_PRIVATE_COOKIEJAR_KEY=$(openssl rand -base64 64)
-export DATABASE_URL="postgresql://postgres:admin@localhost"
+---
-export VAULT_ADDR="http://localhost:8200"
-export VAULT_KV_MOUNT_LOCATION="secrets"
-export VAULT_PKI_MOUNT_LOCATION="certs"
-export VAULT_PKI_ROLE_NAME="role"
-# copy the vault root token
-export VAULT_TOKEN="hvs.something"
+## Running carbide-api from an IDE (RustRover / IntelliJ)
-# Run SQL migrations.
-cargo run --package carbide-api --no-default-features migrate
+IDE setup is not complete; you may want to set
+**Rust → External Linters → Additional Arguments** to `--no-default-features`.
-sudo mkdir /opt/carbide/firmware # carbide expects this directory to exist (even if empty).
+Run `./dev/mac-local-dev/run-carbide-api.sh` once to completion, then kill it —
+this ensures Vault and Postgres are initialised and the token file exists.
-RUST_BACKTRACE=1 cargo run --package carbide-api --no-default-features -- run --config-path dev/mac-local-dev/carbide-api-config.toml
-```
+Retrieve the environment variables for the run configuration:
-In another terminal, run machine-a-tron.
```bash
-sudo echo sudo enabled # get sudo without password (so we don't have to run cargo as root)
-
-REPO_ROOT=. cargo run --bin machine-a-tron dev/machine-a-tron/config/mac.toml --forge-root-ca-path /Users/fchua/repos/carbide/dev/certs/localhost/ca.crt --client-cert-path /Users/fchua/repos/carbide/dev/certs/localhost/localhost.crt --client-key-path /Users/fchua/repos/carbide/dev/certs/localhost/localhost.key
+echo "CARBIDE_WEB_AUTH_TYPE=basic"
+echo "DATABASE_URL=postgresql://postgres:admin@localhost"
+echo "VAULT_ADDR=http://localhost:8201"
+echo "VAULT_KV_MOUNT_LOCATION=secrets"
+echo "VAULT_PKI_MOUNT_LOCATION=certs"
+echo "VAULT_PKI_ROLE_NAME=role"
+echo "VAULT_TOKEN=$(cat /tmp/carbide-localdev-vault-root-token)"
```
+Cargo run parameters:
+```
+run --package carbide-api --no-default-features -- run
+--config-path /dev/mac-local-dev/carbide-api-config.toml
+```
-[//]: # (Edit your config map entry 'carbide_address' to point to :1079 then restart the elektra-site-agent pods.)
-
-[//]: # (```bash)
-
-[//]: # (kubectl edit configmap elektra-config-map-6745d4gct5 -n elektra-site-agent)
-
-[//]: # (kubectl delete pod elektra-site-agent-0 elektra-site-agent-1 elektra-site-agent-2 -n elektra-site-agent)
-
-[//]: # (```)
+> The config file uses CWD-relative TLS paths. Set the IDE run configuration's
+> **Working Directory** to the repository root, or use the absolute-path temp
+> config that `run-carbide-api.sh` writes to `/tmp/carbide-api-config-.toml`.
diff --git a/dev/mac-local-dev/carbide-api-config.toml b/dev/mac-local-dev/carbide-api-config.toml
index 413f5fe50b..ca7dde32c8 100644
--- a/dev/mac-local-dev/carbide-api-config.toml
+++ b/dev/mac-local-dev/carbide-api-config.toml
@@ -15,7 +15,7 @@
# limitations under the License.
#
listen = "[::]:1079"
-listen_mode = "plaintext_http2"
+listen_mode = "tls"
metrics_endpoint = "[::]:1080"
database_url = "postgresql://postgres:admin@localhost"
asn = 4294967000
@@ -57,11 +57,22 @@ hardware_health_reports = "MonitorOnly"
[firmware_global]
autoupdate = true
+# NOTE: carbide-api resolves these paths relative to the process working
+# directory, NOT relative to this config file. Do NOT run carbide-api
+# directly with this file from an IDE or any directory other than the repo
+# root — cert loading will fail silently.
+# run-carbide-api.sh works around this by copying the config to /tmp and
+# rewriting these values to absolute paths before passing it to the binary.
[tls]
-identity_pemfile_path = "/Users/fchua/repos/carbide/dev/certs/localhost/localhost.crt"
-identity_keyfile_path = "/Users/fchua/repos/carbide/dev/certs/localhost/localhost.key"
-root_cafile_path = "/Users/fchua/repos/carbide/dev/certs/localhost/ca.crt"
-admin_root_cafile_path = "/Users/fchua/repos/carbide/dev/certs/forge_root.pem"
+identity_pemfile_path = "dev/certs/localhost/localhost.crt"
+identity_keyfile_path = "dev/certs/localhost/localhost.key"
+root_cafile_path = "dev/certs/localhost/ca.crt"
+# Use the same local CA for admin client certs so that the client cert
+# generated by dev/certs/localhost/gen-certs.sh is accepted. The
+# checked-in dev/certs/forge_root.pem is the real NVIDIA Forge Root CA
+# whose private key is not available locally, making it impossible to
+# issue valid admin client certs against it for local dev.
+admin_root_cafile_path = "dev/certs/localhost/ca.crt"
[auth]
permissive_mode = true
diff --git a/dev/mac-local-dev/run-carbide-admin-cli.sh b/dev/mac-local-dev/run-carbide-admin-cli.sh
new file mode 100755
index 0000000000..0ec0541f65
--- /dev/null
+++ b/dev/mac-local-dev/run-carbide-admin-cli.sh
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+#
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Wrapper to run carbide-admin-cli against the local dev carbide-api instance
+# started by run-carbide-api.sh.
+#
+# Usage (from repo root or any directory):
+# ./dev/mac-local-dev/run-carbide-admin-cli.sh [args...]
+#
+# Examples:
+# ./dev/mac-local-dev/run-carbide-admin-cli.sh version
+# ./dev/mac-local-dev/run-carbide-admin-cli.sh machine show
+# ./dev/mac-local-dev/run-carbide-admin-cli.sh ipxe-template list
+# ./dev/mac-local-dev/run-carbide-admin-cli.sh ipxe-template get ubuntu-24.04-netboot
+# ./dev/mac-local-dev/run-carbide-admin-cli.sh os-image show
+# ./dev/mac-local-dev/run-carbide-admin-cli.sh --format json ipxe-template list
+#
+
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+
+export REPO_ROOT="$REPO_ROOT"
+# Default to the locally-generated certs produced by dev/certs/localhost/gen-certs.sh.
+# server_identity.pem / forge_developer_local_only_root_cert_pem are checked-in
+# certs that have long expired and cannot be renewed without the NVIDIA CA private key.
+CARBIDE_API_URL="${CARBIDE_API_URL:-https://localhost:1079}"
+FORGE_ROOT_CA_PATH="${FORGE_ROOT_CA_PATH:-$REPO_ROOT/dev/certs/localhost/ca.crt}"
+CLIENT_CERT_PATH="${CLIENT_CERT_PATH:-$REPO_ROOT/dev/certs/localhost/client.crt}"
+CLIENT_KEY_PATH="${CLIENT_KEY_PATH:-$REPO_ROOT/dev/certs/localhost/client.key}"
+
+CLI_BIN="$REPO_ROOT/target/debug/carbide-admin-cli"
+
+if [ ! -x "$CLI_BIN" ]; then
+ echo "Binary not found at $CLI_BIN — building first..."
+ cargo build -p carbide-admin-cli --manifest-path "$REPO_ROOT/Cargo.toml"
+fi
+
+exec "$CLI_BIN" \
+ --carbide-api "$CARBIDE_API_URL" \
+ --forge-root-ca-path "$FORGE_ROOT_CA_PATH" \
+ --client-cert-path "$CLIENT_CERT_PATH" \
+ --client-key-path "$CLIENT_KEY_PATH" \
+ "$@"
diff --git a/dev/mac-local-dev/run-carbide-api.sh b/dev/mac-local-dev/run-carbide-api.sh
new file mode 100755
index 0000000000..a903d28420
--- /dev/null
+++ b/dev/mac-local-dev/run-carbide-api.sh
@@ -0,0 +1,187 @@
+#!/usr/bin/env bash
+#
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+#
+# Self-contained script to start carbide-api on macOS.
+# Run from the repository root: ./dev/mac-local-dev/run-carbide-api.sh
+#
+# Prerequisites: Docker Desktop, Rust toolchain, jq
+#
+
+set -euo pipefail
+
+# -----------------------------------------------------------------------------
+# Configuration
+# -----------------------------------------------------------------------------
+VAULT_CONTAINER="carbide-vault"
+VAULT_PORT=8201
+VAULT_ADDR="http://localhost:$VAULT_PORT"
+TOKEN_FILE="/tmp/carbide-localdev-vault-root-token"
+PG_CONTAINER="pgdev"
+
+# -----------------------------------------------------------------------------
+# Helpers
+# -----------------------------------------------------------------------------
+die() {
+ echo "❌ $*" >&2
+ exit 1
+}
+
+info() {
+ echo "ℹ️ $*"
+}
+
+ok() {
+ echo "✓ $*"
+}
+
+# Ensure we're in the repo root
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$REPO_ROOT"
+
+# -----------------------------------------------------------------------------
+# Prerequisites
+# -----------------------------------------------------------------------------
+echo ""
+echo "=== Carbide API - Mac Local Development ==="
+echo ""
+
+for cmd in docker cargo jq curl; do
+ command -v "$cmd" >/dev/null 2>&1 || die "$cmd not found. Please install it."
+done
+ok "Required binaries: docker, cargo, jq, curl"
+
+if ! docker ps >/dev/null 2>&1; then
+ die "Docker is not running. Start Docker Desktop."
+fi
+ok "Docker is running"
+
+# -----------------------------------------------------------------------------
+# Start Vault (port 8201 - dedicated for carbide, avoids kind cluster conflict)
+# -----------------------------------------------------------------------------
+if docker ps --format '{{.Names}}' | grep -w "$VAULT_CONTAINER" >/dev/null; then
+ ok "Vault container already running"
+ [ -f "$TOKEN_FILE" ] || die "Token file missing. Remove container and retry: docker rm -f $VAULT_CONTAINER"
+ chmod 600 "$TOKEN_FILE"
+else
+ info "Starting Vault on port $VAULT_PORT..."
+ docker rm -f "$VAULT_CONTAINER" 2>/dev/null || true
+
+ docker run --rm --detach --name "$VAULT_CONTAINER" --cap-add=IPC_LOCK \
+ -e 'VAULT_LOCAL_CONFIG={"storage": {"file": {"path": "/vault/file"}}, "listener": [{"tcp": { "address": "0.0.0.0:8200", "tls_disable": true}}], "default_lease_ttl": "168h", "max_lease_ttl": "720h", "ui": true}' \
+ -p "$VAULT_PORT:8200" hashicorp/vault:1.20.2 server >/dev/null 2>&1 || die "Failed to start vault"
+
+ echo "Waiting for vault..."
+ sleep 2
+ until curl -s "$VAULT_ADDR/v1/sys/health" >/dev/null 2>&1; do sleep 1; done
+
+ info "Initializing vault..."
+ INIT=$(docker exec "$VAULT_CONTAINER" sh -c "export VAULT_ADDR=http://127.0.0.1:8200; vault operator init -key-shares=1 -key-threshold=1 -format=json")
+ UNSEAL_KEY=$(echo "$INIT" | jq -r ".unseal_keys_b64[0]")
+ ROOT_TOKEN=$(echo "$INIT" | jq -r ".root_token")
+ (umask 077 && echo "$ROOT_TOKEN" > "$TOKEN_FILE")
+
+ info "Configuring vault secrets..."
+ docker exec "$VAULT_CONTAINER" sh -c "
+ export VAULT_ADDR=http://127.0.0.1:8200
+ vault operator unseal \"$UNSEAL_KEY\"
+ vault login \"$ROOT_TOKEN\"
+ vault secrets enable -path=secrets -version=2 kv
+ echo '{\"UsernamePassword\": {\"username\": \"root\", \"password\": \"vault-password\" }}' | vault kv put /secrets/machines/bmc/site/root -
+ echo '{\"UsernamePassword\": {\"username\": \"root\", \"password\": \"vault-password\" }}' | vault kv put /secrets/machines/all_dpus/site_default/uefi-metadata-items/auth -
+ echo '{\"UsernamePassword\": {\"username\": \"root\", \"password\": \"vault-password\" }}' | vault kv put /secrets/machines/all_hosts/site_default/uefi-metadata-items/auth -
+ vault secrets enable -path=certs pki
+ vault write certs/root/generate/internal common_name=myvault.com ttl=87600h
+ vault write certs/config/urls issuing_certificates=\"http://vault.example.com:8200/v1/pki/ca\" crl_distribution_points=\"http://vault.example.com:8200/v1/pki/crl\"
+ vault write certs/roles/role allowed_domains=example.com allow_subdomains=true max_ttl=72h require_cn=false allowed_uri_sans=\"spiffe://forge.local/*\"
+ " >/dev/null 2>&1
+
+ ok "Vault initialized at $VAULT_ADDR"
+fi
+
+# -----------------------------------------------------------------------------
+# TLS certificates
+# Runs unconditionally; gen-certs.sh is idempotent (skips files that exist
+# and are newer than their signing key), so this is fast on subsequent runs.
+# -----------------------------------------------------------------------------
+info "Ensuring TLS certificates are up to date..."
+(cd "$REPO_ROOT/dev/certs/localhost" && ./gen-certs.sh) >/dev/null 2>&1
+ok "TLS certificates ready"
+
+# -----------------------------------------------------------------------------
+# Start Postgres
+# -----------------------------------------------------------------------------
+if docker ps --format '{{.Names}}' | grep -w "$PG_CONTAINER" >/dev/null; then
+ ok "Postgres already running"
+else
+ CERTS_DIR="$REPO_ROOT/dev/certs/localhost"
+ info "Starting Postgres..."
+ docker run --rm --detach --name "$PG_CONTAINER" \
+ -e POSTGRES_PASSWORD="admin" \
+ -e POSTGRES_HOST_AUTH_METHOD=trust \
+ -v "$CERTS_DIR/localhost.crt:/var/lib/postgresql/server.crt:ro" \
+ -v "$CERTS_DIR/localhost.key:/var/lib/postgresql/server.key:ro" \
+ -p 5432:5432 \
+ postgres:14.5-alpine \
+ -c ssl=on \
+ -c ssl_cert_file=/var/lib/postgresql/server.crt \
+ -c ssl_key_file=/var/lib/postgresql/server.key \
+ -c max_connections=300 >/dev/null 2>&1 || die "Failed to start postgres"
+
+ sleep 2
+ ok "Postgres started"
+fi
+
+# -----------------------------------------------------------------------------
+# Environment
+# -----------------------------------------------------------------------------
+export CARBIDE_WEB_AUTH_TYPE="${CARBIDE_WEB_AUTH_TYPE:-basic}"
+export DATABASE_URL="postgresql://postgres:admin@localhost"
+export VAULT_ADDR="$VAULT_ADDR"
+export VAULT_KV_MOUNT_LOCATION="secrets"
+export VAULT_PKI_MOUNT_LOCATION="certs"
+export VAULT_PKI_ROLE_NAME="role"
+export VAULT_TOKEN="$(cat "$TOKEN_FILE")"
+
+# -----------------------------------------------------------------------------
+# Firmware directory (carbide expects this)
+# -----------------------------------------------------------------------------
+if [ ! -d /opt/carbide/firmware ]; then
+ info "Creating /opt/carbide/firmware (may prompt for password)..."
+ sudo mkdir -p /opt/carbide/firmware
+fi
+
+# -----------------------------------------------------------------------------
+# Generate a resolved config with absolute TLS paths
+#
+# carbide-api opens TLS paths relative to the process working directory, not
+# relative to the config file. The checked-in config uses relative paths
+# (e.g. "dev/certs/…") which only work when CWD == repo root. When launched
+# from an IDE or any other directory the cert load will silently fail.
+# We rewrite those paths to absolute ones in a throwaway /tmp copy so the
+# binary is always given correct paths regardless of CWD.
+# -----------------------------------------------------------------------------
+CARBIDE_TMP_CONFIG="/tmp/carbide-api-config-$$.toml"
+sed "s|= \"dev/|= \"$REPO_ROOT/dev/|g" \
+ "$REPO_ROOT/dev/mac-local-dev/carbide-api-config.toml" > "$CARBIDE_TMP_CONFIG"
+ok "Resolved config written to $CARBIDE_TMP_CONFIG"
+
+# -----------------------------------------------------------------------------
+# Migrations & Run
+# -----------------------------------------------------------------------------
+echo ""
+echo "=== Running migrations ==="
+cargo run --package carbide-api --no-default-features migrate || die "Database migrations failed; fix the issue above and re-run this script."
+
+echo ""
+echo "=== Starting Carbide API ==="
+info "TPM/attestation features are not supported on Mac (requires Linux + TPM)."
+echo " All other functionality is available."
+echo ""
+echo " Web UI: https://localhost:1079/admin"
+echo " gRPC: grpcurl -insecure localhost:1079 list"
+echo ""
+
+exec env RUST_BACKTRACE=1 cargo run --package carbide-api --no-default-features -- run \
+ --config-path "$CARBIDE_TMP_CONFIG"
diff --git a/dev/mac-local-dev/set-env.sh b/dev/mac-local-dev/set-env.sh
deleted file mode 100755
index 80e6cc77c4..0000000000
--- a/dev/mac-local-dev/set-env.sh
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/bin/bash
-#
-# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
-# SPDX-License-Identifier: Apache-2.0
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-# set-env.sh
-#
-# Generate customized config and display environment variables needed to run a native carbide-api on MacOS.
-#
-
-set -e
-
-CUR_DIR="$(pwd)"
-
-# customize config to point to local certificates:
-CUSTOM_CONFIG="dev/mac-local-dev/carbide-api-config-custom.toml"
-sed -e 's|/.*carbide/dev|'"$CUR_DIR"'/dev|' < dev/mac-local-dev/carbide-api-config.toml > "${CUSTOM_CONFIG}"
-
-export DATABASE_URL="postgresql://postgres:admin@localhost"
-
-export CARBIDE_WEB_AUTH_TYPE=oauth2
-export CARBIDE_WEB_OAUTH2_CLIENT_SECRET=${CARBIDE_WEB_OAUTH2_CLIENT_SECRET:unset}
-export CARBIDE_WEB_PRIVATE_COOKIEJAR_KEY="$(openssl rand -base64 64)"
-
-export VAULT_ADDR="http://localhost:8200"
-export VAULT_KV_MOUNT_LOCATION="secrets"
-export VAULT_PKI_MOUNT_LOCATION="certs"
-export VAULT_PKI_ROLE_NAME="role"
-export VAULT_TOKEN="$(cat /tmp/localdev-docker-vault-root-token)"
-
-echo "# required variables to run carbide-api:"
-printenv | grep -e '^VAULT_' -e '^CARBIDE_' -e DATABASE_URL | sed -e 's/^/export /'
-echo ""
-echo "# variables on a single line to feed IntelliJ run configuration:"
-printenv | grep -e '^VAULT_' -e '^CARBIDE_' | sed -e 's/$/;/' | tr -d '\n' | sed -e 's/;$//'
-echo
-