feat(cli): read secret-bearing option values from a file or stdin (#137)
Every CLI option that can carry a secret gets a `--<name>-file <path|->`
sibling ('-' reads stdin), so the value stays out of the process list and
shell history: --password-file (global), --primary-config-file /
--backup-config-file (data-connection create|update), --auth-config-file
(external-system create|update), --connection-string-file (db-connection
create|update), --credentials-file (notification smtp create|update) and
--auth-token-file (notification sms create|update). Follows the existing
--config-file convention; `@path` is avoided because System.CommandLine
expands @-tokens as response files.
The value is read at parse time; a missing/unreadable/empty source, or '-'
on a terminal stdin, is a parse error naming option + path only, never the
content. Inline and file forms are mutually exclusive, at most one option
may read stdin, and one trailing line break is stripped. Inline forms keep
working.
docker/seed-sites.sh and docker-env2/seed-sites.sh now write the MxGateway
config (windev gateway key) to a mode-600 temp file removed by an EXIT trap
and pass --primary-config-file; seed-flow-outbound-demo.sh pipes its rig
connection string via --connection-string-file -. CLI README (new "Secrets
from a file or stdin" section + per-command tables), Component-CLI.md and
both docker READMEs updated.
Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -27,7 +27,7 @@ Standalone executable, not part of the Host binary. Deployed on any machine with
|
||||
|
||||
The CLI authenticates with a bearer token exchanged once per process (review 2026-10-21 05/U1, 05/P1):
|
||||
|
||||
1. The user provides credentials via `--username` / `--password` (or `SCADABRIDGE_USERNAME` / `SCADABRIDGE_PASSWORD`).
|
||||
1. The user provides credentials via `--username` / `--password` or `--password-file <path|->` (or `SCADABRIDGE_USERNAME` / `SCADABRIDGE_PASSWORD`).
|
||||
2. On the first management call of the process, the CLI posts the credentials to `POST /auth/token`. The server performs the LDAP bind, group lookup and role resolution, and returns a short-lived HS256 JWT carrying the username, roles and permitted sites (lifetime `JwtExpiryMinutes`, default 15 min). That is the invocation's **only** LDAP bind.
|
||||
3. Every later call in the process — `POST /management`, the audit REST endpoints, and the `debug stream` hub (including its automatic reconnects) — sends `Authorization: Bearer <token>`; the server validates it locally with no LDAP bind. A token near expiry is re-exchanged.
|
||||
4. A bearer call answered `401` re-exchanges once (unless the token was minted for that very call) and, if still refused, retries with HTTP Basic and stays on Basic for the rest of the process.
|
||||
@@ -184,8 +184,8 @@ scadabridge deploy status [--instance-id <id>] [--status <status>] [--page <n>]
|
||||
```
|
||||
scadabridge data-connection list [--site-id <id>]
|
||||
scadabridge data-connection get --id <id>
|
||||
scadabridge data-connection create --site-id <id> --name <name> --protocol <protocol> [--primary-config <json>] [--backup-config <json>] [--failover-retry-count <n>]
|
||||
scadabridge data-connection update --id <id> --name <name> --protocol <protocol> [--primary-config <json>] [--backup-config <json>] [--failover-retry-count <n>]
|
||||
scadabridge data-connection create --site-id <id> --name <name> --protocol <protocol> [--primary-config <json> | --primary-config-file <path|->] [--backup-config <json> | --backup-config-file <path|->] [--failover-retry-count <n>]
|
||||
scadabridge data-connection update --id <id> --name <name> --protocol <protocol> [--primary-config <json> | --primary-config-file <path|->] [--backup-config <json> | --backup-config-file <path|->] [--failover-retry-count <n>]
|
||||
scadabridge data-connection delete --id <id>
|
||||
scadabridge data-connection move --id <id> --site-id <destination-site-id>
|
||||
```
|
||||
@@ -211,8 +211,8 @@ scadabridge data-connection certs remove --site <identifier> --thumbprint <hex>
|
||||
```
|
||||
scadabridge external-system list
|
||||
scadabridge external-system get --id <id>
|
||||
scadabridge external-system create --name <name> --endpoint-url <url> --auth-type <type> [--auth-config <json>] [--timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
|
||||
scadabridge external-system update --id <id> [--name <name>] [--endpoint-url <url>] [--auth-type <type>] [--auth-config <json>] [--timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
|
||||
scadabridge external-system create --name <name> --endpoint-url <url> --auth-type <type> [--auth-config <json> | --auth-config-file <path|->] [--timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
|
||||
scadabridge external-system update --id <id> [--name <name>] [--endpoint-url <url>] [--auth-type <type>] [--auth-config <json> | --auth-config-file <path|->] [--timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
|
||||
scadabridge external-system delete --id <id>
|
||||
scadabridge external-system method list --external-system-id <id>
|
||||
scadabridge external-system method get --id <id>
|
||||
@@ -234,7 +234,7 @@ scadabridge notification recipient add --list-id <id> --name <name> [--email <em
|
||||
scadabridge notification recipient delete --id <recipient-id>
|
||||
scadabridge notification delete --id <id>
|
||||
scadabridge notification smtp list
|
||||
scadabridge notification smtp update --id <id> --server <host> --port <n> --auth-mode <mode> --from-address <email> [--tls-mode <mode>] [--credentials <value>]
|
||||
scadabridge notification smtp update --id <id> --server <host> --port <n> --auth-mode <mode> --from-address <email> [--tls-mode <mode>] [--credentials <value> | --credentials-file <path|->]
|
||||
```
|
||||
|
||||
Notification rename and individual recipient edits preserve unrelated IDs and
|
||||
@@ -242,10 +242,10 @@ names. Whole-list update replaces recipients. Recipient add requires at least
|
||||
one contact; the server validates it against the owning list channel.
|
||||
|
||||
```sh
|
||||
scadabridge notification smtp create --server <host> --port <n> --auth-mode <mode> --from-address <email> [--tls-mode <mode>] [--credentials <value>] [--oauth2-authority <url>] [--oauth2-scope <scope>] [--transport <Smtp|Ews>]
|
||||
scadabridge notification smtp create --server <host> --port <n> --auth-mode <mode> --from-address <email> [--tls-mode <mode>] [--credentials <value> | --credentials-file <path|->] [--oauth2-authority <url>] [--oauth2-scope <scope>] [--transport <Smtp|Ews>]
|
||||
scadabridge notification sms list
|
||||
scadabridge notification sms create --account-sid <sid> --auth-token <token> [--from-number <number>] [--messaging-service-sid <sid>] [--api-base-url <url>] [--connection-timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
|
||||
scadabridge notification sms update --id <id> --account-sid <sid> [--from-number <number>] [--messaging-service-sid <sid>] [--api-base-url <url>] [--auth-token <token>] [--connection-timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
|
||||
scadabridge notification sms create --account-sid <sid> (--auth-token <token> | --auth-token-file <path|->) [--from-number <number>] [--messaging-service-sid <sid>] [--api-base-url <url>] [--connection-timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
|
||||
scadabridge notification sms update --id <id> --account-sid <sid> [--from-number <number>] [--messaging-service-sid <sid>] [--api-base-url <url>] [--auth-token <token> | --auth-token-file <path|->] [--connection-timeout-seconds <n>] [--max-retries <n>] [--retry-delay-seconds <n>]
|
||||
```
|
||||
|
||||
SMS requires at least one sender: a From number or a Messaging Service SID.
|
||||
@@ -432,8 +432,8 @@ scadabridge shared-script delete --id <id>
|
||||
```
|
||||
scadabridge db-connection list
|
||||
scadabridge db-connection get --id <id>
|
||||
scadabridge db-connection create --name <name> --connection-string <string>
|
||||
scadabridge db-connection update --id <id> [--name <name>] [--connection-string <string>]
|
||||
scadabridge db-connection create --name <name> (--connection-string <string> | --connection-string-file <path|->)
|
||||
scadabridge db-connection update --id <id> [--name <name>] (--connection-string <string> | --connection-string-file <path|->)
|
||||
scadabridge db-connection delete --id <id>
|
||||
```
|
||||
|
||||
@@ -512,15 +512,35 @@ The `--format json|table` option is recursive and accepted on every management c
|
||||
|
||||
Configuration is resolved in the following priority order (highest wins):
|
||||
|
||||
1. **Command-line options**: `--url`, `--username`, `--password`, `--format`.
|
||||
1. **Command-line options**: `--url`, `--username`, `--password` / `--password-file`, `--format`.
|
||||
2. **Environment variables**:
|
||||
- `SCADABRIDGE_MANAGEMENT_URL` — Management API URL (e.g., `http://central-host:5000`).
|
||||
- `SCADABRIDGE_FORMAT` — Default output format (`json` or `table`).
|
||||
- `SCADABRIDGE_USERNAME` / `SCADABRIDGE_PASSWORD` — LDAP credentials. Preferred over
|
||||
`--password` on the command line, which is visible in process listings and shell
|
||||
history. Credentials are never read from the config file.
|
||||
`--password-file <path|->` is the per-invocation equivalent (see *Secret-bearing options*).
|
||||
3. **Configuration file**: `~/.scadabridge/config.json` — Persistent defaults for management URL and output format only (never credentials).
|
||||
|
||||
### Secret-bearing options (file / stdin)
|
||||
|
||||
Every option that can carry a secret has a `-file` sibling (ScadaBridge#137): `--password-file`
|
||||
(global), `--primary-config-file` / `--backup-config-file` (`data-connection create|update`),
|
||||
`--auth-config-file` (`external-system create|update`), `--connection-string-file`
|
||||
(`db-connection create|update`), `--credentials-file` (`notification smtp create|update`) and
|
||||
`--auth-token-file` (`notification sms create|update`).
|
||||
|
||||
- `--x-file <path>` reads the file; `--x-file -` reads stdin (a file named `-` is `./-`). One
|
||||
trailing line break is stripped.
|
||||
- Inline and file forms of one value are mutually exclusive; at most one option per invocation may
|
||||
read stdin; `-` on a terminal stdin is refused rather than blocking.
|
||||
- The value is read at parse time. A missing, unreadable or empty source is a parse error that names
|
||||
the option and path only — never the content.
|
||||
- `@path` is deliberately not used: System.CommandLine expands `@`-prefixed tokens as response files.
|
||||
|
||||
The inline forms remain for non-secret use. Seed scripts in `docker/` and `docker-env2/` use the
|
||||
file/stdin forms for any value that carries a key or password.
|
||||
|
||||
### Configuration File Format
|
||||
|
||||
```json
|
||||
|
||||
Reference in New Issue
Block a user