Webhook icon
Schedule icon
Switch icon
Fail icon
Query icon
If icon
SlackIncomingWebhook icon
Exit icon
Pause icon
OutputValues icon
Queries icon
Set icon
Delete icon
GetKeys icon
Script icon
Loop icon

Just-in-Time PostgreSQL Access with Approval, Automatic Revocation and a Drift Sweep

Grant PostgreSQL roles for a limited time after approval, revoke them automatically, and sweep for expired or unofficial grants.

Categories
Infrastructure

Standing write or admin rights on production databases are one of the most common audit findings, and one of the riskiest. Every engineer who might need them during an incident keeps them all year. Just-in-time access fixes this: nobody holds the privilege until they need it, a lead approves each use, and it disappears when the time is up. This blueprint does that for PostgreSQL roles, with no extra tooling. It's for platform, security and data teams that must show auditors who had access to what, when, and why.

How it works

Request mode (a manual run, which gives a ready-made request form, or the access_request webhook from a Slack command, portal or ticketing tool):

  1. check_login, check_reason and check_minutes (io.kestra.plugin.core.execution.Fail with a condition) reject, each with its own message, a malformed login name, a reason shorter than 10 characters, or more minutes than the role allows (max_minutes: readonly 480, readwrite 240, admin 60). user_exists (io.kestra.plugin.jdbc.postgresql.Query, parameterized) confirms the login exists.
  2. already_active ends a duplicate request while the same grant is still active.
  3. ask_approver posts the request to Slack, and approval (io.kestra.plugin.core.flow.Pause) waits up to two hours for Approve or Deny. The approver can shorten the window and add a note.
  4. On approval:
    • grant (io.kestra.plugin.jdbc.postgresql.Queries, in a transaction) runs GRANT <role> TO <login>, and verify_grant reads the membership back from pg_auth_members;
    • register (io.kestra.plugin.core.kv.Set) records who, what, why, the ticket, the approval and the expiry;
    • tell_granted posts "granted until …".
  5. access_window (Pause with pauseDuration set to the approved minutes) waits out the window. Resuming it ends the access early. Then revoke runs REVOKE, verify_revoke confirms the membership is gone, unregister (io.kestra.plugin.core.kv.Delete) removes the record, and Slack is told.

Sweep mode (the sweep schedule, every 15 minutes) is the safety net:

  • active_grants (io.kestra.plugin.core.kv.GetKeys) and members read the registered grants and the real members of the managed roles;
  • reconcile finds registered grants past their expiry, for example when a request execution was killed or Kestra was down when the timer ended, and members who were granted a managed role outside this flow;
  • cleanup (io.kestra.plugin.core.flow.Loop) revokes the expired ones, and tell_sweep reports both lists.

Grants made outside the flow are reported, never revoked, because removing them is a decision for a person.

Prerequisites

  • PostgreSQL 16 or later, with the managed roles (app_readonly, app_readwrite, app_admin, or your own names in the role input and max_minutes), and a personal login per engineer.
  • A dedicated administration login for Kestra that can manage only those roles, and never use them itself: GRANT app_readonly, app_readwrite, app_admin TO kestra_jit WITH ADMIN TRUE, INHERIT FALSE, SET FALSE;
  • Docker on the worker for the sweep's Python script.
  • Secrets:
    • JIT_DB_URL: the JDBC URL of the database, for example jdbc:postgresql://db:5432/app.
    • JIT_DB_USERNAME and JIT_DB_PASSWORD: the administration login above.
    • SLACK_WEBHOOK_URL: Slack incoming webhook for requests, grants, revocations, sweep reports and failure alerts.
    • JIT_WEBHOOK_KEY: a long random string you create (for example openssl rand -hex 16), which forms the secret part of the request webhook URL.

Inputs

  • mode (SELECT request or sweep, default request). The sweep schedule sets it to sweep.
  • requester (STRING): your existing personal PostgreSQL login. The role is granted to it, so no password is ever created or sent.
  • role (SELECT app_readonly, app_readwrite or app_admin, default app_readonly): only these roles can be granted.
  • minutes (INT, default 60): capped per role by the max_minutes variable.
  • reason (STRING): why the access is needed, at least 10 characters.
  • ticket (STRING, optional): an incident or ticket reference.

Quick start

  1. Create the managed roles and the administration login, add the five secrets, and save the flow.
  2. Run it manually with your own login, a managed role, 5 minutes and a reason. Approve it in the paused execution, check the membership in PostgreSQL, and watch it disappear after 5 minutes.
  3. Run it with mode set to sweep to see the reconciliation report.
  4. Enable the sweep schedule, and point your Slack command or portal at the access_request webhook with a JSON body: {"requester": "...", "role": "...", "minutes": 60, "reason": "...", "ticket": "..."}.

Outputs

  • {{ outputs.decision }} and {{ outputs.expires_at }}: the approver's decision and when the access ends.
  • KV keys jit-grant-<login>-<role>: one per active grant, with the requester, role, reason, ticket, approval time, approver note, expiry and execution.
  • {{ outputs.reconcile.vars.expired }} and {{ outputs.reconcile.vars.drift }} in sweep runs.
  • The execution history: every request, decision, grant and revocation, with timestamps, as the audit trail.

Common pitfalls

  • On open-source Kestra the resume record has no user name, so approved_by is "not recorded". Keep approvals in a Slack channel, or use Enterprise Edition for named approvers.
  • The administration login must have ADMIN on the managed roles. On PostgreSQL 16 and later, a REVOKE only removes grants that login made itself, which is why grants made by hand show up as drift.
  • The role list exists in three places (the role input, max_minutes, and the sweep query). Keep them in sync when you rename roles.
  • Long windows keep an execution paused for their whole duration. That's expected; the sweep covers restarts and kills.

How to extend

  • Grant roles in other systems the same way, for example AWS IAM with io.kestra.plugin.aws.cli.AwsCLI, or GitHub team membership with an HTTP task.
  • Write each grant and revocation to an audit table with one more Query task.
  • Let the sweep also revoke drift after an approval step, instead of only reporting it.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.