Skip to content

Feature: stdin and env-var input for auth service-account set --key #600

@alexminza

Description

@alexminza

What

Let gog auth service-account set --key=... accept the service-account JSON key from stdin or an environment variable, the same way gog auth import already accepts the refresh token (--refresh-token-stdin / --refresh-token-file / --refresh-token-env) and gog auth tokens import already accepts - for stdin. Today service-account set is the only credential-intake path that's file-path-only — orchestrators that hold the SA key in memory (the natural shape for non-interactive deployment) must write it to a tempfile, manage cleanup with trap, and worry about the on-disk exposure surface between write and use.

Why

Today's intake shape

Three credential-intake commands, three different input contracts:

Command stdin env var file
gog auth import --refresh-token-stdin --refresh-token-env --refresh-token-file
gog auth tokens import <inPath> <inPath> = - <inPath> (positional)
gog auth service-account set --key=<path> --key=<path>
gog auth credentials set <path> <path> = - <path> (positional)

The pattern is otherwise consistent — every other intake path supports stdin. service-account set is the outlier.

Verified at internal/cmd/auth_service_account.go:

type AuthServiceAccountSetCmd struct {
    Email string `arg:"" name:"email" required:""`
    Key   string `name:"key" required:"" help:"Path to service account JSON key file"`
}

func (c *AuthServiceAccountSetCmd) Run(ctx context.Context, flags *RootFlags) error {
    // ...
    keyPath := strings.TrimSpace(c.Key)
    keyPath, err := config.ExpandPath(keyPath)
    // ...
    data, err := os.ReadFile(keyPath)  // file-only

No - fallback for stdin, no --key-env, no --key-stdin.

Project-side cost

Service-account workspace domain-wide-delegation is the natural auth shape for headless workspace automation (CI jobs, agent runs, server-side renderers). The orchestrator typically holds the SA key in memory — pulled from a secret store, decrypted from a database column, passed via env from a deployment system. With --key=<path>-only:

  • Tempfile dance is forced. mktemp, trap 'rm -f' EXIT, write the JSON, hope the process didn't crash between write and cleanup, hope no other reader picked up the file in that window.
  • On-disk exposure surface is non-zero. The tempfile sits at 0600 in $TMPDIR for the duration of the call. Image leaks, crash dumps, monitoring agents that snapshot /tmp all capture it.
  • Argv hygiene constraint partially defeated. auth import was carefully designed to keep tokens out of /proc/<pid>/cmdline. service-account set lets you pass a path, but the content still has to land somewhere on disk first. Stdin / env routing closes that gap.
  • API consistency. Operators wiring deployment automation across multiple auth subcommands hit one "why is this one different" cliff. Predictable shapes lower the bar to correct, secure wiring.

Precedent already in this repo

auth import shipped these three input modes specifically for non-interactive deployment (release notes for v0.17.0):

Auth: add gog auth import --client --email with --refresh-token-stdin, --refresh-token-file, or --refresh-token-env for non-interactive token import without exposing secrets in argv

The same rationale applies one-for-one to service-account set. Same threat model, same intake shape, same desired ergonomics.

Suggested shape

Mirror auth import's flag set on service-account set:

# Today (still supported)
gog auth service-account set --key=/path/to/sa.json admin@example.com

# New: stdin
printf '%s' "$SA_KEY_JSON" | gog auth service-account set --key=- admin@example.com

# New: file (already supported; explicit naming for symmetry)
gog auth service-account set --key=/path/to/sa.json admin@example.com

# New: env
SA_KEY_JSON="$(read-from-vault)" gog auth service-account set --key-env=SA_KEY_JSON admin@example.com

Three possible flag patterns to consider:

Option A: extend --key to accept - for stdin (like auth credentials set):

... --key=- admin@example.com

Option B: separate flags mirroring auth import:

... --key-stdin admin@example.com
... --key-file=<path> admin@example.com
... --key-env=<VAR> admin@example.com

Option C: both--key=- works (back-compat extension), the explicit --key-stdin / --key-env aliases for parity with auth import.

Option B is the cleanest mirror of auth import — exactly one source, named explicitly, exclusive of the others (auth_import.resolveRefreshToken validates "exactly one source" — same validation should apply here). Option A is the smallest change but groups three distinct concepts under one flag.

Maintainer's call. The load-bearing part is that stdin and env modes both exist, with the same argv-clean / no-tempfile guarantees auth import already provides.

Validation

Borrow the existing pattern from auth_import.go resolveRefreshToken:

  • Exactly one source must be specified; conflicts return a usage error.
  • Empty input is a parse error (mirrors errMissingRefreshToken).
  • Existing parser parseServiceAccountJSON runs unchanged on the assembled []byte.

Scope

  • internal/cmd/auth_service_account.go: extend AuthServiceAccountSetCmd with the new flags + a resolveServiceAccountKey() helper that mirrors resolveRefreshToken()'s shape.
  • No changes to storage shape — the parsed SA JSON still lands at $XDG_CONFIG_HOME/gogcli/service-accounts/<email>.json via config.WriteFileAtomic.
  • No changes to consumer code paths (tokenSourceForServiceAccountScopes etc.).

Out of scope

  • Moving the SA JSON itself to the keyring. The SA key is bulkier (~2KB JSON) than a refresh token and has a different threat model (single-tenant, infrequently rotated). Worth a separate proposal — file later if a deployment pattern hits the on-disk concern hard enough.
  • Dropping the --key=<path> mode. Tooling that downloads SA keys from Google Cloud Console naturally produces files; existing deployments rely on this. Pure addition, no removal.

Context

  • Sibling commands gog auth import (v0.17.0) and gog auth tokens import (- for stdin already supported) demonstrate @steipete already accepts this intake-shape pattern in this repo.
  • Part of a small cluster of credential-handling proposals filed in the same week: #596 (move client_secret to keyring), #597 (cross-process locking around keyring writes), #598 (access-token seeding in auth import), #599 (${VAR} interpolation in credentials.json). Independent of all four — this issue only touches auth service-account set's intake flags, no shared code surface — but completes the set of "non-interactive intake parity across auth subcommands."
  • No existing issue, PR, or discussion proposes this — searched for service-account set stdin, --key-env, --key-stdin, service account input; discussions are disabled on this repo. Filing as a clean new ask.

References

All references pinned to v0.17.0:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions