<?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: Granite</title>
    <description>The latest articles on DEV Community by Granite (granite-so).</description>
    <link>https://dev.to/granite-so</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%2Forganization%2Fprofile_image%2F14307%2F1bade391-52e7-41a8-b485-21d4e2e1251c.jpeg</url>
      <title>DEV Community: Granite</title>
      <link>https://dev.to/granite-so</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/granite-so"/>
    <language>en</language>
    <item>
      <title>Read-Only Kubernetes Access for AI Agents: Why "Please Don't Delete Anything" Isn't a Security Boundary</title>
      <dc:creator>Alex Gorshkov</dc:creator>
      <pubDate>Tue, 18 Aug 2026 13:36:09 +0000</pubDate>
      <link>https://dev.to/granite-so/read-only-kubernetes-access-for-ai-agents-why-please-dont-delete-anything-isnt-a-security-2cem</link>
      <guid>https://dev.to/granite-so/read-only-kubernetes-access-for-ai-agents-why-please-dont-delete-anything-isnt-a-security-2cem</guid>
      <description>&lt;p&gt;I keep seeing the same pattern: take an LLM, give it access to &lt;code&gt;kubectl&lt;/code&gt; or the k8s API, write something like "you can only read, don't delete or modify anything" in the system prompt or an attached skill, and consider the problem solved. I went through this myself and at some point realized that this isn't a security boundary — it's a polite request.&lt;/p&gt;

&lt;p&gt;This isn't a hypothetical risk: you probably remember how in July 2025, the Replit agent deleted the SaaStr database despite a direct prohibition on making any changes — not Kubernetes and not MCP, but the same pattern. The "don't touch anything" instruction was right there in the context, and there was simply no one to enforce it except the model itself. Giving an agent write access to a k8s cluster means assembling exactly the same construct that already cost SaaStr their database.&lt;/p&gt;

&lt;p&gt;I'm far from the first to cover this topic, and a lot of read-only MCP servers have appeared recently. But it's surprising how often "read-only" is misunderstood in them. For example, in MCP servers for Kubernetes, "read-only" is often implemented as an environment variable that filters the &lt;code&gt;tools/list&lt;/code&gt; response, rather than as the absence of a function in the registry. That's exactly how &lt;code&gt;mcp-server-kubernetes&lt;/code&gt; (20k weekly downloads on npm) worked: the &lt;code&gt;ALLOW_ONLY_READONLY_TOOLS&lt;/code&gt; flag hid mutating tools from the list, while &lt;code&gt;tools/call&lt;/code&gt; still accepted &lt;code&gt;kubectl_delete&lt;/code&gt; directly, bypassing the filter.&lt;/p&gt;

&lt;p&gt;This became CVE-2026-46519, CVSS 8.8 — the same principle this article is about, taken to an actual exploit: a function hidden from the list is not the same as a function that doesn't exist. And this isn't just a community problem — Azure/mcp-kubernetes, Microsoft's official MCP server for Kubernetes, is built exactly the same way: &lt;code&gt;--access-level readonly|readwrite&lt;/code&gt; instead of simply not having mutating tools in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Instructions Don't Work as a Restriction
&lt;/h2&gt;

&lt;p&gt;A model is not a sandbox. If it has &lt;code&gt;delete_pod&lt;/code&gt; or &lt;code&gt;scale_deployment&lt;/code&gt; in its list of available tools, it can technically call it regardless of what's written in the system prompt. For example, an attacker doesn't need cluster access for this — a regular HTTP request with a spoofed header value like User-Agent is enough. Nginx or the app itself will log it as-is: Kubernetes simply captures the container's stdout/stderr without any sanitization.&lt;/p&gt;

&lt;p&gt;Then someone (or the assistant itself) asks the model to "check this pod's logs" — a completely innocuous request — and the model reads that line as part of the context, without distinguishing it from the system prompt. The same story applies to instructions from an attached skill or another plugin, which end up in the context as equally "trusted"; to jailbreaks; to ordinary hallucinations in an attempt to "fix" a problem you just asked it to explain. An instruction — whether in a system prompt, a skill, or a container log — is data that the model interprets, not code that constrains it.&lt;/p&gt;

&lt;p&gt;This means the only boundary that actually holds is which tools exist in the registry available to it. If the &lt;code&gt;delete_pod&lt;/code&gt; function doesn't exist, it doesn't matter what prompt injection, jailbreak, or the model itself in a fit of "helpfulness" says: there's nothing to call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Looks Like in a Tool Registry
&lt;/h2&gt;

