<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vinod Patil</title>
    <description>The latest articles on DEV Community by Vinod Patil (@vinod_patil_a7bd8dcbcc42e).</description>
    <link>https://dev.to/vinod_patil_a7bd8dcbcc42e</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2206781%2Fbc72ef61-63cf-4c29-b281-e80731934988.jpg</url>
      <title>DEV Community: Vinod Patil</title>
      <link>https://dev.to/vinod_patil_a7bd8dcbcc42e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vinod_patil_a7bd8dcbcc42e"/>
    <language>en</language>
    <item>
      <title>Step-Up MFA Is the Wrong Response to Credential-Theft Tools in a Privileged Session</title>
      <dc:creator>Vinod Patil</dc:creator>
      <pubDate>Sun, 18 Oct 2026 12:11:41 +0000</pubDate>
      <link>https://dev.to/vinod_patil_a7bd8dcbcc42e/step-up-mfa-is-the-wrong-response-to-credential-theft-tools-in-a-privileged-session-11lp</link>
      <guid>https://dev.to/vinod_patil_a7bd8dcbcc42e/step-up-mfa-is-the-wrong-response-to-credential-theft-tools-in-a-privileged-session-11lp</guid>
      <description>&lt;p&gt;Most session-control products respond to a risky command the same way: pause, ask for another MFA prompt, continue if the user passes.&lt;/p&gt;

&lt;p&gt;For many commands that is reasonable. A destructive command in the wrong directory is usually a mistake by someone who is allowed to be there. A second factor confirms the person, and the person can explain.&lt;/p&gt;

&lt;p&gt;For credential-theft tooling it is the wrong control. The question is not "is this really you?". It is "why is a credential-dumping tool being used in a production session at all?". A valid second factor does not answer that. It confirms that the right person, or the right person's stolen session, is about to dump credentials. (Disclosure: I work on zenxPAM.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in zenxPAM
&lt;/h2&gt;

&lt;p&gt;Command analysis now recognises known credential-theft tooling and treats it as a &lt;strong&gt;hard session block&lt;/strong&gt;. The session is disconnected. It does not get a step-up prompt, and it does not just raise an alert for someone to read later.&lt;/p&gt;

&lt;p&gt;The order of checks is the point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;command in session
      │
      ▼
 hard-block check ── known credential-theft tooling ──► terminate session
      │ no match
      ▼
 risk scoring ──► step-up MFA / alert / allow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hard-block check runs &lt;strong&gt;before&lt;/strong&gt; the step-up MFA gate, so a credential-theft tool never reaches a prompt that a stolen session could pass. The threat response is chosen from the reason for the decision: a hard block maps to ending the session, rather than to the alert-the-security-team or step-up responses used for ordinary risk. Further commands from a session that has already been terminated are refused as well, so a blocked session cannot keep sending input in the gap before the disconnect lands.&lt;/p&gt;

&lt;h2&gt;
  
  
  An operator who understands what happened
&lt;/h2&gt;

&lt;p&gt;When a session ends because of a policy block, the viewer shows &lt;strong&gt;Session Blocked&lt;/strong&gt;, not the generic "terminated by an administrator" message. It sounds minor. An operator who sees "terminated by an administrator" phones the admin. One who sees "Session Blocked" with the policy reason knows what happened, and the audit record says the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A deliberate trade-off
&lt;/h2&gt;

&lt;p&gt;Some administrative tools have legitimate uses, and a hard block will occasionally end a session that was doing honest work. That is a deliberate choice for a privileged-access product. The expectation is that the approved request scope already covers what the operator needs. If a team's work genuinely requires such a tool, the answer is a narrower, explicit approved scope for that task, not a softer default for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the right default
&lt;/h2&gt;

&lt;p&gt;Every control in a PAM system is a statement about what you believe an approved session is for. If an approval says "restart the billing service", credential-theft tooling is outside that statement under any identity. Treating it as a question to be answered with another MFA tap invites the wrong conclusion, that the only problem is whether the person is who they say they are.&lt;/p&gt;

