Problem Statement

For decades, the standard pattern for granting an application access to a database or external system involved creating a dedicated service account or static credentials as an application secret (e.g., svc_app_prod) in LDAP or directly in the database. While simple to implement, this approach has become an enterprise-grade security liability.

This pattern introduces three critical enterprise risks:

  • Standing Privileges & Blast Radius: The account exists 24/7, even when the application is idle. If an attacker compromises the credentials — via a leaked .env file or source code repository — they gain indefinite, undetected access to the database.
  • The Rotation Nightmare: Rotating a shared database password often requires coordinated application restarts. If the password is changed in the database but not updated in the application's configuration simultaneously, the application crashes.
  • Orphaned Accounts: When applications are decommissioned, their service accounts are frequently left behind, creating "ghost accounts" that auditors flag during compliance checks (SOC 2, PCI-DSS).

The core risk is simple: static credentials that never expire are a ticking clock. Every day they exist unused is another day an attacker could be silently using them.

Solution: The Identity Broker API (/issue)

The solution is to introduce a dedicated Identity Broker API — a lightweight /issue endpoint that acts as the single gatekeeper between all workloads and every target system (databases, message queues, cloud storage). Applications never hold a password; they hold only a short-lived lease that expires automatically.

This broker sits on top of enterprise PAM tools like CyberArk Conjur or HashiCorp Vault, which perform the actual dynamic secret generation. The broker adds the policy layer: it enforces which workload identity can request which role on which target, logs every issuance to an immutable audit trail, and ensures cleanup happens even when the PAM engine experiences transient failures.

Eliminate Long-Lived Credentials

Core principle: A credential should exist for exactly as long as the work that requires it — no longer. The /issue API is the mechanism that enforces this.

Why Build a Custom Broker Abstraction Layer?

Enterprise PAM tools like CyberArk Conjur and HashiCorp Vault are powerful — but integrating them directly into every application creates a hidden dependency that is as risky as the static credentials you are trying to eliminate.

Building a thin, organisation-owned broker library that wraps the PAM product behind your /issue API is the architectural pattern that makes JIT access both sustainable and enterprise-ready. Here is why this abstraction layer is critical:

No Vendor Lock-In

When an application calls issueApiClient.fetchCredential("inventory_db"), it does not know whether HashiCorp Vault, CyberArk, or your own LDAP script is running behind the API. The contract is the /issue endpoint — not the PAM vendor.

If your organisation decides to migrate from HashiCorp Vault to CyberArk Conjur (or vice versa), the change is entirely internal to the broker. Every application across every team continues operating without a single line of application code changing.

Organisation-Specific Rules & Policy Enforcement

Off-the-shelf PAM products enforce their own generic security policies. Your broker layer lets you enforce organisation-specific rules on top of them, such as:

  • Workload allow-lists: Only pre-approved workload identities (Kubernetes service accounts, cloud IAM roles) may request credentials for a given target.
  • Time-window policies: Certain sensitive databases may only be accessed during business hours; all other requests are rejected at the broker — before they reach the PAM engine.
  • Role scoping by environment: A workload tagged env=staging is blocked from ever requesting a production database role, regardless of what the PAM product would permit on its own.
  • Unified rate limiting: Prevent credential-stuffing attacks against the broker by enforcing per-workload request quotas across all PAM backends simultaneously.

Decoupling Applications from PAM Vendors

Without an abstraction layer, each application team integrates the PAM SDK directly. This means:

  • Every application must be updated when the PAM vendor releases a breaking SDK change.
  • Secrets management logic is scattered across dozens of codebases in different languages.
  • Audit and observability tooling must be built separately in every application.

With the custom broker layer, all of that complexity lives in one place. Applications are reduced to a simple HTTP call. The broker owns the PAM SDK, the retry logic, the audit logging, and the vendor-specific credential lifecycle — and it does so once, for everyone.

Architectural principle: Treat your /issue broker the same way you treat a payment gateway — it is the single, hardened integration point. Applications should be consumers of credentials, never managers of them. The broker is where vendor relationships, organisational policies, and security hardening are concentrated.
🔗 Related: Google Workload Identity Federation

The same principle of building an organisation-owned wrapper library applies to federated identity token exchange. In the Google Workload Identity Federation guide, we explore how wrapping the WLIF token exchange behind a custom library removes the direct dependency on Google's credential helper binary — giving your organisation the same benefits covered here: no vendor lock-in, centralised policy enforcement, and the freedom to swap the underlying mechanism without touching application code.

Why Traditional Static Accounts Fail

Why do traditional static accounts fail in modern cloud-native environments? Because Identity and Permissions must be decoupled from permanence.

