diff --git a/docs/client_spec_auth.md b/docs/client_spec_auth.md new file mode 100644 index 0000000..408d293 --- /dev/null +++ b/docs/client_spec_auth.md @@ -0,0 +1,162 @@ +# Client Authentication and Provisioning Specification + +This document defines the interface and protocol flow for the client application interacting with the bootstrap authentication server. It serves as a guide for implementing any client, particularly the dual mode bash script client. + +## Core Concepts + +The bootstrapping process establishes trust between a new client device, an existing administrator device, and the authentication server. The authentication server uses Ed25519 public key cryptography for authentication and `age` for secure payload encryption. + +### Dual Mode Client + +The client application operates in one of two modes: +1. **Requester Mode**: Used by a new, unprovisioned device to request access and receive secrets. +2. **Approver Mode**: Used by an already provisioned administrator device to authorize pending requests. + +--- + +## The Authentication and Provisioning Flow + +The complete flow consists of five sequential phases. + +### Phase 1: Administrator Bootstrapping +1. The server starts up. If the `ADMIN_PUBLIC_KEY` environment variable is set, the server automatically registers and approves this public key as an administrator device in the database. +2. The administrator client on the admin machine is configured to use the corresponding private key. + +### Phase 2: Client Request Initiation (Requester Mode) +1. The new device runs the client in requester mode: + ```bash + b me (optional: --server ) [default auth server is https://b.adityagupta.dev/auth] + ``` +2. The client script generates an Ed25519 key pair locally. +3. The client sends a `POST /api/register` request containing its generated public key. +4. The server registers the device in a `pending` state and returns: + - A short, human readable `user_code` (e.g. 4 to 8 characters). + - A unique `device_id`. +5. The client script displays the `user_code` to the operator and begins polling the challenge endpoint: + ``` + GET /api/challenge/poll?device_id= + ``` + +### Phase 3: Administrator Approval (Approver Mode) +1. The operator reads the `user_code` from the requesting device's terminal. +2. On the administrator device, the operator runs the client in approver mode: + ```bash + b trust [--server ] + ``` +3. The administrator client queries the server to retrieve the pending registration details: + ``` + GET /api/pending/ + ``` +4. The server returns the pending `device_id` and the requester's `public_key`. +5. The administrator client displays these details. The operator confirms the request. +6. The administrator client signs a payload containing the requester's `device_id` and the approval action using its Ed25519 administrator private key. +7. The administrator client submits the signature and its public key to the server: + ``` + POST /api/approve + ``` +8. The server verifies the administrator's signature against the registered administrator public keys. If valid, the server transitions the requester device state from `pending` to `approved`. + +### Phase 4: Payload Provisioning +1. Once the requester device is approved, the server generates the secret payload (or retrieves it from a secure source). +2. The server encrypts the payload using the requester's public key via `age`. +3. The server stores this encrypted payload as a challenge response. + +### Phase 5: Challenge Completion and Retrieval +1. The next poll from the requester client to `GET /api/challenge/poll?device_id=` succeeds. +2. The server returns the `age` encrypted payload. +3. The requester client decrypts the payload using its local private key. +4. The secrets are successfully provisioned. + +--- + +## API Endpoints Reference + +### 1. Register Device +- **Endpoint**: `POST /api/register` +- **Request Body**: + ```json + { + "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5..." + } + ``` +- **Response Body (200 OK)**: + ```json + { + "device_id": "uuid-string", + "user_code": "A3F9K2" + } + ``` + +### 2. Get Pending Device Details +- **Endpoint**: `GET /api/pending/` +- **Response Body (200 OK)**: + ```json + { + "device_id": "uuid-string", + "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5..." + } + ``` + +### 3. Approve Device +- **Endpoint**: `POST /api/approve` +- **Request Body**: + ```json + { + "device_id": "uuid-string", + "admin_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...", + "signature": "hex-encoded-signature-bytes" + } + ``` +- **Response Body (200 OK)**: Empty or success confirmation. + +### 4. Poll Challenge +- **Endpoint**: `GET /api/challenge/poll?device_id=` +- **Response Body (200 OK - Pending)**: + ```json + { + "status": "pending" + } + ``` +- **Response Body (200 OK - Approved & Encrypted)**: + ```json + { + "status": "approved", + "payload": "-----BEGIN AGE ENCRYPTED FILE-----\n..." + } + ``` + +--- + +## Bash Client Commands and Usage + + +### Global Dependencies +- `curl` or `wget` for making HTTP requests. +- `jq` for parsing JSON payloads. +- `ssh-keygen` for Ed25519 key generation and signing. +- `age` and `age-keygen` for decrypting the final payload. + +### Command Structure + +#### 1. Device Registration Request +Generates local keys, registers with the server, and polls for the encrypted secret payload. +```bash +b me \ + --server \ + [--key-dir ] \ + [--poll-interval ] +``` +- `--server`: Base URL of the bootstrap authentication server. +- `--key-dir`: Directory where the new Ed25519 and age keys will be saved (defaults to `~/.config/bootstrap-client/`). +- `--poll-interval`: Frequency of polling in seconds (defaults to `5`). + +#### 2. Request Approval +Retrieves a pending request by its user code, requests user confirmation, signs the approval payload, and sends it to the server. +```bash +b trust \ + --server \ + --admin-key +``` +- `--code`: The short human readable code displayed on the requesting device. +- `--server`: Base URL of the bootstrap authentication server. +- `--admin-key`: Path to the administrator's private key used to sign the approval. diff --git a/lib/plugins.sh b/lib/plugins.sh index fcad34a..61fe9f2 100644 --- a/lib/plugins.sh +++ b/lib/plugins.sh @@ -139,20 +139,25 @@ run_plugin() { return 1 fi - local local_plugin - if [ "$is_ephemeral" = "true" ]; then - log_info "Downloading plugin '$plugin_name' (ephemeral)..." - local_plugin=$(mktemp --suffix=".sh" 2>/dev/null || mktemp) - if ! curl -fsSL "$url" -o "$local_plugin"; then + log_info "Downloading and running plugin '$plugin_name' (ephemeral)..." + local script_content + if ! script_content=$(curl -fsSL "$url"); then log_error "Failed to download plugin '$plugin_name' from $url" - rm -f "$local_plugin" return 1 fi - chmod +x "$local_plugin" + + # Execute the plugin directly in memory in a subshell + ( + export BOOTSTRAP_DIR + # We use bash -c and pass the script content to keep stdin free for interactive plugins + # The "$0" arg for bash -c is set to the plugin name + bash -c "$script_content" "$plugin_name" "${cmd_args[@]}" + ) + return $? else local plugin_dir="$BOOTSTRAP_DIR/plugins" - local_plugin="$plugin_dir/${plugin_name}.sh" + local local_plugin="$plugin_dir/${plugin_name}.sh" if [ ! -f "$local_plugin" ]; then log_info "Downloading plugin '$plugin_name'..." @@ -164,20 +169,13 @@ run_plugin() { fi chmod +x "$local_plugin" fi + + log_info "Running plugin '$plugin_name'..." + # Execute the plugin in a subshell, passing any additional arguments + ( + export BOOTSTRAP_DIR + bash "$local_plugin" "${cmd_args[@]}" + ) + return $? fi - - log_info "Running plugin '$plugin_name'..." - # Execute the plugin in a subshell, passing any additional arguments - ( - export BOOTSTRAP_DIR - bash "$local_plugin" "${cmd_args[@]}" - ) - local ret=$? - - if [ "$is_ephemeral" = "true" ]; then - rm -f "$local_plugin" - fi - - return $ret } - diff --git a/readme.md b/readme.md index 72bc346..313c756 100644 --- a/readme.md +++ b/readme.md @@ -109,7 +109,7 @@ b up b up --force ``` -### Plugins (`b `) +## Plugins (`b `) Plugins are first-party or third-party applications written to work directly with `bootstrap`. Unlike installers (or packages) which modify your system by compiling code, downloading binaries, and altering shell configuration files, **plugins are lazy-loaded scripts that execute within a subshell**. @@ -131,7 +131,7 @@ b my_plugin Plugins are automatically checked for updates and lazily re-downloaded whenever you run `b up`. -If you prefer to run a plugin strictly in **ephemeral mode** (meaning it will bypass the cache, download to a temporary location, execute, and then delete itself to save space and guarantee the absolute latest version), simply pass the `-e` or `--ephemeral` flag: +If you prefer to run a plugin strictly in **ephemeral mode** (meaning it will bypass the cache and execute directly in memory to guarantee the absolute latest version without leaving any footprint), simply pass the `-e` or `--ephemeral` flag: ```bash b my_plugin -e