&lt;p&gt;It also sits alongside, not instead of, the other layers: intent-scoped access, approved-application controls on the session host, and endpoint protection on the target.&lt;/p&gt;

&lt;p&gt;If you run privileged sessions through a gateway, ask one question of your platform: what happens when a session uses credential-theft tooling? A prompt is a policy decision. A shrug is a gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;See how zenxPAM enforces intent-scoped, just-in-time privileged sessions:&lt;/strong&gt; &lt;a href="https://zenxsys.com" rel="noopener noreferrer"&gt;https://zenxsys.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://zenxsys.com/blog/block-credential-dumping-tools-privileged-sessions" rel="noopener noreferrer"&gt;https://zenxsys.com/blog/block-credential-dumping-tools-privileged-sessions&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>cybersecurity</category>
      <category>windows</category>
    </item>
    <item>
      <title>Auditing Kubernetes Long-Lived Token Secrets Without Reading a Single Secret</title>
      <dc:creator>Vinod Patil</dc:creator>
      <pubDate>Sun, 18 Oct 2026 11:25:41 +0000</pubDate>
      <link>https://dev.to/vinod_patil_a7bd8dcbcc42e/auditing-kubernetes-long-lived-token-secrets-without-reading-a-single-secret-1dk3</link>
      <guid>https://dev.to/vinod_patil_a7bd8dcbcc42e/auditing-kubernetes-long-lived-token-secrets-without-reading-a-single-secret-1dk3</guid>
      <description>&lt;p&gt;Since Kubernetes 1.24 a service account no longer gets a token Secret automatically. That does not mean your cluster has none. Anyone who ran &lt;code&gt;kubectl create secret&lt;/code&gt; with &lt;code&gt;type: kubernetes.io/service-account-token&lt;/code&gt;, and every cluster upgraded from an older release, can still have &lt;strong&gt;tokens that never expire&lt;/strong&gt;, sitting in a namespace nobody looks at.&lt;/p&gt;

&lt;p&gt;They are the classic leaked credential: long-lived, bearer, and usually forgotten. Finding them sounds like a one-liner. It isn't, and the obvious way to do it is a security problem of its own. (Disclosure: I work on zenxPAM, which is where this scan lives.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "just list the Secrets"
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get secrets &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--field-selector&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;kubernetes.io/service-account-token &lt;span class="nt"&gt;-o&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns each Secret &lt;strong&gt;with its &lt;code&gt;data&lt;/code&gt;&lt;/strong&gt;, meaning the token itself. Your audit tool now holds every long-lived credential in the cluster. If the tool is compromised, logs the response, or caches it, you have turned an inventory into a credential store.&lt;/p&gt;

&lt;p&gt;Worse, there is a subtlety in the RBAC model that surprises people: the &lt;code&gt;list&lt;/code&gt; verb on &lt;code&gt;secrets&lt;/code&gt; is enough to read their values (a list returns full objects). Granting "just list" to an audit account is not a read-only-metadata grant. It is a grant to read every secret in scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask the API server for metadata only
&lt;/h2&gt;

&lt;p&gt;The API server can return partial objects. Send this header on the list request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Accept: application/json;as=PartialObjectMetadataList;g=meta.k8s.io;v=v1, application/json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get names, uids, labels, annotations and timestamps back, and &lt;strong&gt;no &lt;code&gt;data&lt;/code&gt;&lt;/strong&gt;. In our lab test on a real k3s 1.30.5 cluster, the same request without that header returned &lt;code&gt;data&lt;/code&gt;, and with it returned none. So the header is what keeps values out, not the verb.&lt;/p&gt;