&lt;p&gt;Let's take a concrete example: an MCP server for Kubernetes. In its registry, it makes sense to register only read tools — &lt;code&gt;list_pods&lt;/code&gt;, &lt;code&gt;list_deployments&lt;/code&gt;, &lt;code&gt;get_yaml&lt;/code&gt;, &lt;code&gt;get_events&lt;/code&gt;, &lt;code&gt;read_pod_logs&lt;/code&gt;, &lt;code&gt;start_pod_log_stream&lt;/code&gt;, and so on, around thirty in total. And not a single &lt;code&gt;delete_*&lt;/code&gt;, &lt;code&gt;scale_*&lt;/code&gt;, &lt;code&gt;exec_*&lt;/code&gt;, &lt;code&gt;apply_*&lt;/code&gt;, or &lt;code&gt;port_forward_*&lt;/code&gt; — not because they're disabled by some flag, but because those functions simply don't exist in the code.&lt;/p&gt;

&lt;p&gt;All mutating operations — scale, rollout restart, delete, cordon/drain — in this design live in a separate, human path: through a GUI with a confirmation dialog, through a CLI with an explicit flag or a "y/n" prompt. It doesn't matter which — what matters is that a human confirms it, and only then does a direct Kubernetes API call happen, without the model and without the AI tool registry at all. These are two different code paths, not one with a "allowed/forbidden" flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Server, Two Transports
&lt;/h2&gt;

&lt;p&gt;A separate problem arises when an AI assistant is available in two forms: as a built-in panel inside a larger tool, and as a standalone binary for external MCP clients (Claude Desktop, Claude Code, etc.). The temptation is to quickly assemble a separate set of tools for the built-in variant. Over time, the sets diverge, and one accidentally ends up with an extra tool that the other doesn't have.&lt;/p&gt;

&lt;p&gt;It's more reliable to run the same MCP server in both cases and communicate with it genuinely through the MCP protocol, just over different transports: in-memory for the built-in variant, stdio for the external client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shutdown&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;mcpserver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;serverTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clientTransport&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewInMemoryTransports&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serverTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Implementation&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"desktop-assistant"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clientTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// the same ListTools any external MCP client would call&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the codebase physically has one server and one list of read-only tools — not an original and a separate copy for the GUI that someone will forget about. In practice, this eliminates exactly one class of bugs: when six months of refactoring later, a tool gets added to one list and forgotten in the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read-Only Doesn't Mean "Nothing Is Visible"
&lt;/h2&gt;

&lt;p&gt;Read-only solves the problem of state mutation, but not the problem of leaking data that's already in the cluster. If your kubeconfig has access to read Secrets in a namespace, the model theoretically can too by calling &lt;code&gt;get_yaml&lt;/code&gt;. This needs to be addressed at the data level, not the prompt level: Secret values are redacted before the YAML reaches the tool response —&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;redacted&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— and the AI (whether the built-in assistant or the MCP client) physically never sees the decrypted value, because it's replaced with a placeholder before the YAML string is even constructed. In this design, it also makes sense to reject &lt;code&gt;apply&lt;/code&gt; if &lt;code&gt;&amp;lt;redacted&amp;gt;&lt;/code&gt; remains in the YAML — otherwise the placeholder could accidentally overwrite the real value.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Approach Doesn't Solve
&lt;/h2&gt;

&lt;p&gt;The model can still read a lot of data you already have RBAC access to. Read-only limits what can be done, not what can be seen within the same permissions.&lt;/p&gt;

&lt;p&gt;Load on the API server from a chatty tool-calling loop isn't limited by read-only alone — that requires reasonable measures (a limit on the number of iterations in a conversation, a byte limit on tool results, timeouts on log reads, capped and idle-reaped streams), not cryptographic guarantees.&lt;/p&gt;

&lt;p&gt;If keeping prompts and tool outputs on the machine entirely is important, that's a separate configuration (a local model via Ollama/vLLM/LM Studio), not a consequence of read-only architecture by itself.&lt;/p&gt;

&lt;p&gt;A compromised registry or poisoned tool descriptions are a separate issue: tool poisoning works against read-only tools too if the model trusts instructions inside a description just as much as the system prompt. A list of thirty read tools doesn't by itself guarantee that each one does exactly what it says — for a detailed breakdown of this topic, see "MCP and Agent Security."&lt;/p&gt;