Creating a random user is useless if that user has no permissions. Conversely, granting permanent permissions to a static user violates the principle of Least Privilege.

Enterprises solve this by utilizing dynamic secrets engines (like CyberArk Conjur or HashiCorp Vault). Instead of relying on static accounts, the architecture shifts to Zero Standing Privileges (ZSP). In this model:

  • The account literally does not exist until the exact second it is needed.
  • It is granted exact role-based permissions upon creation.
  • It is wiped out of existence the moment its Time-To-Live (TTL) expires.

We need a pattern that allows an application to securely request access, receive a temporary credential, and seamlessly renew that credential without dropping active database connections.

Standing Privileges vs. Zero Standing Privileges

Attribute Static Service Account JIT / ZSP Model
Credential Lifetime Indefinite (months / years) Minutes to hours (TTL-bound)
Rotation Requirement Manual, disruptive Automatic, seamless
Blast Radius on Compromise High — unlimited access window Minimal — expires within TTL
Ghost Account Risk High Near-zero (auto-deleted)
Compliance Auditability Difficult (shared accounts) Full traceability per workload

How to Pattern

To implement this, we introduce an Identity Broker API (/issue) that sits between the consuming application and the enterprise PAM system / LDAP. The architecture is broken down into three phases.

Phase 1: Workload Identity — Authenticating to the Broker

Before the /issue API generates a database password, it must verify who is asking. The API should never rely on static API keys. Instead, it should leverage Workload Identity Federation (e.g., SPIFFE/SPIRE, Kubernetes Service Account OIDC tokens, or AWS IAM Roles).

The application presents a short-lived cryptographic token to the /issue API, proving its identity. This eliminates the chicken-and-egg problem of needing a static secret to get a dynamic secret.

Key Principle: The /issue API authenticates the workload identity (e.g., a Kubernetes service account or a cloud IAM role), not a human user. This makes the credential lifecycle fully automated and auditable.

Phase 2: The Dynamic Provisioning Architecture

Once authenticated, the /issue API orchestrates the creation of the credential using one of two methods:

Method A: The Database-Direct Method (Bypassing LDAP)

The secrets engine logs directly into the target database using a highly privileged "Role Manager" account. It executes a dynamic SQL statement to create an ephemeral user and immediately grants it a pre-existing role.


-- Step 1: Create the ephemeral user with a random password
--         NOTE: password value uses single quotes (string literal);
--         double quotes in PostgreSQL are identifier delimiters, not string delimiters
CREATE USER "v-ext-12345" WITH PASSWORD 'a8f#Kz!9pL';

-- Step 2: Grant a pre-existing, scoped role (never grant raw table permissions)
GRANT app_read_only TO "v-ext-12345";

-- Step 3: Database-enforced TTL as a hard safety net
ALTER USER "v-ext-12345" VALID UNTIL '2025-01-01 10:00:00';

-- On TTL expiry: the PAM system executes cleanup
REVOKE app_read_only FROM "v-ext-12345";
DROP USER "v-ext-12345";

Method B: The LDAP Group Mapping Method

If enterprise policy mandates that all identities live in Active Directory, the engine creates a temporary user in a specific Organizational Unit (OU) and adds that user to an AD Group (e.g., GRP_DB_Inventory_ReadWrite).

The database authenticates the user via LDAP and grants access based on group membership — the database itself never needs to know about the ephemeral user's creation or deletion.

  • Provisioning: Create user in OU=Temporary_Access → Add to GRP_DB_Inventory_ReadWrite
  • Revocation: Remove from the AD group → Delete the temporary OU entry

Phase 3: The Consuming Application Pattern (Handling the TTL)

If the /issue API grants a password with a 60-minute TTL, how does a long-running Spring Boot or Node.js application survive the rotation? The key insight is that databases generally do not terminate active TCP connections when a password changes — they only reject new connection attempts.

Pattern A: The Proactive "Lease Renewal" — The 80% Rule

This is the industry standard for long-running microservices.

  • The application fetches the credential with a 60-minute TTL and initializes its database connection pool (e.g., HikariCP).
  • A background thread is scheduled to wake up at 80% of the TTL (minute 48).
  • At minute 48, the application calls the /issue API to fetch the next password.
  • The application gracefully updates the connection pool configuration with the new password.
Crucial Configuration: The connection pool's maxLifetime must be set shorter than the TTL (e.g., 45 minutes). This forces the pool to retire old connections and establish new ones using the fresh password before the original password expires at minute 60.

// Spring Boot + HikariCP: Proactive Lease Renewal (Pattern A)
@Service
public class JitCredentialService {