&lt;p&gt;We did not stop at trusting the header:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The scanner parses the response with a reader that extracts &lt;strong&gt;only allow-listed fields&lt;/strong&gt;. It never materialises &lt;code&gt;data&lt;/code&gt; or &lt;code&gt;stringData&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If a server ignores the header and sends values anyway, the property is skipped unread, nothing is kept, logged or returned, and the scan records a warning: "The API server returned Secret values; they were discarded."&lt;/li&gt;
&lt;li&gt;The scan only uses &lt;code&gt;GET&lt;/code&gt;. No &lt;code&gt;get secret&lt;/code&gt; on individual objects, no watch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two lists per scope: one with &lt;code&gt;fieldSelector=type=kubernetes.io/service-account-token&lt;/code&gt; for the legacy tokens, one with a label selector for the Secrets zenxPAM itself creates as revocation markers (see the previous post in this series).&lt;/p&gt;

&lt;h2&gt;
  
  
  What a finding tells you
&lt;/h2&gt;

&lt;p&gt;For each long-lived token Secret we record the namespace, the service account, the Secret's uid, its age, and when it was last used. The last one is the useful part, and Kubernetes gives it to you for free: the API server writes a &lt;code&gt;kubernetes.io/legacy-token-last-used&lt;/code&gt; label on the Secret.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On Kubernetes 1.28 and later, no label means &lt;strong&gt;never used since tracking started&lt;/strong&gt;. That is the "delete this" candidate.&lt;/li&gt;
&lt;li&gt;On 1.26 and 1.27 the information is partial; below that it is unknown. The scan says so rather than guessing.&lt;/li&gt;
&lt;li&gt;In our lab test the API server wrote the label 0.3 to 0.5 seconds after a token was used.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Four traps we hit (and fixed) building it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A page cap that retries once is a denial-of-service.&lt;/strong&gt; Anyone who can create Secrets can attach up to 256 KiB of annotations. With a 1 MB page cap and one retry, a single planted object can stall the scan forever. The page size now keeps shrinking: 100, 25, 5, then 1, and it only fails if a single object is still over the cap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Labels are attacker-copyable.&lt;/strong&gt; A Secret carrying &lt;code&gt;app.kubernetes.io/managed-by=zenxpam&lt;/code&gt; is not necessarily ours. A Secret is shown as managed only when its &lt;strong&gt;name&lt;/strong&gt; is also the deterministic one the rotator uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A resume cursor with only a position skips namespaces.&lt;/strong&gt; If someone edits the namespace list between pages, a position-only cursor resumes at a shifted index and the scan still ends "complete". The cursor now carries a fingerprint of the scope list, and a mismatch fails the scan instead of silently under-reporting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The save validator and the scanner must accept the same config.&lt;/strong&gt; If they disagree on what a valid namespace list looks like, a saved setting fails every scan.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these show up in a happy-path demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions: use a separate account
&lt;/h2&gt;

&lt;p&gt;The scan needs one verb, &lt;code&gt;list&lt;/code&gt; on &lt;code&gt;secrets&lt;/code&gt;, in the namespaces you choose (a Role per namespace), or cluster-wide with a ClusterRole. Because of the &lt;code&gt;list&lt;/code&gt;-reads-values point above, &lt;strong&gt;do not reuse the rotation admin token for this&lt;/strong&gt;. Create a separate, read-only cloud account limited to the namespaces you scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; NS create role zenx-inventory &lt;span class="nt"&gt;--verb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;list &lt;span class="nt"&gt;--resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secrets
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; NS create rolebinding zenx-inventory &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;zenx-inventory &lt;span class="nt"&gt;--serviceaccount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;NS:SERVICE_ACCOUNT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scan itself never reads a single Secret, so &lt;code&gt;get&lt;/code&gt; is not granted, and in our test the cluster refused &lt;code&gt;get&lt;/code&gt; for that account while the scan still succeeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens after you find them
&lt;/h2&gt;