&lt;p&gt;The entire approach described here is simple precisely because it deliberately doesn't solve the more general problem — giving an agent any write access at all. If you really need it (for example, for production debugging with the ability to fix things), that's a fundamentally different, much heavier architecture: whitelisting specific commands, rate-limiting, role-based restrictions, an immutable audit log with alerts.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>kubernetes</category>
      <category>security</category>
    </item>
    <item>
      <title>How Kubernetes Storage Actually Works - A Guide for Sysadmins</title>
      <dc:creator>Alex Gorshkov</dc:creator>
      <pubDate>Fri, 07 Aug 2026 22:51:47 +0000</pubDate>
      <link>https://dev.to/granite-so/how-kubernetes-storage-actually-works-a-guide-for-sysadmins-53i9</link>
      <guid>https://dev.to/granite-so/how-kubernetes-storage-actually-works-a-guide-for-sysadmins-53i9</guid>
      <description>&lt;p&gt;Pods are ephemeral. They restart, get rescheduled, get deleted - and everything written inside the container goes with them. That's fine for a stateless API. It's a disaster for a database, a log directory, or a file upload folder.&lt;/p&gt;

&lt;p&gt;Kubernetes solves this with a chain of abstractions that, at first glance, look like bureaucracy: a PVC that asks for a PV, a StorageClass that describes a PV you don't have yet, and a CSI driver that talks to something outside the cluster entirely. Once you see how the pieces hand off to each other, though, the design is straightforward - and, more usefully, so is debugging it.&lt;/p&gt;

&lt;p&gt;Here's the whole chain, end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The big picture
&lt;/h2&gt;

&lt;p&gt;The shortest possible summary of Kubernetes storage is: &lt;strong&gt;Pod → PVC → CSI → physical disk → PV&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you follow a request through the system, it travels along a very specific route. First, an application asks for storage by creating a &lt;strong&gt;PersistentVolumeClaim (PVC)&lt;/strong&gt; - essentially saying, "I need 5 GB that survives restarts." Kubernetes inspects this claim and searches for a compatible &lt;strong&gt;PersistentVolume (PV)&lt;/strong&gt; that already exists in the cluster. If a suitable PV isn't sitting idle, Kubernetes doesn't panic; it calls in a &lt;strong&gt;CSI driver&lt;/strong&gt; as an intermediary. The CSI driver translates the request and talks to the actual storage provider - a cloud block-storage API, a SAN, or an NFS appliance - in whatever language that provider speaks. Once the disk physically exists, the cluster &lt;strong&gt;binds&lt;/strong&gt; the newly created PV to the PVC, flipping the PVC status from &lt;code&gt;Pending&lt;/code&gt; to &lt;code&gt;Bound&lt;/code&gt;. Finally, the pod mounts the volume and gets its own persistent scratchpad for databases, logs, and files.&lt;/p&gt;

&lt;p&gt;The key insight that makes this entire architecture click: &lt;strong&gt;Kubernetes doesn't store your data.&lt;/strong&gt; It manages a reliable route to whoever does. Every component below exists to build and maintain one segment of that route.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSI and StorageClass: who creates the disk
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Container Storage Interface (CSI)&lt;/strong&gt; is the standard plugin interface that sits between Kubernetes and a storage backend. Kubernetes has no native idea how to carve out a volume on OpenStack Cinder, AWS EBS, or a Ceph cluster, and it doesn't need to. It simply calls the CSI driver and lets the driver deal with vendor-specific API calls. These drivers run as regular pods inside your cluster, and they are typically split into two distinct roles: a &lt;strong&gt;Controller&lt;/strong&gt; plugin, which handles the volume lifecycle (create, delete, resize, snapshot) and runs centrally, and a &lt;strong&gt;Node&lt;/strong&gt; plugin, which handles the messy work of attaching and mounting the volume on a specific host. The Node plugin runs as a DaemonSet, placing one instance on every schedulable node. This split isn't just an implementation detail; it’s the key to debugging. If the disk was never created, you have a Controller problem. If the disk exists but the pod won't start, it's almost certainly a Node problem.&lt;/p&gt;