    private final HikariDataSource dataSource;
    private static final long RENEWAL_DELAY_MINUTES = 48; // 80% of 60-min TTL

    @PostConstruct
    public void initializeAndScheduleRenewal() {
        CredentialResponse cred = issueApiClient.fetchCredential("inventory_db");
        applyCredentialToPool(cred);

        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(
            this::renewCredential,
            RENEWAL_DELAY_MINUTES, RENEWAL_DELAY_MINUTES, TimeUnit.MINUTES
        );
    }

    private void renewCredential() {
        CredentialResponse fresh = issueApiClient.fetchCredential("inventory_db");
        applyCredentialToPool(fresh);
        log.info("JIT credential rotated successfully at {}", Instant.now());
    }

    private void applyCredentialToPool(CredentialResponse cred) {
        // maxLifetime set in application.properties:
        // spring.datasource.hikari.max-lifetime=2700000 (45 min in ms)
        dataSource.getHikariConfigMXBean().setPassword(cred.getPassword());
    }
}

Pattern B: The Sidecar / Agent Pattern

To avoid writing lease-renewal logic into every microservice, enterprises deploy a Sidecar container alongside the application pod. The Sidecar is solely responsible for authenticating with the /issue API and writing fresh credentials to an in-memory volume (e.g., /dev/shm/db-creds). The application code remains completely unaware that the password is rotating.


# Kubernetes Pod spec: Sidecar credential injector (Pattern B)
spec:
  volumes:
    - name: db-creds
      emptyDir:
        medium: Memory   # in-memory only, never written to disk

  initContainers:
    - name: credential-init
      image: corp/jit-broker-agent:latest
      command: ["sh", "-c", "jit-agent fetch --target inventory_db --out /creds/db.env"]
      volumeMounts:
        - mountPath: /creds
          name: db-creds

  containers:
    - name: app
      image: corp/inventory-service:latest
      volumeMounts:
        - mountPath: /creds
          name: db-creds

    - name: credential-sidecar
      image: corp/jit-broker-agent:latest
      command: ["sh", "-c", "jit-agent renew --interval 48m --out /creds/db.env"]
      volumeMounts:
        - mountPath: /creds
          name: db-creds

Pattern C: Dual Passwords (Database-Side)

For legacy applications that cannot gracefully rotate connection pools, modern databases (like MySQL 8+ and Oracle) support Dual Passwords. The PAM system issues a new password but retains the old one for an overlapping window (e.g., 10 minutes), preventing AuthenticationException errors during the race condition of a password swap.


-- MySQL 8+: Dual Password rotation (Pattern C)
-- Set new primary password, keep old as secondary for 10-min overlap
ALTER USER "v-ext-12345"@"%" IDENTIFIED BY "newSecurePassword!" RETAIN CURRENT PASSWORD;

-- After the overlap window, discard the old secondary password
ALTER USER "v-ext-12345"@"%" DISCARD OLD PASSWORD;

Enterprise Best Practices

Building the /issue API is only half the battle. To make this system production-ready and compliant, the following best practices must be implemented.

1. Implement Reconciliation Loops (The Cleanup Failsafe)

The creation of a temporary account is a feature; the guaranteed deletion of that account is the security. If the PAM system or LDAP server experiences a network blip exactly when a TTL expires, the deletion command will fail, leaving an active "ghost account" behind.

Best Practice: Implement an out-of-band reconciliation cron job that runs periodically (e.g., every hour). This job scans the database or the OU=Temporary_Access LDAP folder and forcefully deletes any account whose creation timestamp exceeds the maximum allowed TTL.


-- Reconciliation query: find and forcefully clean up ghost accounts
-- Scheduled as an independent cron job (every hour)
SELECT usename
FROM pg_catalog.pg_user
WHERE usename LIKE "v-ext-%"
  AND valuntil < NOW();

-- For each row returned, execute cleanup:
-- REVOKE app_read_only FROM "v-ext-XXXXX";
-- DROP USER "v-ext-XXXXX";

2. Audit Traceability (Mapping Ephemeral to Persistent)

When an auditor sees that user v-ext-12345 dropped a database table, that information is useless unless they know which application was using that ID.

Best Practice: The /issue API must maintain an immutable audit log mapping the ephemeral ID to the Workload Identity:


// Immutable audit log entry (append-only Firestore / SIEM)
{
  "timestamp":     "2025-01-01T09:00:00Z",
  "workload":      "payment-service-prod",
  "k8s_namespace": "production",
  "k8s_pod":       "payment-service-7d8f9",
  "target_db":     "inventory_db",
  "ephemeral_id":  "v-ext-12345",
  "ttl_minutes":   60,
  "expires_at":    "2025-01-01T10:00:00Z",
  "issued_by":     "jit-broker/issue-api v2.3.1"
}