&lt;p&gt;Findings are stale, never-used, or without an owner. From a finding you can open the rotation flow with the namespace and service account filled in. Rotation issues new, revocable tokens. It does &lt;strong&gt;not&lt;/strong&gt; delete the old long-lived Secret: that stays until you remove it (&lt;code&gt;kubectl -n NS delete secret NAME&lt;/code&gt;), because deleting a credential some forgotten consumer still uses is a decision for a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Verified on a real single-node k3s 1.30.5 cluster through the full UI and services, with a separate read-only account. Not verified on EKS, AKS or GKE, against a large number of Secrets, or against a legacy token the cluster has marked invalid.&lt;/li&gt;
&lt;li&gt;Bound (projected) tokens are not stored anywhere, so they cannot be listed. Service accounts without a token Secret are principals, not credentials, and are not findings.&lt;/li&gt;
&lt;li&gt;The scan is read-only. It never changes the cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;See how zenxPAM inventories and rotates non-human credentials:&lt;/strong&gt; &lt;a href="https://zenxsys.com" rel="noopener noreferrer"&gt;https://zenxsys.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://zenxsys.com/blog/audit-kubernetes-long-lived-service-account-tokens" rel="noopener noreferrer"&gt;https://zenxsys.com/blog/audit-kubernetes-long-lived-service-account-tokens&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>devops</category>
      <category>rbac</category>
    </item>
    <item>
      <title>Kubernetes Token Rotation Doesn't Revoke Anything. Here's How to Fix That With Bound Marker Secrets</title>
      <dc:creator>Vinod Patil</dc:creator>
      <pubDate>Sun, 18 Oct 2026 11:00:53 +0000</pubDate>
      <link>https://dev.to/vinod_patil_a7bd8dcbcc42e/kubernetes-token-rotation-doesnt-revoke-anything-heres-how-to-fix-that-with-bound-marker-secrets-2p0b</link>
      <guid>https://dev.to/vinod_patil_a7bd8dcbcc42e/kubernetes-token-rotation-doesnt-revoke-anything-heres-how-to-fix-that-with-bound-marker-secrets-2p0b</guid>
      <description>&lt;p&gt;You rotate a Kubernetes service account token. The new one works. You update the consumer, close the ticket, and feel good.&lt;/p&gt;

&lt;p&gt;Then someone asks: &lt;strong&gt;"Is the old token dead?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It isn't. A token minted through the TokenRequest API stays valid until its &lt;code&gt;exp&lt;/code&gt; claim, whether that is in one hour or in seven days. "Rotation" that only creates a new credential is key &lt;em&gt;issuance&lt;/em&gt;. The leak window you were trying to close is still open.&lt;/p&gt;

&lt;p&gt;This post explains the API feature that makes revocation possible, the extra machinery you need around it, and what we measured when we built it into zenxPAM, Zenxsys's agentless, just-in-time privileged access platform. (Disclosure: I work on zenxPAM.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What we found when we ran the naive rotation
&lt;/h2&gt;

&lt;p&gt;Before changing anything we ran the existing rotation path against a real single-node k3s 1.30.5 cluster. It worked, and it had gaps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A rotation never invalidated the previous token. Depending on the requested lifetime, that is up to a 24 hour to 7 day window where a copied token still authenticates.&lt;/li&gt;
&lt;li&gt;No preflight, so a missing RBAC verb surfaced as a failed rotation halfway through.&lt;/li&gt;
&lt;li&gt;No check that the new token actually authenticates before the old one is dropped.&lt;/li&gt;
&lt;li&gt;No grace window for consumers that pick up the new token a few minutes late.&lt;/li&gt;
&lt;li&gt;No way back if the new token turns out to be wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rotation without verification, grace and rollback is how you get a 2 a.m. page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The primitive: bound-object tokens
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;POST /api/v1/namespaces/{ns}/serviceaccounts/{sa}/token&lt;/code&gt; accepts &lt;code&gt;spec.boundObjectRef&lt;/code&gt;. If you bind the token to an object, the API server also checks that the object still exists &lt;em&gt;and has the same UID&lt;/em&gt; whenever the token is presented. Delete the object and the token stops working.&lt;/p&gt;

&lt;p&gt;Bind to a Pod and the token lives and dies with the Pod. That is what projected volumes do. For a token that a CI system or an external workload holds, there is no Pod to bind to. A &lt;strong&gt;Secret&lt;/strong&gt; works fine as the anchor. We call it a marker Secret: type &lt;code&gt;Opaque&lt;/code&gt;, no data, only a name and a UID.&lt;/p&gt;