&lt;p&gt;To tell Kubernetes &lt;em&gt;what kind&lt;/em&gt; of disk you want, you use a &lt;strong&gt;StorageClass&lt;/strong&gt;. Think of it as a configuration profile for your storage. It specifies the provisioner (which CSI driver to call), the &lt;code&gt;reclaimPolicy&lt;/code&gt; (what happens to the disk when the claim is deleted), and the &lt;code&gt;volumeBindingMode&lt;/code&gt;. The &lt;code&gt;volumeBindingMode&lt;/code&gt; is particularly critical because it dictates the timing of the disk creation. &lt;code&gt;Immediate&lt;/code&gt; mode creates the volume as soon as the PVC is created, regardless of whether any pod will ever use it. In a multi-zone cluster, this can be a trap because the volume might be created in an availability zone where the pod can’t run. &lt;code&gt;WaitForFirstConsumer&lt;/code&gt;, on the other hand, delays everything until a pod is scheduled. This ensures the disk lands in the same zone as the pod, avoiding a "volume stuck in the wrong zone" headache. The rest of the StorageClass is a set of parameters - options like disk type, IOPS tier, or replication settings - that are passed directly to the CSI driver.&lt;/p&gt;

&lt;p&gt;To see what your cluster offers, &lt;code&gt;kubectl get storageclass&lt;/code&gt; and &lt;code&gt;kubectl describe storageclass&lt;/code&gt; are your starting points. Pay close attention to which class is marked as default. Any PVC that doesn't explicitly name a &lt;code&gt;storageClassName&lt;/code&gt; silently uses that default, which is a fast track to accidentally provisioning production data onto a slow, non-replicated tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  PV and PVC: the claim and the volume
&lt;/h2&gt;

&lt;p&gt;These two objects are easy to mix up conceptually, so it helps to think of them as physical property vs. a ticket. A &lt;strong&gt;PersistentVolume (PV)&lt;/strong&gt; is the actual, provisioned piece of storage - a cluster-level resource, like a node. It's "the disk." A &lt;strong&gt;PersistentVolumeClaim (PVC)&lt;/strong&gt; is a namespaced request for storage with certain properties. It's "the ticket." The PVC specifies the size, access mode, and StorageClass it needs, and if a matching PV exists, Kubernetes binds them together.&lt;/p&gt;

&lt;p&gt;Binding isn't a fuzzy match; it requires three conditions to line up exactly. The StorageClass must match, the access modes must be compatible, and the capacity must be sufficient. If the PV is 10 GB and the PVC asks for 5 GB, they bind - but you lose access to the extra 5 GB because a PV can only satisfy a single claim. If any of these conditions fails, the PVC doesn't throw an error and it doesn't retry with looser requirements; it just sits in &lt;code&gt;Pending&lt;/code&gt; indefinitely. That silent waiting is the single most common "why won't my pod start" scenario in Kubernetes storage.&lt;/p&gt;

&lt;p&gt;You also need to understand the access modes, because they dictate what topologies your pods can use. &lt;code&gt;ReadWriteOnce&lt;/code&gt; (RWO) creates a volume that can only be mounted as read/write by a single node. This is a crucial distinction: the lock is per &lt;em&gt;node&lt;/em&gt;, not per &lt;em&gt;pod&lt;/em&gt;. Multiple pods on the same node can share an RWO volume just fine. However, if you scale a Deployment beyond one replica and the new pod lands on a different node, it will sit in &lt;code&gt;Pending&lt;/code&gt; forever because it can't reach the volume. &lt;code&gt;ReadWriteMany&lt;/code&gt; (RWX) lifts this restriction, allowing many nodes to mount the volume simultaneously, but this mode is typically only available with shared filesystems like NFS or CephFS, not standard block storage. &lt;code&gt;ReadOnlyMany&lt;/code&gt; (ROX) is the read-only counterpart, mountable across many nodes.&lt;/p&gt;

&lt;p&gt;When the PVC finally goes away, what happens to your data is dictated by the &lt;code&gt;reclaimPolicy&lt;/code&gt; on the StorageClass. The &lt;code&gt;Delete&lt;/code&gt; policy cleans up everything - the PV and the underlying disk in your cloud provider - the moment the PVC is removed. It’s convenient for CI and aggressively unforgiving in production. The &lt;code&gt;Retain&lt;/code&gt; policy preserves the data, leaving the PV in a &lt;code&gt;Released&lt;/code&gt; state. An administrator then has to manually tidy it up or re-attach it, making this the only safe choice for databases or anything containing state you’d be sad to lose. (You might see a &lt;code&gt;Recycle&lt;/code&gt; policy in legacy manifests, which wiped the data and put the PV back in the pool, but it’s deprecated in favor of dynamic provisioning).&lt;/p&gt;

