Vault AppRole authentication workflow in Python

AppRole is how a machine proves its identity to Vault without a long-lived token sitting in the image. The mechanics matter: the role_id ships with the app, the secret_id arrives at runtime, and the token they yield expires and must be renewed. This page implements the full workflow, extending the HashiCorp Vault Python SDK cluster.

Problem 1: a root token baked into the image

# ANTI-PATTERN: a long-lived token committed with the app
client = hvac.Client(url=URL, token="s.rootTokenInGit")   # never expires, never rotate-able

A token in the image is a permanent credential anyone with the image can extract.

Problem 2: ignoring token TTL

# ANTI-PATTERN: assumes the token is valid forever
client.auth.approle.login(role_id=RID, secret_id=SID)
# ... hours later, the lease has expired and every call returns 403

AppRole tokens have a TTL; long-running workers must re-authenticate before it lapses.

Secure implementation

# secrets/approle.py
import time
import hvac
from pydantic import SecretStr

class VaultSession:
    def __init__(self, url: str, role_id: str, secret_id: SecretStr):
        self._url, self._role_id = url, role_id
        self._secret_id = secret_id           # delivered at runtime, never committed
        self._client: hvac.Client | None = None
        self._expires_at = 0.0

    def client(self) -> hvac.Client:
        if self._client is None or time.monotonic() > self._expires_at - 30:
            self._login()                     # re-auth 30s before expiry
        return self._client

    def _login(self) -> None:
        c = hvac.Client(url=self._url)
        resp = c.auth.approle.login(
            role_id=self._role_id,
            secret_id=self._secret_id.get_secret_value(),
        )
        if not c.is_authenticated():
            raise SystemExit("Vault AppRole authentication failed")
        self._client = c
        self._expires_at = time.monotonic() + resp["auth"]["lease_duration"]

The session re-authenticates 30 seconds before the lease expires, so a long-running worker never makes a call with a dead token. The secret_id is a SecretStr injected at runtime.

Gotchas & version-specific behaviour

  • secret_id is often single-use or short-TTL; request a fresh one when it expires.
  • Read lease_duration from the login response — do not hard-code the TTL.
  • role_id is non-secret; secret_id is the credential. Treat them differently.
  • Use time.monotonic() for expiry math so clock changes cannot extend the lease.

Production parity checklist

  • secret_id injected at runtime by the orchestrator; never in the repo or image.
  • Re-authentication happens before the token TTL lapses.
  • The AppRole is scoped to the minimum policies the service needs.
  • secret_id wrapped in SecretStr; never logged.
  • A failed authentication stops the process rather than degrading silently.

Conclusion

A session that re-authenticates ahead of TTL turns AppRole into a credential that is always fresh and never stored. For dynamic database credentials on top of this session, see Vault Dynamic Database Credentials in Python.