&lt;p&gt;We proved it on the lab cluster before building on it: request a token bound to a Secret, delete the Secret, poll with the token. In our lab test the API server refused the token about 10 seconds later (9.2 to 11.0 seconds across runs). The delay is the API server's authentication cache. A design that promises instant revocation would be lying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two slots, A/B, per identity
&lt;/h2&gt;

&lt;p&gt;Each managed identity gets two marker slots in the target namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;zxp-&amp;lt;first 12 hex of identity id&amp;gt;-a
zxp-&amp;lt;first 12 hex of identity id&amp;gt;-b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both carry the labels &lt;code&gt;app.kubernetes.io/managed-by=zenxpam&lt;/code&gt; and &lt;code&gt;zenxpam.io/nhi=&amp;lt;id&amp;gt;&lt;/code&gt;. The current token is bound to one slot. The next rotation uses the other. That is what lets the old token keep working during a grace window while the new one is already live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rotation, in order
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; resolve admin credentials ──► validate target ──► read current token (claims only)
            │
            ▼
 pick the free slot ──► create marker Secret ──► TokenRequest bound to it (uid from create response)
            │
            ▼
 verify: SelfSubjectReview with the NEW token must say system:serviceaccount:&amp;lt;ns&amp;gt;:&amp;lt;sa&amp;gt;
            │
            ▼
 keep previous token in the vault ──► write new token ──► grace window? ─┬─ yes: old token stays valid
                                                                         └─ no:  delete old marker Secret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Details that matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The new token is proven before anything switches.&lt;/strong&gt; A &lt;code&gt;SelfSubjectReview&lt;/code&gt; with the new token must return the expected service account. A 401 fails the rotation. A 403 or 404 (a hardened or old cluster that does not allow the check) is not a failure, but the step is marked skipped with a warning rather than silently passing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure after the marker Secret is created always compensates.&lt;/strong&gt; Delete the new marker, restore the vault, delete the retained copy. No orphaned Secrets, no half-rotated state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The grace window is capped at the old token's remaining lifetime.&lt;/strong&gt; An expired token needs no revoking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finalize deletes the old marker Secret&lt;/strong&gt; and then polls the &lt;em&gt;old&lt;/em&gt; token for up to 30 seconds. Because of that 10 second auth cache, "refused" is a pass and "still accepted after 30 s" is a warning with evidence, not a hidden assumption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roll back first proves the previous token still authenticates&lt;/strong&gt;, then restores it and deletes the new marker. If the old token is already dead, nothing is changed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Least privilege for the rotator itself
&lt;/h2&gt;

&lt;p&gt;A rotator with cluster-admin would just move the problem. The admin service account needs, in the target namespace only:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Permission&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;create&lt;/code&gt; on &lt;code&gt;serviceaccounts/token&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;resourceNames&lt;/code&gt;: the target service account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;create&lt;/code&gt; on &lt;code&gt;secrets&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;namespace wide (Kubernetes cannot restrict create by name)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;get&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt; on &lt;code&gt;secrets&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;resourceNames&lt;/code&gt;: the two slot names only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No cluster-wide rights. The honest footnote: because &lt;code&gt;create&lt;/code&gt; cannot be name-limited, the rotator can create Secrets in that namespace, but it can only read or delete its own two markers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody expects: the rotator is a confused-deputy risk
&lt;/h2&gt;