&lt;p&gt;Broadly, provisioning happens in one of two ways. In &lt;strong&gt;Static&lt;/strong&gt; provisioning, an admin creates PV objects by hand, pointing to pre-existing disks - useful when managing an NFS export or specialized hardware outside the cluster. In &lt;strong&gt;Dynamic&lt;/strong&gt; provisioning, the PVC references a StorageClass, and the CSI driver creates the disk on demand without any pre-made PV objects. This is the default operational mode for practically every modern cluster. When things go wrong, &lt;code&gt;kubectl describe pvc &amp;lt;name&amp;gt;&lt;/code&gt; is the most valuable tool you have; the Events section at the bottom usually names the exact reason a binding or provisioning call failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mounting: what happens on the node
&lt;/h2&gt;

&lt;p&gt;Once the binding is resolved, the heavy lifting shifts to &lt;strong&gt;kubelet&lt;/strong&gt;, the agent on each node. When a pod with an attached volume starts, kubelet reads the pod spec, asks the CSI Node driver to attach the disk to the physical server, mounts the storage into the node’s global filesystem, and finally bind-mounts that specific directory into the container's isolated namespace.&lt;/p&gt;

&lt;p&gt;Every Kubernetes volume lands under a predictable, well-hidden path on the node: &lt;code&gt;/var/lib/kubelet/pods/&amp;lt;pod-uuid&amp;gt;/volumes/&lt;/code&gt;. Memorizing this path pays off when a pod claims its data is gone. You can SSH into the node and check the directory. An empty directory at that path means the mount step failed entirely. A populated directory that the container can’t see points to a problem in the container’s mount propagation settings or a simple typo in the &lt;code&gt;mountPath&lt;/code&gt; definition. This distinction saves hours of staring at YAML when you're under pressure.&lt;/p&gt;

&lt;p&gt;The pod spec connects everything with two interlocking sections: the &lt;code&gt;volumes&lt;/code&gt; block names the source, referencing the PVC by its claim name, and the &lt;code&gt;volumeMounts&lt;/code&gt; block inside the container spec declares where that volume appears inside the filesystem tree. The shared &lt;code&gt;name&lt;/code&gt; field is the only glue linking the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  A debugging checklist
&lt;/h2&gt;

&lt;p&gt;When storage misbehaves, walking the chain in order is the fastest way to find the single broken link. Always start by checking if the claim is bound: &lt;code&gt;kubectl get pvc -A&lt;/code&gt; and &lt;code&gt;kubectl describe pvc &amp;lt;name&amp;gt;&lt;/code&gt;, focusing on the Events section. If the PVC is stuck in &lt;code&gt;Pending&lt;/code&gt;, you immediately know the problem is that no matching PV exists, or the CSI Controller can't provision one - so you check the provisioner logs. Next, verify the volume itself with &lt;code&gt;kubectl get pv&lt;/code&gt; and &lt;code&gt;kubectl describe pv &amp;lt;name&amp;gt;&lt;/code&gt;. A mismatch here means looking at the StorageClass definition. If the PVC is &lt;code&gt;Bound&lt;/code&gt; but the pod is stuck in &lt;code&gt;ContainerCreating&lt;/code&gt;, the fault has moved to the node; &lt;code&gt;kubectl describe pod &amp;lt;name&amp;gt;&lt;/code&gt; will show attach or mount errors in its events, and you should be looking at the CSI Node pod logs on that specific node using &lt;code&gt;kubectl logs&lt;/code&gt;. The final fallback is the raw driver diagnostics, found with &lt;code&gt;kubectl -n kube-system get pods | grep csi&lt;/code&gt; to locate the controller and node pods, and &lt;code&gt;kubectl -n kube-system logs&lt;/code&gt; to see the verbose error from the CSI driver itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The whole system is one sequential handoff: you create a PVC; Kubernetes calls a CSI driver via the StorageClass; the CSI driver creates the real disk on the storage backend; a PV appears and binds to the PVC; the pod mounts the volume and starts writing. Once that chain - &lt;strong&gt;Pod → PVC → CSI → external storage → PV&lt;/strong&gt; - is in your head, the layers stop looking like ceremony. Each object owns exactly one segment of the route, which means every failure has exactly one place to look. And it's worth repeating the point that grounds the entire design: Kubernetes never stores your data. It builds and maintains a reliable path to whatever does.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>sysadmin</category>
    </item>
  </channel>
</rss>