3. Scope the Broker's Privileges (Least Privilege)

The service account used by your /issue API to create temporary users is essentially a "God-Mode" account. If compromised, an attacker could create permanent backdoor accounts.

Best Practice: Use strict LDAP Delegation or Database Role scoping. The broker's service account must only have permissions to CREATE and DELETE users within a specific, isolated LDAP OU, and can only GRANT a predefined list of approved roles.


-- PostgreSQL: Scope the broker role to ONLY what it needs
GRANT CREATEROLE TO jit_broker_role;

-- Broker can only grant pre-approved application roles
GRANT app_read_only  TO jit_broker_role WITH GRANT OPTION;
GRANT app_read_write TO jit_broker_role WITH GRANT OPTION;

-- Explicitly deny elevated privileges
ALTER ROLE jit_broker_role NOSUPERUSER NOREPLICATION NOCREATEDB;

4. Resiliency and Exponential Backoff

If the /issue API goes down, applications cannot boot, and running applications will eventually crash when their connection pools expire.

Best Practice: Consuming applications must implement retry logic with exponential backoff when calling the /issue API. Because the app attempts to renew at the 80% mark, it has a 20% buffer window (e.g., 12 minutes) to keep retrying the API before the database severs the connection.


// Spring Retry: Exponential backoff for /issue API calls
@Retryable(
    value = { IssueApiException.class },
    maxAttempts = 5,
    backoff = @Backoff(delay = 2000, multiplier = 2.0, maxDelay = 30000)
)
public CredentialResponse fetchCredential(String targetDb) {
    return issueApiClient.issue(targetDb);
}

@Recover
public CredentialResponse onIssueApiFailure(IssueApiException ex, String targetDb) {
    log.error("CRITICAL: /issue API unreachable after max retries for {}", targetDb);
    alertingService.triggerP1Alert("jit-broker-unreachable", targetDb);
    throw new CredentialUnavailableException("Cannot obtain JIT credential", ex);
}

Conclusion

Transitioning to Just-In-Time access is not merely a password rotation strategy; it is a fundamental shift in enterprise architecture. By forcing applications to request ephemeral access via an /issue broker, organizations achieve true Zero Standing Privileges.

While the creation of a temporary account is straightforward, the architectural complexity lies in:

  • The guaranteed revocation of that access (reconciliation loops).
  • The graceful handling of connection pools during rotation (the 80% renewal rule).
  • The robust auditing required for compliance (immutable ephemeral-to-workload mapping).

Whether utilizing enterprise tools like CyberArk Conjur or building a custom LDAP-brokered API, designing for the cleanup and rotation lifecycle is what separates a fragile script from a resilient, production-ready enterprise security pattern.

🔐 Eliminate Static Credentials Entirely

Combine this JIT pattern with Workload Identity Federation to remove long-lived Google Service Account keys from your stack entirely. Check out our guide on Google Workload Identity Federation to complete your Zero Trust journey.

Implementation FAQ & Common Pitfalls

expand_more

Frequently Asked Questions

What happens if the /issue API is unavailable at startup?

The application should fail fast and not start. A retry with exponential backoff (up to a reasonable timeout, e.g., 2 minutes) is acceptable. Do not fall back to a cached static credential — that defeats the purpose of JIT.

Can I use this pattern with non-database targets (e.g., S3, Kafka)?

Yes. The same pattern applies: the /issue API generates a short-lived IAM role assumption token (for S3) or a temporary Kafka ACL user. The TTL and renewal logic remain identical.

What is the difference between CyberArk Conjur and HashiCorp Vault?

Both are mature enterprise PAM solutions. CyberArk Conjur is typically chosen by organizations with existing CyberArk PAM investments and strong LDAP/AD integration requirements. HashiCorp Vault is preferred in cloud-native or multi-cloud environments due to its extensive secrets engine ecosystem (AWS, GCP, Azure, databases) and its open-source community.

How short should the TTL be?

Industry practice: 1 hour for most application-to-database credentials. Mission-critical systems with strong renewal automation may use 15 to 30 minutes. The TTL must always exceed your connection pool's maxLifetime.

Does the database terminate active connections when the password changes?

No — this is the key property that makes this architecture viable. In PostgreSQL, MySQL, and Oracle, existing authenticated sessions remain open after a password change. Only new connection attempts must use the new password. The pool's maxLifetime setting drives the graceful cycling of old connections.