&lt;p&gt;A system that takes an API URL and an admin token from configuration and then calls the URL is a gadget for exfiltrating whatever token it is given. Three rules in zenxPAM close that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The cluster address and CA come &lt;strong&gt;only&lt;/strong&gt; from the cloud-account connector, never from the identity being rotated.&lt;/li&gt;
&lt;li&gt;The admin token and CA vault keys must start with &lt;code&gt;K8S_&lt;/code&gt;. The server refuses any other name, so a connector cannot be pointed at an unrelated vault secret and a cluster address you control.&lt;/li&gt;
&lt;li&gt;Private or internal API hosts are refused unless an operator has put them on an allowlist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Verified on a real k3s 1.30.5 cluster, not on EKS, AKS or GKE. Managed clusters may cap token lifetime or restrict &lt;code&gt;SelfSubjectReview&lt;/code&gt;; the preflight reports the lifetime the API server grants.&lt;/li&gt;
&lt;li&gt;If you turn binding off (&lt;code&gt;KubeBindToSecret=false&lt;/code&gt;), the old token &lt;strong&gt;cannot&lt;/strong&gt; be revoked and stays valid until expiry. The result says so, with the end time.&lt;/li&gt;
&lt;li&gt;Revocation lags about 10 seconds. Plan the grace window with that in mind.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this belongs in a PAM platform and not a cron job
&lt;/h2&gt;

&lt;p&gt;You can script all of this with &lt;code&gt;kubectl&lt;/code&gt;. What you cannot easily script is the evidence: which step ran, in which order, who approved the rotation, what the verification returned, whether the old token was confirmed refused. In zenxPAM every preflight, rotation, roll back and cleanup writes an evidence record with timestamped steps, the outcome and credential fingerprints (last four characters only, never a value), and the action is audited. The answer to "is the old token dead?" is a record, not a recollection.&lt;/p&gt;

&lt;p&gt;Next up in this series: how the same pattern (verify new, keep old, revoke on a timer, roll back on demand) works for cloud keys that have no equivalent of a bound object.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;See how zenxPAM handles non-human identities:&lt;/strong&gt; &lt;a href="https://zenxsys.com" rel="noopener noreferrer"&gt;https://zenxsys.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://zenxsys.com/blog/kubernetes-service-account-token-revocation" rel="noopener noreferrer"&gt;https://zenxsys.com/blog/kubernetes-service-account-token-revocation&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>devops</category>
      <category>rbac</category>
    </item>
    <item>
      <title>Session Recordings Are Not Evidence Until You Can Prove Nobody Touched Them</title>
      <dc:creator>Vinod Patil</dc:creator>
      <pubDate>Sun, 18 Oct 2026 10:23:42 +0000</pubDate>
      <link>https://dev.to/vinod_patil_a7bd8dcbcc42e/session-recordings-are-not-evidence-until-you-can-prove-nobody-touched-them-2f2o</link>
      <guid>https://dev.to/vinod_patil_a7bd8dcbcc42e/session-recordings-are-not-evidence-until-you-can-prove-nobody-touched-them-2f2o</guid>
      <description>&lt;p&gt;An auditor asks: "Show me what the contractor did on the production database on March 3rd."&lt;/p&gt;

&lt;p&gt;You have a screen recording. The auditor's next question is the hard one: "How do I know this file is the one that was recorded, and that nobody trimmed it afterwards?"&lt;/p&gt;

&lt;p&gt;If your answer is "it's in a locked folder", you have a recording but not evidence. This post covers how ZenxPAM links a recording to the request that allowed it, and how it makes later tampering detectable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with a recording on its own
&lt;/h2&gt;

&lt;p&gt;A video file says what happened. It says nothing about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who asked for the access, and who approved it;&lt;/li&gt;
&lt;li&gt;which policy decision allowed the session;&lt;/li&gt;
&lt;li&gt;whether the file was changed after capture;&lt;/li&gt;
&lt;li&gt;whether it was deleted, and why.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each gap is a place where an investigation stalls. So we treat the recording as one link in a chain, not as the artefact.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Capture happens at the gateway
&lt;/h2&gt;

&lt;p&gt;Operators connect through a browser, and the gateway streams the session to the target through Apache Guacamole. Because all traffic passes the gateway, the recording is made there, in real time. There is no agent on the target to uninstall and no endpoint driver to disable.&lt;/p&gt;

