<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>PipelineClear — Notes on CI/CD &amp; Cloud Security Engineering</title>
    <link>https://pipelineclear.com/</link>
    <description>Field notes on CI/CD and cloud security engineering, written by Somesh Motupally — covering secret scanning, policy-as-code, and platform security tooling.</description>
    <language>en-us</language>
    <lastBuildDate>Thu, 24 Sep 2026 21:40:00 -0500</lastBuildDate>
    <item>
      <title>Network Policies: Default-Deny Without Breaking Everything</title>
      <link>https://pipelineclear.com/blog/network-policy-default-deny.html</link>
      <guid>https://pipelineclear.com/blog/network-policy-default-deny.html</guid>
      <pubDate>Thu, 24 Sep 2026 09:00:00 -0500</pubDate>
      <description>How we rolled out default-deny Kubernetes NetworkPolicies across namespaces — observing real traffic first, so the gates didn't take down production.</description>
      <content:encoded><![CDATA[<h2>The problem</h2>
  <p>Kubernetes networking is flat by default. Every pod can reach every other pod on any port — and beyond the cluster, kube-dns, the API server, and cloud metadata endpoints are all reachable unless something says otherwise. A compromised public-facing pod isn't contained to its own workload; it's a beachhead with a network view of everything.</p>
  <p>We saw this directly during a review: a frontend pod with no business touching the data layer could open a connection to the database port, and nothing in the network path objected. The same pod could reach the cloud metadata endpoint — the classic path to credential theft in a hijacked container. None of this violated any rule, because there were no rules. The policy-as-code gates we'd built controlled <em>what</em> could deploy; they said nothing about <em>who could talk to whom</em> once it was running.</p>

  <h2>Why lockdown-by-default wasn't enough</h2>
  <p>The instinctive fix — write NetworkPolicies that allow only what we could think of, apply them everywhere — failed the first time we tried it, and failed loudly. DNS resolution broke the moment kube-dns traffic (UDP/TCP port 53) wasn't in the allow-list. Health checks started failing. Legitimate inter-service calls dropped, and the pages came in at 2 AM. Within days, teams were asking for blanket exceptions, and some policies were quietly removed to stop the bleeding — which left us worse off than before: a network policy story on paper, and a flat network in practice.</p>
  <p>The deeper problem was that we were writing rules from guesses about traffic instead of from evidence of it. Nobody had a complete, current map of which service talks to which — and even if we had one, it would have gone stale the next sprint. There was also an uncomfortable discovery along the way: NetworkPolicies are only enforced if your CNI actually implements them. Apply a beautiful policy set on a CNI that silently ignores it, and you have the worst outcome — a false sense of containment.</p>

  <h2>The solution: default-deny, rolled out in stages</h2>
  <p>We went back at it with a staged approach: <strong>default-deny ingress and egress on every namespace, but rolled out one namespace at a time, with an observation phase before enforcement.</strong></p>

  <p>The rollout ran in four steps:</p>
  <ul>
    <li><strong>Verify the CNI enforces.</strong> Before writing a single policy, we confirmed our CNI (Calico in our case) actually enforces NetworkPolicy — a quick test policy against a scratch namespace proved it. A policy that isn't enforced is a liability disguised as a control.</li>
    <li><strong>Observe first, enforce second.</strong> We deployed the default-deny policies in audit/log mode first — Calico flow logs gave us a week of real traffic per namespace. That log, not our guesses, became the source of truth for the allow-lists.</li>
    <li><strong>Build allow-lists from observed flows.</strong> DNS to kube-dns (port 53) was carved out everywhere on day one. Then, per workload: ingress only from the ingress controller and the specific callers the logs showed; egress only to the dependencies the logs showed — the database port, the handful of external APIs, nothing else.</li>
    <li><strong>Enforce namespace by namespace.</strong> We started with the least sensitive namespaces and moved up. Each namespace got its own rollout, its own monitoring window, and a fast rollback path — so a misconfigured rule was a contained incident, never a cluster-wide outage.</li>
  </ul>

  <p>None of this was one big YAML drop. The policy set is versioned alongside the rest of the platform config, and the exception process is narrow and logged: if a workload genuinely needs a new path, the request names the source, destination, port, and reason, and it expires on a timer.</p>

  <h2>Results</h2>
  <ul>
    <li>Lateral movement across namespaces became structurally impossible — a compromised pod is contained to its own namespace's allow-listed paths, which are deliberately narrow.</li>
    <li>The metadata endpoint and database ports became unreachable from workloads that never needed them in the first place, closing off entire classes of container-escape and credential-theft paths.</li>
    <li>Because the allow-lists came from observed traffic, the rollout caused zero production incidents — a deliberate contrast with the first attempt.</li>
    <li>Exceptions stopped being silent. Every deviation from default-deny is now a logged, time-boxed, reviewed decision instead of a carve-out nobody remembers making.</li>
  </ul>

  <h2>Lessons learned</h2>
  <ul>
    <li><strong>DNS is the first thing that breaks.</strong> Carve out kube-dns (UDP/TCP 53) everywhere before you enforce anything else. Everyone learns this the hard way exactly once.</li>
    <li><strong>Egress is where the value is.</strong> Ingress is usually already fronted by an ingress controller with its own controls. The unguarded direction is outbound — that's where data leaves and credentials get stolen.</li>
    <li><strong>A week of flow logs beats a day of guessing.</strong> The observation phase was the whole difference between the failed rollout and the successful one. Traffic data is the spec.</li>
    <li><strong>Roll out per namespace, not per cluster.</strong> Small blast radius per step means a bad rule is a page, not an outage — and teams build trust in the process as each namespace lands cleanly.</li>
    <li><strong>A gate without an exception path gets bypassed.</strong> Same lesson as the admission gates: a narrow, logged, time-boxed override process keeps people going through policy instead of around it.</li>
    <li><strong>Verify enforcement before you trust it.</strong> A NetworkPolicy on a CNI that ignores it is decoration. Test it in a scratch namespace and watch the drop happen.</li>
  </ul>

  <h2>What's next</h2>
  <p>IP-and-port rules get us containment, but they're still coarse: they say nothing about <em>which workload</em> is on the other end of an allowed connection. The next step is identity-aware policy — using workload identity (via the service mesh's mTLS identities, or Cilium's identity-based policies) so rules say "the checkout service may talk to the payments API" instead of "anything in namespace A may reach port 443 in namespace B." That's the move from network segmentation to real zero-trust inside the cluster — and the SLSA provenance work from the previous post's roadmap is still queued behind it.</p>

  <hr>
  <p>If you've rolled out default-deny NetworkPolicies without taking down production, I'd like to hear how you handled the observation phase — <a href="mailto:hello@pipelineclear.com">get in touch</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Policy-as-Code Gates: Blocking Bad Deploys Before They Ship</title>
      <link>https://pipelineclear.com/blog/policy-as-code-gates.html</link>
      <guid>https://pipelineclear.com/blog/policy-as-code-gates.html</guid>
      <pubDate>Fri, 10 Jul 2026 09:00:00 -0500</pubDate>
      <description>How we used OPA and Kyverno to turn security policy from a document nobody read into an automatic gate that blocks non-compliant deploys.</description>
      <content:encoded><![CDATA[<h2>The problem</h2>
  <p>Security policy at most organizations lives in a document. "Containers must not run as root." "Images must come from an approved registry." "No privileged containers in production." These rules get written once, reviewed in an audit, and then quietly ignored — because nothing in the deployment path actually checks for them. A misconfigured manifest that violates every rule in the document will deploy exactly as successfully as one that follows all of them.</p>
  <p>We hit this directly: a security review found containers running as root, some with privileged access they didn't need, and images pulled from registries nobody had explicitly approved. None of this was malicious — it was just what happens when policy exists as prose instead of as something the pipeline actually enforces. By the time these findings surfaced, the workloads were already live.</p>

  <h2>Why manual review wasn't enough</h2>
  <p>The instinctive fix — add a manual security review step before production deploys — didn't scale and didn't match how the org actually shipped software. Reviewers became a bottleneck, review quality varied by who was on call, and the whole model depended on a human catching a YAML misconfiguration by eye, every time, under time pressure. We needed the check to happen automatically, consistently, and early enough that failing it was cheap — a rejected pull request, not an incident report.</p>

  <h2>The solution: policy gates with OPA and Kyverno</h2>
  <p>We built a layered policy enforcement setup using <strong>Open Policy Agent (OPA)</strong> for CI-time checks against Infrastructure-as-Code, and <strong>Kyverno</strong> as an admission controller enforcing the same intent at the Kubernetes cluster level — so a violation is caught whichever way it tries to reach production.</p>

  <p>The policies we enforced were deliberately the highest-leverage ones first:</p>
  <ul>
    <li><strong>No privileged containers</strong> — blocks any pod spec requesting privileged mode or unnecessary host access</li>
    <li><strong>Non-root enforcement</strong> — rejects containers that don't explicitly set a non-root user</li>
    <li><strong>Approved registry only</strong> — images must originate from our internal or explicitly allow-listed registries; anything else is rejected at admission</li>
    <li><strong>Signed image verification</strong> — using Sigstore/cosign, Kyverno verifies image signatures at admission time, so an unsigned or tampered image simply cannot run, regardless of how it got built</li>
    <li><strong>Mandatory resource limits</strong> — no container without CPU/memory limits defined, closing off a whole class of noisy-neighbor and cost issues alongside the security angle</li>
  </ul>

  <p>OPA runs earlier — against Terraform plans and Kubernetes manifests in CI, using Conftest — so violations get caught in a pull request with a clear, specific failure message before anything is ever applied. Kyverno acts as the backstop at the cluster's admission controller, so even a change that bypasses CI (a manual <code>kubectl apply</code>, a hotfix, a misconfigured pipeline elsewhere) still can't land a non-compliant workload. Neither layer alone was sufficient; together they close the gap between "policy that's written" and "policy that's actually true in production."</p>

  <h2>Results</h2>
  <ul>
    <li>Non-compliant manifests started getting rejected automatically at the PR stage, with the specific rule and reason surfaced directly to the engineer — no waiting on a reviewer's availability.</li>
    <li>Privileged and root-running containers in production dropped to effectively zero going forward, since the admission controller makes them structurally impossible to deploy, not just discouraged.</li>
    <li>Unsigned or unverified images became unable to run in the cluster at all, closing a real supply-chain gap rather than relying on trust in the build pipeline alone.</li>
    <li>Security review shifted from "find problems after the fact" to "maintain and extend the policy set" — a meaningfully different, more scalable use of the same team's time.</li>
  </ul>

  <h2>Lessons learned</h2>
  <ul>
    <li><strong>Enforce at more than one layer.</strong> CI-time checks are fast feedback for developers; admission-time checks are the actual guarantee. Relying on either alone leaves a gap someone will eventually find.</li>
    <li><strong>Start with rules nobody can reasonably argue with.</strong> "No privileged containers" and "must run as non-root" are hard to object to, which built trust in the gate before we introduced anything more opinionated.</li>
    <li><strong>Error messages are part of the policy.</strong> A rejected deploy with a vague "denied by policy" message trains people to route around the gate. A rejection that names the exact rule and the exact fix keeps the gate credible.</li>
    <li><strong>Image signing was the highest-value, latest addition.</strong> It directly answers "how do we know this image is what we think it is," which matters more every year as supply-chain attacks keep growing in frequency and sophistication.</li>
    <li><strong>A gate without an exception path gets bypassed.</strong> We built a narrow, logged, time-boxed override process for legitimate edge cases — without it, people find ways around policy instead of through it.</li>
  </ul>

  <h2>What's next</h2>
  <p>We're extending the policy set to cover network policy enforcement (default-deny with explicit allow rules) and tightening the signature verification requirement to include provenance attestation (SLSA-style build provenance), not just a valid signature — moving from "this image wasn't tampered with" to "this image was built by the pipeline we expect, from the source we expect."</p>

  <hr>
  <p>If you're building out policy-as-code enforcement with OPA, Kyverno, or a similar stack, I'd like to hear how you're approaching exception handling — <a href="mailto:hello@pipelineclear.com">get in touch</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Building org-wide, self-service security pipelines with reusable Harness templates</title>
      <link>https://pipelineclear.com/blog/cicd-security-templates.html</link>
      <guid>https://pipelineclear.com/blog/cicd-security-templates.html</guid>
      <pubDate>Mon, 09 Mar 2026 09:00:00 -0500</pubDate>
      <description>How we replaced inconsistent, team-by-team CI/CD security with versioned, account-level Harness templates that every pipeline inherits automatically.</description>
      <content:encoded><![CDATA[<h2>The problem</h2>
  <p>A year ago, our CI/CD security posture looked like most growing engineering orgs': inconsistent. Every team owned its own pipeline, and security controls — dependency scanning, secret detection, SAST, image scanning — were either bolted on ad hoc, copy-pasted from whichever team had last dealt with an audit finding, or missing entirely. Some pipelines ran Trivy. Others didn't scan container images at all. Secrets management practices varied wildly from team to team, and enforcing even a baseline policy meant chasing down dozens of pipeline owners individually.</p>
  <p>The result was predictable: security findings that should have been caught in minutes were surfacing in later-stage reviews or audits, engineers were duplicating the same scanning logic across pipelines with slightly different (and often outdated) configurations, and there was no single place to push a fix when a new CVE or policy requirement came down. When a new scanning requirement landed, rolling it out meant manually touching every pipeline across every team — a process that never actually reached full coverage.</p>

  <h2>Why ad hoc fixes weren't enough</h2>
  <p>Our first instinct was the obvious one: write documentation, share example YAML, ask teams to adopt it. That failed the way it usually does. Docs went stale. Teams copied a snapshot of a pipeline config and never pulled updates. Enforcement was voluntary, so coverage was inconsistent, and every security fix had to be manually re-propagated to every consumer. We needed the fix to live in one place and update everywhere automatically — which meant we needed a platform-level primitive, not a wiki page.</p>

  <h2>The solution: account-level reusable security templates</h2>
  <p>We rebuilt our security tooling as a set of <strong>account-level (org-wide) Harness CI/CD templates</strong> — versioned, centrally maintained pipeline building blocks that any team could pull into their own pipeline with a few lines of configuration, rather than hand-rolling scanning logic themselves.</p>
  <p>Each template encapsulated one security control as a reusable step or stage:</p>
  <ul>
    <li><strong>Secret scanning</strong> (Gitleaks) — runs on every push, gated to block merges on verified secret exposure</li>
    <li><strong>SAST</strong> — static analysis wired to fail builds on critical/high findings above an agreed threshold, tuned over time to keep false positives manageable</li>
    <li><strong>Dependency/SCA scanning</strong> — flags vulnerable dependencies and, where relevant, kicks off SBOM generation</li>
    <li><strong>Container image scanning</strong> (Trivy) — scans built images before they're allowed to push to the registry</li>
  </ul>
  <p>Teams consume these templates the same way they'd use any shared library: reference the template version, pass a handful of parameters (severity thresholds, exclusions, notification targets), and get the full control without writing or maintaining the underlying logic themselves. The majority of our pipelines run on GitHub Actions and Harness CI/CD, with the rest on Azure DevOps for specific tracking and logging workflows — so we designed the templates to be portable in principle, keeping the enforcement logic decoupled from any one platform's quirks.</p>
  <p>Critically, this isn't a "set it and forget it" library. We treat the template set as a living, contribution-driven system: when a new vulnerability class needs coverage, a scanning tool gets upgraded, or an audit surfaces a gap, the fix goes into the shared template once and every consuming pipeline inherits it on its next run. Engineers across the org regularly contribute enhancements back — new checks, tuned thresholds, support for a new artifact type — the same way you'd contribute to an internal open-source project.</p>

  <h2>Results</h2>
  <ul>
    <li>Manual pipeline security work dropped dramatically — what used to require touching every team's pipeline individually to roll out a new control now happens in one template update.</li>
    <li>Time-to-rollout for a new security requirement went from weeks to days, in some cases hours, since propagation is automatic for any pipeline referencing the shared template.</li>
    <li>Coverage became consistent org-wide instead of dependent on which team happened to prioritize it — every pipeline built on the templates gets the same baseline controls by default, not by request.</li>
    <li>Audit prep time shrank significantly, since we can point to a single, versioned source of truth for what security controls are enforced, rather than reconstructing per-team configurations.</li>
  </ul>

  <h2>Lessons learned</h2>
  <ul>
    <li><strong>Centralizing logic beats centralizing mandates.</strong> Asking teams to comply with a policy is weaker than giving them a default that's easier to adopt than to avoid.</li>
    <li><strong>Versioning matters as much as the controls themselves.</strong> Teams need the confidence to pin a version and upgrade on their own schedule, or adoption stalls.</li>
    <li><strong>Treat the template set as a product, not a policy doc.</strong> Regular contributions, a changelog, and an easy path for teams to request or add new checks kept engagement high and prevented the templates from going stale the way our original documentation did.</li>
    <li><strong>Start with the highest-leverage control, not the most controls.</strong> Secret scanning was our first template because it had the clearest, least controversial value — that early win built the trust needed to expand into SAST and image scanning later.</li>
  </ul>

  <h2>What's next</h2>
  <p>We're extending the template set to cover policy-as-code enforcement (OPA/Conftest-style checks) and working on making the templates genuinely platform-agnostic, so the same control logic can be consumed identically whether a team is on Harness, GitHub Actions, or Azure DevOps.</p>

  <hr>
  <p>If you're standardizing CI/CD security across a multi-team org, I'd love to hear how you approached it — <a href="mailto:hello@pipelineclear.com">get in touch</a>.</p>]]></content:encoded>
    </item>
  </channel>
</rss>
