> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.itential.com/itential-gateway/5/secrets/external-secrets/configure-custom-plugin-provider/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.itential.com/_mcp/server. # Configure a custom plugin provider > Use the Itential Gateway plugin interface to retrieve secrets from any external system using a customer-supplied executable. Gateway 5.5+ The custom plugin provider lets you connect Itential Gateway to any secrets manager using a customer-supplied executable. Gateway calls the executable as a subprocess using a stable, documented protocol. The executable retrieves the secret using whatever logic is appropriate for your environment. Use a custom plugin when: * You use HashiCorp Vault KV v1 (only KV v2 is supported by the bundled Vault connector) * Your organization uses IAM-based authentication to Vault * You need to integrate with a secrets manager not covered by the bundled providers ## Plugin protocol Gateway invokes your plugin with the `get` subcommand: ```bash /path/to/your-plugin get ``` ### Input Gateway writes a single JSON object to the plugin's stdin: ```json { "path": "secret/my-app/db-password", "key": "password", "config": { "env": { "VAULT_ADDR": "https://vault.example.com", "VAULT_TOKEN_FILE": "/etc/gateway/vault_token" } } } ``` | Field | Description | | ------------ | ---------------------------------------------------------------------------------------------------------- | | `path` | The secret path in the external system | | `key` | Optional. When set, Gateway treats the retrieved value as JSON and extracts this field from it. | | `config.env` | Non-sensitive configuration from provider registration: URLs, file path references. Never raw credentials. | ### Output — success On success, write a JSON object to stdout and exit with code 0: ```json {"value": "the-plaintext-secret-value"} ``` ### Output — failure On failure, write a descriptive error message to stderr, write nothing to stdout, and exit with a non-zero code. Gateway includes your stderr output in the operator-facing error. ## Security model Credentials must never pass through stdin or stdout. Store tokens, certificates, and API keys as files on the gateway server. Reference them using environment variables set at provider registration: ```bash iagctl create secret-provider my-plugin \ --type plugin \ --command /usr/local/bin/my-plugin \ --env VAULT_TOKEN_FILE=/etc/gateway/vault_token \ --env VAULT_ADDR=https://vault.example.com ``` Gateway passes the `config.env` values to the plugin at invocation time. The plugin reads credentials directly from disk—they never travel over the stdin/stdout pipe or appear in Gateway logs. ## Reference implementation — Python (HashiCorp Vault KV v1, token auth) The following example retrieves a secret from HashiCorp Vault KV v1 using a token file. It uses only Python standard library modules. ```python #!/usr/bin/env python3 """ Itential Gateway custom plugin — HashiCorp Vault KV v1 with token file. Invocation: /path/to/plugin get Input: JSON via stdin {"path": "...", "key": "...", "config": {"env": {...}}} Output: JSON via stdout {"value": "..."} on success; non-zero exit + stderr on failure """ import json import os import sys import urllib.request import urllib.error def main(): try: request_data = json.load(sys.stdin) except json.JSONDecodeError as e: print(f"Failed to parse input: {e}", file=sys.stderr) sys.exit(1) path = request_data.get("path") key = request_data.get("key") env_config = request_data.get("config", {}).get("env", {}) vault_addr = env_config.get("VAULT_ADDR") or os.environ.get("VAULT_ADDR") token_file = env_config.get("VAULT_TOKEN_FILE") if not vault_addr: print("VAULT_ADDR is required", file=sys.stderr) sys.exit(1) if not token_file: print("VAULT_TOKEN_FILE is required", file=sys.stderr) sys.exit(1) try: with open(token_file) as f: token = f.read().strip() except OSError as e: print(f"Failed to read token file: {e}", file=sys.stderr) sys.exit(1) url = f"{vault_addr.rstrip('/')}/v1/{path.lstrip('/')}" req = urllib.request.Request(url, headers={"X-Vault-Token": token}) try: with urllib.request.urlopen(req) as resp: data = json.loads(resp.read()) except urllib.error.HTTPError as e: print(f"Vault request failed ({e.code}): {e.reason}", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Failed to retrieve secret: {e}", file=sys.stderr) sys.exit(1) secret_data = data.get("data", {}) if key: if key not in secret_data: print(f"Key '{key}' not found at path '{path}'", file=sys.stderr) sys.exit(1) value = secret_data[key] else: value = json.dumps(secret_data) print(json.dumps({"value": value})) if __name__ == "__main__": main() ``` ## Reference implementation — Go (Azure Key Vault, DefaultAzureCredential) The following example retrieves a secret from Azure Key Vault using the Azure SDK's `DefaultAzureCredential`. It demonstrates that the plugin protocol is language-agnostic and works with any external secrets source, including providers not covered by the bundled connectors. ```go package main import ( "context" "encoding/json" "fmt" "os" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" ) type input struct { Path string `json:"path"` Key string `json:"key"` Config struct { Env map[string]string `json:"env"` } `json:"config"` } func main() { var in input if err := json.NewDecoder(os.Stdin).Decode(&in); err != nil { fmt.Fprintf(os.Stderr, "failed to parse input: %v\n", err) os.Exit(1) } vaultURL := in.Config.Env["AZURE_VAULT_URL"] if vaultURL == "" { fmt.Fprintln(os.Stderr, "AZURE_VAULT_URL is required") os.Exit(1) } cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { fmt.Fprintf(os.Stderr, "failed to create credential: %v\n", err) os.Exit(1) } client, err := azsecrets.NewClient(vaultURL, cred, nil) if err != nil { fmt.Fprintf(os.Stderr, "failed to create Key Vault client: %v\n", err) os.Exit(1) } resp, err := client.GetSecret(context.Background(), in.Path, "", nil) if err != nil { fmt.Fprintf(os.Stderr, "failed to retrieve secret: %v\n", err) os.Exit(1) } if resp.Value == nil { fmt.Fprintf(os.Stderr, "secret value is nil for path: %s\n", in.Path) os.Exit(1) } if err := json.NewEncoder(os.Stdout).Encode(map[string]string{"value": *resp.Value}); err != nil { fmt.Fprintf(os.Stderr, "failed to encode output: %v\n", err) os.Exit(1) } } ``` Register this provider with the vault URL passed as a non-sensitive `--env` value. `DefaultAzureCredential` picks up the Azure identity from the gateway server's environment (managed identity, environment variables, or CLI login): ```bash iagctl create secret-provider my-azure-kv \ --type plugin \ --command /usr/local/bin/azure-kv-plugin \ --env AZURE_VAULT_URL=https://my-vault.vault.azure.net ``` ## Register a custom plugin provider #### Place the plugin on the gateway server Copy your plugin executable to the gateway server and make it executable: ```bash chmod +x /usr/local/bin/my-plugin ``` If you run Itential Gateway in a cluster, every gateway server needs a copy of the plugin at the same path. #### Register the provider Register the provider by specifying the plugin type, the path to the executable, and any non-sensitive configuration as `--env` flags. Each `--env` value is passed to the plugin as `config.env` at invocation time. ```bash iagctl create secret-provider my-vault-plugin \ --type plugin \ --command /usr/local/bin/my-plugin \ --env VAULT_TOKEN_FILE=/etc/gateway/vault_token \ --env VAULT_ADDR=https://vault.example.com ``` #### Verify registration Confirm the provider appears in the list: ```bash iagctl get secret-providers ``` ## Next steps * [Create secret aliases that point to secrets through this provider](./manage-secret-aliases) > Use the Itential Gateway plugin interface to retrieve secrets from any external system using a customer-supplied executable.