&lt;p&gt;Each recording gets a unique storage path of the form &lt;code&gt;/{tenantId}/{sessionId}/{recording-id}&lt;/code&gt;, and UTC start and end timestamps are stored on the session. When the recording is uploaded, a SHA-256 hash is computed and stored with its metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The recording is linked back to the request
&lt;/h2&gt;

&lt;p&gt;Every recording points to its session, and every session points back to how it was allowed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Access request -&amp;gt; Approval -&amp;gt; Policy evaluation -&amp;gt; Session -&amp;gt; Recording -&amp;gt; Audit log entries -&amp;gt; Immutable chain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The links are foreign keys in both directions, so an investigator can start from a recording and reach the original request, or start from a request and find every recording it produced. "Who approved this?" and "what did they do?" are answered from the same place.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The audit trail is a hash chain
&lt;/h2&gt;

&lt;p&gt;Alongside the normal audit log, every event is anchored in an append-only chain in PostgreSQL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each entry stores a SHA-256 hash over its id, action, entity type, timestamp, block index and the previous entry's hash;&lt;/li&gt;
&lt;li&gt;block indexes run in sequence per tenant, so a missing entry leaves a visible gap;&lt;/li&gt;
&lt;li&gt;the data layer rejects updates and deletes of chain entries, and the API only exposes append and read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A verification endpoint walks the chain for a tenant and checks three things: each hash matches its recomputed value, each entry points at the real hash of the one before it, and the sequence has no gaps. A separate tamper-detection call reports any broken link.&lt;/p&gt;

&lt;p&gt;One honest limit: this is &lt;strong&gt;tamper-evident, not tamper-proof&lt;/strong&gt;. Someone with raw database access can still rewrite rows. What the chain gives you is that the rewrite is detectable the next time anyone runs verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Deleting a recording leaves a record of the deletion
&lt;/h2&gt;

&lt;p&gt;Retention policies eventually remove old recordings, and that is where evidence usually disappears quietly. ZenxPAM writes audit entries for every step, and they are anchored in the same chain:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;What the audit trail records&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recording archived&lt;/td&gt;
&lt;td&gt;Source path, archive target, archive path and time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recording deleted&lt;/td&gt;
&lt;td&gt;A tombstone with the original path and, if archived, where the copy went&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legal hold placed or released&lt;/td&gt;
&lt;td&gt;Who did it and the stated reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete attempted on a held recording&lt;/td&gt;
&lt;td&gt;The refused attempt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hold release attempted without authority&lt;/td&gt;
&lt;td&gt;The attempt and the caller's roles&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Legal holds also separate duties: an auditor can place a hold but cannot release it. Release needs an admin role. A held recording is skipped by the retention sweep and refused by the delete path.&lt;/p&gt;

&lt;p&gt;Audit ingest is all-or-nothing too. If an entry cannot be anchored in the chain, the whole write is rolled back and the caller gets an error, so an unanchored audit row is never accepted silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The evidence pack
&lt;/h2&gt;

&lt;p&gt;When an auditor asks for a period, the platform builds an evidence pack: counts and references for audit logs, recordings, compliance reports and policy evaluations, in a JSON manifest. A SHA-256 hash is computed over the manifest and stored with the pack, so anyone who downloads it can check it independently.&lt;/p&gt;

&lt;p&gt;This gives your auditors something to verify, not only something to read. It is evidence for your auditors; it does not by itself make an organisation compliant with any framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this
&lt;/h2&gt;

&lt;p&gt;If you run privileged access today, three questions are worth asking about your own setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can you go from a recording to the approval that allowed it in one query?&lt;/li&gt;
&lt;li&gt;If a recording were trimmed tomorrow, would anything notice?&lt;/li&gt;
&lt;li&gt;If one were deleted, could you prove who deleted it and why?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any answer is "no", the recording is a video, not evidence.&lt;/p&gt;

