Class CommandExternalTokenProvider

java.lang.Object
co.ankatech.ankasecure.sdk.auth.CommandExternalTokenProvider
All Implemented Interfaces:
ExternalTokenProvider

public final class CommandExternalTokenProvider extends Object implements ExternalTokenProvider
Runs a command the operator configured and takes its standard output as the external token.

This is the escape hatch for every credential shape nobody standardised: a vendor CLI that already knows how to authenticate, a hardware-backed agent, a site-specific broker. It is also, unavoidably, arbitrary code execution with the calling process's privileges, and it is built accordingly.

Five controls, and why each one is here

  1. An environment-only opt-in. ANKASECURE_ALLOW_EXTERNAL_TOKEN_COMMAND must be exactly 1 in the process environment. It is deliberately NOT readable from cli.properties or any other configuration file, so a configuration file alone can never cause this process to spawn another one. Google gates executable-sourced credentials the same way and for the same reason, with the same environment-only rule; CVE-2026-66902 is what an implementation of the same feature looks like when the gate is missing.
  2. No shell. The configured command is tokenized here into an argv array and handed to ProcessBuilder directly. There is no sh -c and no cmd /c, so a semicolon, a backtick or a pipe character in a configuration value is an argument, not a second command.
  3. A bounded timeout that is always applied. A command that hangs would otherwise hang the calling workload with no diagnosis at all; the process is destroyed forcibly and the failure names the command and the elapsed budget.
  4. Bounded output. Standard output is read up to the same ceiling any token must satisfy, and standard error is drained concurrently and discarded so a chatty command cannot deadlock on a full pipe buffer.
  5. Nothing the server returns reaches the command. The argv is fixed at construction and is immutable thereafter. This is the containment property: the SDK executes what the operator configured and can execute nothing else.

The contract with the command

Print the token to standard output and exit 0. Nothing else is parsed: no JSON envelope, no version field, no expiry. An expiry would have to be believed, and the SDK does not read the external token's claims - the token is opaque to it, deliberately. The lifetime that governs caching is the one on the ANKASecure token the platform issues in exchange, which the platform states outright.

Standard error is not surfaced, and that is a decision

A failure reports the command and the exit status but never the text the command wrote to standard error. Surfacing it is the usual practice and it is genuinely more convenient, but the SDK cannot know that a given command does not echo the credential it just minted, and the rule that secrets are never logged is worth more than a better error message. An operator diagnosing a failing command should run it themselves, where its output is theirs to read.

The whole argv is named, not just the executable

Interpreter-plus-script is the ordinary shape of this setting - bash get-token.sh, a vendor CLI with a subcommand, a broker with a profile name - so naming only argv[0] tells the operator the interpreter is broken when the missing artifact is the script. Every element of the argv is reported instead.

That is not a relaxation of the rule above. The argv is what the operator wrote into their own configuration and what this process hands to ProcessBuilder; the operating system's own process listing already shows it verbatim. Standard error is the command's OUTPUT, which the SDK has no way to vet - the distinction is between the configuration and what running it produced, and only the second one can carry a minted credential.

  • Field Details

    • ALLOW_ENV_VARIABLE

      public static final String ALLOW_ENV_VARIABLE
      The environment variable that must be set to 1 before any command will be executed. Environment only: it is never read from a properties file.
      See Also:
    • ALLOW_ENV_VALUE

      public static final String ALLOW_ENV_VALUE
      The one value that enables execution. Anything else - including true - does not.
      See Also:
    • DEFAULT_TIMEOUT

      public static final Duration DEFAULT_TIMEOUT
      Applied when the configuration names no timeout.
    • MIN_TIMEOUT

      public static final Duration MIN_TIMEOUT
      A configured timeout below this is raised to it; a command needs some room to start.
    • MAX_TIMEOUT

      public static final Duration MAX_TIMEOUT
      A configured timeout above this is lowered to it.
  • Constructor Details

  • Method Details

    • tokenize

      public static List<String> tokenize(String commandLine)
      Splits a configured command line into an argv array WITHOUT involving a shell.

      Whitespace separates arguments; a double-quoted run is one argument and may contain whitespace. Nothing else is interpreted - no variable expansion, no globbing, no operators - because every one of those is a way for a configuration value to mean more than it says.

      Parameters:
      commandLine - the configured command line
      Returns:
      the argv; never empty
      Throws:
      NullPointerException - if commandLine is null
      IllegalArgumentException - if the line is blank or has an unterminated quote
    • executionAllowed

      public static boolean executionAllowed()
      True when the process environment carries the opt-in.

      Reads System.getenv(java.lang.String) directly and takes no argument a caller could substitute: the gate is a property of the process, and a gate that can be handed its own answer is a gate with an override.

      Returns:
      true if commands may be executed
    • fetchExternalToken

      public SecretChars fetchExternalToken()
      Description copied from interface: ExternalTokenProvider
      Produces the current external token.

      How current it is belongs to the implementation, and the two answers are both correct for their case. A provider that READS an out-of-band token re-reads on every call, because something else refreshes it and a copy held here is a copy that expires without anyone noticing. A provider that OBTAINS the token itself knows the lifetime its issuer stated for it, so it may reuse one inside that lifetime - and when the issuer states no lifetime, it falls back to fetching per call rather than guessing one.

      Specified by:
      fetchExternalToken in interface ExternalTokenProvider
      Returns:
      the token; the caller takes ownership and closes it
    • kind

      public ExternalTokenSourceKind kind()
      Description copied from interface: ExternalTokenProvider
      Which of the configured source kinds this provider is, or null for a consumer-supplied provider that is none of them.
      Specified by:
      kind in interface ExternalTokenProvider
      Returns:
      the kind, or null when the provider is the consumer's own
    • describe

      public String describe()
      Description copied from interface: ExternalTokenProvider
      A short operator-facing description of WHERE the token comes from - a path, a command name, an issuer - for diagnostics. It must never contain the token.
      Specified by:
      describe in interface ExternalTokenProvider
      Returns:
      the description; never null
    • argv

      public List<String> argv()
      The exact argv this provider will execute. Exposed so a containment check can assert that what runs is what was configured.
      Returns:
      an immutable copy of the argv; never empty
    • timeout

      public Duration timeout()
      The bounded budget actually in force after clamping.
      Returns:
      the timeout; never null
    • toString

      public String toString()
      Names the configured command and the budget; carries nothing the command produced.
      Overrides:
      toString in class Object