&lt;p&gt;ZenxPAM docs and a demo are at &lt;a href="https://zenxsys.com" rel="noopener noreferrer"&gt;zenxsys.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Building an Agentless, AI-Assisted Privileged Access Gateway</title>
      <dc:creator>Vinod Patil</dc:creator>
      <pubDate>Sun, 18 Oct 2026 09:59:50 +0000</pubDate>
      <link>https://dev.to/vinod_patil_a7bd8dcbcc42e/building-an-agentless-ai-assisted-privileged-access-gateway-1i3k</link>
      <guid>https://dev.to/vinod_patil_a7bd8dcbcc42e/building-an-agentless-ai-assisted-privileged-access-gateway-1i3k</guid>
      <description>&lt;p&gt;Traditional PAM leans on thick RDP/SSH clients, jump boxes and endpoint agents. In multi-cloud and containerised estates that means bootstrap latency on every host, exposed management ports, and audit gaps wherever an agent can't be installed. This post covers how ZenxPAM handles three of those problems: browser-native session streaming, on-prem AI session analysis, and tamper-evident audit evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Zero-footprint interactive sessions
&lt;/h2&gt;

&lt;p&gt;The operator opens a token-authenticated HTML5 client in an ordinary browser. There is no plugin, VPN client or local credential.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser (HTTPS / WSS)  -&amp;gt;  ZenxPAM gateway  -&amp;gt;  Apache Guacamole (guacd)  -&amp;gt;  RDP / SSH / VNC target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protocol translation.&lt;/strong&gt; Apache Guacamole 1.6.0 with custom authentication turns the browser's WebSocket stream into RDP, SSH or VNC toward the target. Targets only need to accept connections from the gateway, so management ports stay off the operator's network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials never reach the browser.&lt;/strong&gt; Secrets are injected from the vault server-side at connect time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recording is a side effect of the architecture.&lt;/strong&gt; Because every byte passes through the gateway, Guacamole records the session in real time, with no endpoint monitoring driver.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes&lt;/strong&gt; consoles (&lt;code&gt;kubectl&lt;/code&gt;, &lt;code&gt;k9s&lt;/code&gt;, Lens) are brokered through a jump host with vaulted credentials and recording.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Local-first AI: no data leaves the perimeter
&lt;/h2&gt;

&lt;p&gt;Session telemetry is sensitive, so the AI layer calls a self-hosted Ollama instance on the same Docker network as the services. No external LLM API is involved.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-command risk scoring.&lt;/strong&gt; Commands in a live session get a risk score and level plus risk indicators, and high-risk commands can be blocked with a recorded reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavioural anomaly checks&lt;/strong&gt; run against session activity, and sessions roll up to an overall risk level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-session analysis.&lt;/strong&gt; After a session ends, the commands, deviations and MFA events are summarised by the local model, including average and peak risk scores and the riskiest commands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Natural-language search&lt;/strong&gt; over audit and access data runs through the same local model.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Tamper-evident audit evidence
&lt;/h2&gt;

&lt;p&gt;Each audit event gets a linked entry in an append-only chain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Hash&lt;/code&gt; is SHA-256 over the entry's id, action, entity type, timestamp, block index and previous hash.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PreviousHash&lt;/code&gt; links to the prior entry, with a sequential &lt;code&gt;BlockIndex&lt;/code&gt; per tenant. A Merkle root is stored for block verification.&lt;/li&gt;
&lt;li&gt;The chain lives in PostgreSQL. The data layer rejects any update or delete of chain entries, the API is append-only, and a foreign key prevents hard-deleting a source log that has a chain entry.&lt;/li&gt;
&lt;li&gt;A verification endpoint walks the chain and checks hashes, links and gaps, and a tamper-detection endpoint reports broken links.&lt;/li&gt;
&lt;li&gt;Session recordings get a SHA-256 hash at upload, and evidence packs reference recordings with their integrity hashes. Recordings can be retained in Azure Blob, S3 or GCS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The chain is tamper-evident, not tamper-proof: someone with raw database access can still rewrite rows, and verification is what catches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;Docs and a deployable demo are at &lt;a href="https://zenxsys.com" rel="noopener noreferrer"&gt;zenxsys.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
