<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Containers | Law Zava</title><link>https://lawzava.com/topics/containers/</link><description>Images, runtimes, isolation, and container security beyond the getting-started guide.</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 01 Sep 2026 19:24:17 +0000</lastBuildDate><atom:link href="https://lawzava.com/topics/containers/index.xml" rel="self" type="application/rss+xml"/><item><title>Container Scanning Without the Security Theater</title><link>https://lawzava.com/blog/2022-07-11-container-security-scanning/</link><pubDate>Mon, 11 Jul 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-07-11-container-security-scanning/</guid><description>Most container scanning setups generate noise, not security. Here is how to build a pipeline that actually catches what matters.</description><content:encoded><![CDATA[<p>I spent years in national cyber-defense before I got into startups. One thing that stuck with me: the gap between &ldquo;we have a scanner&rdquo; and &ldquo;we&rsquo;re secure&rdquo; is enormous. Most teams I&rsquo;ve seen have a container scanner somewhere in their CI pipeline. Most of them also have hundreds of unread findings, no triage process, and a vague feeling that they&rsquo;re covered.</p>
<p>They aren&rsquo;t.</p>
<h2 id="what-are-you-actually-scanning">What Are You Actually Scanning</h2>
<p>A container image is a layer cake of risk. The base OS, the system packages, the application dependencies, your code, and whatever config and secrets accidentally ended up in there. Scanning just the OS packages &ndash; which is what most default setups do &ndash; covers maybe 30% of the attack surface.</p>
<p>A useful scan covers:</p>
<ul>
<li>Base image and OS packages</li>
<li>Application dependencies (npm, pip, Go modules, whatever)</li>
<li>Misconfigurations &ndash; running as root, exposed ports, writable filesystems</li>
<li>Secrets embedded in image layers (it happens more than you think)</li>
</ul>
<p>If your scanner only does the first one, you have a false sense of security. Which is worse than no scanner at all, because at least without a scanner you know you&rsquo;re exposed.</p>
<h2 id="where-to-scan">Where to Scan</h2>
<p><strong>CI, before merge.</strong> This is where fixes are cheap. Block on critical findings. Let medium and low findings through with a warning. If you block on everything, developers will route around the scanner. I&rsquo;ve seen it happen at three different companies. The scanner becomes a rubber stamp or, worse, gets disabled.</p>
<p><strong>Registry, on a schedule.</strong> An image that was clean last Tuesday might have a critical CVE by Friday. Rescan images in the registry weekly at minimum. Alert on new critical findings. This is the part most teams skip, and it&rsquo;s the part that matters most for long-lived images.</p>
<p><strong>Admission control.</strong> Prevent unscanned images from running in production. This doesn&rsquo;t need to be complicated &ndash; a simple OPA policy or Kyverno rule that rejects images without a scan annotation. The goal is to close the gap between &ldquo;we scanned it&rdquo; and &ldquo;we&rsquo;re running it.&rdquo;</p>
<h2 id="prioritization-over-volume">Prioritization Over Volume</h2>
<p>A scanner that reports 500 findings isn&rsquo;t helping. It&rsquo;s generating noise. Prioritize by four factors:</p>
<ol>
<li><strong>Exploitability.</strong> Is there a known exploit in the wild? A theoretical vulnerability in a library you don&rsquo;t use isn&rsquo;t urgent.</li>
<li><strong>Exposure.</strong> Is this component internet-facing or buried behind three layers of internal networking?</li>
<li><strong>Criticality.</strong> Is this your payment service or your internal metrics dashboard?</li>
<li><strong>Fix availability.</strong> If there&rsquo;s no patch, what exactly do you want the team to do about it?</li>
</ol>
<p>Handle exceptions with documented waivers &ndash; owner, reason, expiry date. No permanent exceptions. If a waiver expires and nobody renews it, the finding blocks again.</p>
<h2 id="base-images-are-dependencies">Base Images Are Dependencies</h2>
<p>Treat them like one. Pin versions. Set a monthly upgrade cadence. Use minimal bases &ndash; <code>distroless</code> or <code>alpine</code> &ndash; not because they&rsquo;re trendy but because fewer packages means fewer CVEs means less noise.</p>
<p>I&rsquo;ve seen teams running Ubuntu 20.04 base images with 200+ packages when their Go binary needs exactly zero of them. That isn&rsquo;t a base image. That&rsquo;s an attack surface donation.</p>
<h2 id="sign-your-images">Sign Your Images</h2>
<p>Scanning tells you what&rsquo;s inside an image. Signing tells you it&rsquo;s the image you built and nobody tampered with it between your CI pipeline and your cluster. Use cosign, use Notary, use whatever &ndash; but sign your artifacts and verify on pull.</p>
<p>After the SolarWinds and codecov incidents, &ldquo;trust but don&rsquo;t verify&rdquo; isn&rsquo;t a defensible position anymore.</p>
<h2 id="the-minimum-viable-pipeline">The Minimum Viable Pipeline</h2>
<ol>
<li>Scan in CI with Trivy or Grype. Block on critical, warn on high.</li>
<li>Rescan in your registry weekly. Alert on new criticals.</li>
<li>Admission control in Kubernetes. Reject unscanned images.</li>
<li>Pin and regularly update base images.</li>
<li>Sign images in CI. Verify at deploy.</li>
</ol>
<p>That&rsquo;s it. Five steps. None of them are hard individually. The hard part is maintaining the discipline to triage findings, update waivers, and keep base images current. Discipline over heroics, as always.</p>
]]></content:encoded></item><item><title>Hardening Kubernetes: The Stuff That Actually Matters</title><link>https://lawzava.com/blog/2022-02-07-kubernetes-security-hardening/</link><pubDate>Mon, 07 Feb 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-02-07-kubernetes-security-hardening/</guid><description>Kubernetes defaults favor convenience over security. A layered hardening guide covering pods, RBAC, network policies, secrets, and the control plane.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Kubernetes out of the box is optimized for convenience, not security. Lock down pods, network, RBAC, secrets, and the control plane as separate layers. Assume every layer will be tested. My national cyber-defense background taught me defense in depth isn&rsquo;t a buzzword &ndash; it&rsquo;s the only model that survives contact with reality.</p>
<p>Kubernetes ships with defaults designed to get your workloads running. Not to resist a motivated attacker. Not to survive a leaked credential. Not to contain a compromised container. If you&rsquo;re running a production cluster with default settings, you&rsquo;re running a cluster that trusts everything by default. That should scare you.</p>
<p>I&rsquo;ve hardened clusters across multiple organizations and I keep seeing the same gaps. This post walks through the layers that matter, with config you can actually apply. Some of this comes from my time working with national cyber-defense systems, where &ldquo;assume breach&rdquo; wasn&rsquo;t a thought experiment &ndash; it was the operating model.</p>
<h2 id="start-with-a-threat-model-seriously">Start with a threat model (seriously)</h2>
<p>I know. Nobody wants to do this part. But every security control you skip because &ldquo;we&rsquo;re not a bank&rdquo; is a control an attacker won&rsquo;t encounter. You don&rsquo;t need a 40-page document. You need answers to three questions:</p>
<ol>
<li>What data would hurt you if it leaked?</li>
<li>Who has access to your cluster, and how?</li>
<li>If a container gets compromised, what can the attacker reach?</li>
</ol>
<p>If you can&rsquo;t answer these quickly, start there before touching any YAML.</p>
<h2 id="pod-security-the-non-negotiable-baseline">Pod security: the non-negotiable baseline</h2>
<p>Every container should run with the least privilege possible. Full stop. Here is what that looks like in practice:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Pod</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">app</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runAsNonRoot</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runAsUser</span>: <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">fsGroup</span>: <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">app</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">image</span>: <span style="color:#ae81ff">myregistry/app:v1.2.3@sha256:abc123...</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">allowPrivilegeEscalation</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">readOnlyRootFilesystem</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">capabilities</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">drop</span>: [<span style="color:#e6db74">&#34;ALL&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;100m&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;128Mi&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;500m&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;256Mi&#34;</span>
</span></span></code></pre></div><p>Key points:</p>
<ul>
<li><strong><code>runAsNonRoot: true</code></strong> &ndash; if your app needs root, fix the app. I&rsquo;ve seen exactly one legitimate case for running as root in a non-system container in the last three years.</li>
<li><strong>Drop ALL capabilities</strong> &ndash; then add back only what&rsquo;s needed. Most applications need zero Linux capabilities.</li>
<li><strong>Read-only root filesystem</strong> &ndash; forces you to use volumes for writable paths, which is where you want writes anyway.</li>
<li><strong>Resource limits</strong> &ndash; not just for cost. An unbound container can starve the node and affect every other workload on it.</li>
<li><strong>Pin image digests</strong> &ndash; <code>latest</code> isn&rsquo;t a version. Pin to a SHA256 digest for anything that matters.</li>
</ul>
<p>PodSecurityPolicy is deprecated as of 1.21 and removed in 1.25. Replace it. Pod Security Admission (beta in 1.23) implements three levels &ndash; privileged, baseline, restricted. At minimum, enforce <code>baseline</code> cluster-wide and <code>restricted</code> on sensitive namespaces:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Namespace</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pod-security.kubernetes.io/enforce</span>: <span style="color:#ae81ff">restricted</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pod-security.kubernetes.io/audit</span>: <span style="color:#ae81ff">restricted</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pod-security.kubernetes.io/warn</span>: <span style="color:#ae81ff">restricted</span>
</span></span></code></pre></div><p>For more granular control, OPA Gatekeeper or Kyverno let you write custom policies. I prefer Kyverno for readability, but both work.</p>
<h2 id="network-policies-default-deny-then-allow">Network policies: default deny, then allow</h2>
<p>Kubernetes networking is flat by default. Every pod can talk to every other pod. That&rsquo;s fantastic for an attacker who compromises one service and wants to pivot laterally.</p>
<p>Start with a default deny policy on every namespace:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">NetworkPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">default-deny-all</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">podSelector</span>: {}
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">policyTypes</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">Ingress</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">Egress</span>
</span></span></code></pre></div><p>Then explicitly allow what&rsquo;s needed:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">NetworkPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">allow-app-to-db</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">podSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">backend</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">policyTypes</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">Egress</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">egress</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">to</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">podSelector</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">app</span>: <span style="color:#ae81ff">postgres</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">protocol</span>: <span style="color:#ae81ff">TCP</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">port</span>: <span style="color:#ae81ff">5432</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">to</span>:  <span style="color:#75715e"># DNS</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">namespaceSelector</span>: {}
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">protocol</span>: <span style="color:#ae81ff">UDP</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">port</span>: <span style="color:#ae81ff">53</span>
</span></span></code></pre></div><p>A critical gotcha: NetworkPolicy only works if your CNI supports it. Flannel doesn&rsquo;t enforce network policies. Calico, Cilium, and Weave do. Verify enforcement in every environment, including staging. I&rsquo;ve seen teams spend weeks writing policies that were silently ignored because the CNI was wrong.</p>
<p>Don&rsquo;t forget egress. Ingress gets all the attention, but an attacker who compromises a pod and can make outbound HTTPS calls to the internet has an exfiltration path. Restrict egress to known destinations.</p>
<h2 id="rbac-strict-and-boring">RBAC: strict and boring</h2>
<p>Good RBAC is boring. It should be so restrictive that nobody notices it until they try to do something they shouldn&rsquo;t.</p>
<ul>
<li>One service account per workload. Not one per namespace. Not the default service account.</li>
<li>Scope roles to namespaces and specific resources. Cluster-wide roles are for platform teams, not applications.</li>
<li>Disable automatic service account token mounting for pods that don&rsquo;t need to talk to the API server:</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ServiceAccount</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">app-sa</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">automountServiceAccountToken</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></div><p>For human access, use OIDC with your identity provider. Short-lived tokens from <code>kubectl</code> via OIDC are vastly better than long-lived kubeconfig certificates. Nobody should have <code>cluster-admin</code> in production unless they&rsquo;re actively performing cluster operations, and even then, consider just-in-time access.</p>
<h2 id="secrets-they-arent-encrypted">Secrets: they aren&rsquo;t encrypted</h2>
<p>This surprises people. Kubernetes Secrets are base64-encoded in etcd. That&rsquo;s encoding, not encryption. Anyone with etcd access can read every secret in your cluster.</p>
<p>Fix this in layers:</p>
<ol>
<li><strong>Encrypt etcd at rest.</strong> Configure <code>EncryptionConfiguration</code> on the API server with <code>aescbc</code> or <code>secretbox</code> providers.</li>
<li><strong>Use an external secrets manager.</strong> HashiCorp Vault, AWS Secrets Manager, or GCP Secret Manager with an operator like External Secrets. The secret value never lives in etcd.</li>
<li><strong>Scope access.</strong> Only the service accounts that need a secret should be able to read it. RBAC applies to secrets too.</li>
<li><strong>Rotate.</strong> If you can&rsquo;t rotate a secret without a deployment, fix your rotation process before you fix the secret.</li>
</ol>
<p>Never bake secrets into container images. Never put them in ConfigMaps. Never log them. These sound obvious, but I find violations in almost every cluster audit.</p>
<h2 id="control-plane-hygiene">Control plane hygiene</h2>
<p>The API server is the front door to everything. Lock it down:</p>
<ul>
<li>Disable anonymous authentication (<code>--anonymous-auth=false</code>).</li>
<li>Enable audit logging. You need to know who did what and when.</li>
<li>Restrict API server access to trusted networks. If your API server is reachable from the public internet, you have a bigger problem than this blog post covers.</li>
<li>Keep etcd on a private network, TLS-encrypted, and only reachable by the API server.</li>
<li>Treat kubeconfig files like production database credentials. Because that&rsquo;s what they&rsquo;re.</li>
</ul>
<p>If you&rsquo;re on a managed service (EKS, GKE, AKS), the provider handles some of this. But &ldquo;managed&rdquo; doesn&rsquo;t mean &ldquo;secure.&rdquo; Review what they expose and what they lock down. I&rsquo;ve seen managed clusters with surprisingly permissive defaults.</p>
<h2 id="image-supply-chain">Image supply chain</h2>
<p>Stop using <code>latest</code>. Stop using mutable tags. Every production image should be:</p>
<ul>
<li>Built from a minimal base (distroless, Alpine, or scratch for Go binaries).</li>
<li>Scanned for vulnerabilities in CI before it reaches a registry.</li>
<li>Signed, ideally with cosign or Notary.</li>
<li>Pulled from a private registry with access controls.</li>
</ul>
<p>If you&rsquo;re pulling public images directly into production, you&rsquo;re trusting that Docker Hub (or whoever) hasn&rsquo;t been compromised. That trust should make you uncomfortable.</p>
<h2 id="node-level-protections">Node-level protections</h2>
<p>Nodes are part of your trust boundary. A compromised node means compromised pods.</p>
<ul>
<li>Use a minimal OS (Bottlerocket, Flatcar, Talos). Less software means fewer attack vectors.</li>
<li>Patch regularly. Automate node rotation if you can.</li>
<li>Lock down the kubelet API. Disable anonymous kubelet auth.</li>
<li>Apply seccomp profiles. The <code>RuntimeDefault</code> profile is a good starting point and blocks a significant number of syscalls:</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">seccompProfile</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">RuntimeDefault</span>
</span></span></code></pre></div><p>Runtime detection tools like Falco are worth considering. They won&rsquo;t prevent an attack, but they can alert you when something unexpected happens &ndash; a shell spawning inside a container, an unusual network connection, a binary that shouldn&rsquo;t exist being executed.</p>
<h2 id="monitoring-and-incident-response">Monitoring and incident response</h2>
<p>Hardening without visibility is just hoping. You need:</p>
<ul>
<li>Centralized audit logs from the API server.</li>
<li>Alerts on failed authentication attempts and privilege escalations.</li>
<li>Monitoring for unexpected image pulls or outbound connections.</li>
<li>A written incident response playbook. Not a novel. A one-page runbook that answers: who gets paged, what gets isolated, how do we preserve evidence.</li>
</ul>
<p>Write the playbook before you need it. During an actual incident isn&rsquo;t the time to figure out who has cluster access.</p>
<h2 id="the-operating-discipline">The operating discipline</h2>
<p>None of this works as a one-time project. Security is operational:</p>
<ul>
<li>All cluster config changes go through code review. GitOps is your friend here.</li>
<li>Stay on supported Kubernetes versions. Running an EOL version means running with known unpatched vulnerabilities.</li>
<li>Test your controls in staging. I&rsquo;ve seen policies that worked perfectly in staging and failed silently in production because the namespace labels were wrong.</li>
<li>Run backup and restore drills. Not &ldquo;we&rsquo;ve Velero installed.&rdquo; Actually restore from backup and verify the cluster works.</li>
</ul>
<p>Kubernetes hardening isn&rsquo;t a checklist you complete once. It&rsquo;s a set of defaults you enforce continuously, across pods, network, identities, secrets, and the control plane. When those defaults are solid, incidents become containable instead of catastrophic.</p>
]]></content:encoded></item><item><title>Your Container Image Scan Passed. Now What?</title><link>https://lawzava.com/blog/2020-11-30-container-runtime-security/</link><pubDate>Mon, 30 Nov 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-11-30-container-runtime-security/</guid><description>Image scanning tells you what&amp;amp;rsquo;s in the box. Runtime security tells you what it&amp;amp;rsquo;s doing. How we lock down containers with seccomp, network policies, and Falco.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Your image scan is green. Congratulations. You&rsquo;ve answered &ldquo;what&rsquo;s in this container.&rdquo; You haven&rsquo;t answered &ldquo;what is this container doing right now.&rdquo; Runtime security is how you answer that second question &ndash; through least privilege, kernel-level controls, network policy, and behavioral detection. This post walks through exactly how we do it at Decloud, with real configs.</p>
<hr>
<p>I&rsquo;ve had this conversation too many times. Someone shows me a green Trivy scan and says &ldquo;we&rsquo;re secure.&rdquo; No. You know what&rsquo;s <em>in</em> the image. You have no idea what it&rsquo;s <em>doing</em>.</p>
<p>At Decloud we run a lot of containers. Hundreds of services. And after spending time in national cyber-defense, I came away with one conviction that shapes everything: assume the perimeter is already breached. The question isn&rsquo;t whether something gets in. It&rsquo;s how much damage it can do once it&rsquo;s there.</p>
<p>That&rsquo;s runtime security. Not a product. Not a checkbox. A set of controls that limit what containers can do, watch what they&rsquo;re doing, and react when behavior goes sideways.</p>
<h2 id="what-image-scanning-doesnt-catch">What image scanning doesn&rsquo;t catch</h2>
<p>Static scanning is useful. I&rsquo;m not dismissing it. But it&rsquo;s blind to:</p>
<ul>
<li>Zero-days. By definition your scanner doesn&rsquo;t know about them yet.</li>
<li>Stolen credentials being used inside a running container.</li>
<li>Lateral movement. A compromised container talking to your database.</li>
<li>Data exfiltration over DNS or unexpected outbound connections.</li>
<li>Runtime drift. Someone <code>kubectl exec</code>&rsquo;d in and installed curl. Your image is clean. Your container isn&rsquo;t.</li>
</ul>
<p>The mental model shift is important. Scanning is about <em>contents</em>. Runtime security is about <em>behavior</em>.</p>
<h2 id="what-suspicious-actually-looks-like-in-production">What &ldquo;suspicious&rdquo; actually looks like in production</h2>
<p>Forget movie-hacker nonsense. In real production clusters, compromise looks boring:</p>
<ul>
<li>A shell spawns in a container that should only run a Go binary.</li>
<li>A container starts making outbound connections to IPs it&rsquo;s never talked to.</li>
<li>Something writes to <code>/etc/passwd</code> or drops a new binary in <code>/tmp</code>.</li>
<li>CPU spikes because someone&rsquo;s mining Monero in your API pod.</li>
<li>A container that&rsquo;s been running for 40 days suddenly starts probing the Kubernetes API.</li>
</ul>
<p>None of these trip an image scanner. All of them should wake someone up.</p>
<h2 id="start-with-the-dockerfile-seriously">Start with the Dockerfile. Seriously.</h2>
<p>Before you install Falco or write a single policy, harden the image itself. This is where most teams skip steps and then wonder why their runtime security is noisy.</p>
<p>Here&rsquo;s what a production Dockerfile should look like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">golang:1.15-alpine</span> <span style="color:#66d9ef">AS</span> <span style="color:#e6db74">builder</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">WORKDIR</span> <span style="color:#e6db74">/app</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> go.mod go.sum ./<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> go mod download<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> . .<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> CGO_ENABLED<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> GOOS<span style="color:#f92672">=</span>linux go build -o /server .<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">scratch</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> --from<span style="color:#f92672">=</span>builder /server /server<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> --from<span style="color:#f92672">=</span>builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">USER</span> <span style="color:#e6db74">65534:65534</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENTRYPOINT</span> [<span style="color:#e6db74">&#34;/server&#34;</span>]<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Key things here: multi-stage build so no compiler or tools in the final image. <code>scratch</code> base so there&rsquo;s literally no shell, no package manager, no <code>curl</code>, no nothing. Non-root user. If an attacker gets code execution in this container, they land in an empty filesystem with no tools and no root. Good luck.</p>
<p>At Decloud we moved almost all Go services to <code>scratch</code> or <code>distroless</code>. It eliminates entire categories of post-exploitation. Can&rsquo;t spawn a shell if there&rsquo;s no shell.</p>
<h2 id="pod-security-context-the-non-negotiable-baseline">Pod security context: the non-negotiable baseline</h2>
<p>Every pod in your cluster should have this. No exceptions.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">runAsNonRoot</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">runAsUser</span>: <span style="color:#ae81ff">65534</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">runAsGroup</span>: <span style="color:#ae81ff">65534</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">allowPrivilegeEscalation</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">readOnlyRootFilesystem</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">capabilities</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">drop</span>: [<span style="color:#e6db74">&#34;ALL&#34;</span>]
</span></span></code></pre></div><p><code>readOnlyRootFilesystem</code> is the one people push back on. &ldquo;But my app writes temp files!&rdquo; Fine. Mount a single <code>emptyDir</code> for <code>/tmp</code> and nothing else. Don&rsquo;t give up the whole filesystem because one library wants to write a cache file.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">tmp</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/tmp</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">tmp</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">emptyDir</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">sizeLimit</span>: <span style="color:#ae81ff">64Mi</span>
</span></span></code></pre></div><p>At Decloud, we enforce this via OPA Gatekeeper. If your pod spec requests <code>privileged: true</code> or doesn&rsquo;t drop all capabilities, the admission controller rejects it. Period.</p>
<h2 id="seccomp-the-kernel-level-lockdown-most-people-ignore">Seccomp: the kernel-level lockdown most people ignore</h2>
<p>Here&rsquo;s the thing about Linux containers &ndash; they share the host kernel. Every syscall your container makes goes through the same kernel as every other container on that node. Seccomp lets you whitelist which syscalls a container is allowed to make.</p>
<p>The default Docker seccomp profile blocks about 44 syscalls. That&rsquo;s a start. But for a well-behaved Go HTTP server, you need maybe 40-50 syscalls total out of the 300+ available. Everything else should be denied.</p>
<p>Here&rsquo;s a stripped-down seccomp profile we use for stateless API services:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;defaultAction&#34;</span>: <span style="color:#e6db74">&#34;SCMP_ACT_ERRNO&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;architectures&#34;</span>: [<span style="color:#e6db74">&#34;SCMP_ARCH_X86_64&#34;</span>],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;syscalls&#34;</span>: [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;names&#34;</span>: [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;accept4&#34;</span>, <span style="color:#e6db74">&#34;bind&#34;</span>, <span style="color:#e6db74">&#34;brk&#34;</span>, <span style="color:#e6db74">&#34;clone&#34;</span>, <span style="color:#e6db74">&#34;close&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;connect&#34;</span>, <span style="color:#e6db74">&#34;epoll_create1&#34;</span>, <span style="color:#e6db74">&#34;epoll_ctl&#34;</span>, <span style="color:#e6db74">&#34;epoll_pwait&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;exit_group&#34;</span>, <span style="color:#e6db74">&#34;fcntl&#34;</span>, <span style="color:#e6db74">&#34;fstat&#34;</span>, <span style="color:#e6db74">&#34;futex&#34;</span>, <span style="color:#e6db74">&#34;getpeername&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;getpid&#34;</span>, <span style="color:#e6db74">&#34;getsockname&#34;</span>, <span style="color:#e6db74">&#34;getsockopt&#34;</span>, <span style="color:#e6db74">&#34;listen&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;madvise&#34;</span>, <span style="color:#e6db74">&#34;mmap&#34;</span>, <span style="color:#e6db74">&#34;mprotect&#34;</span>, <span style="color:#e6db74">&#34;munmap&#34;</span>, <span style="color:#e6db74">&#34;nanosleep&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;openat&#34;</span>, <span style="color:#e6db74">&#34;read&#34;</span>, <span style="color:#e6db74">&#34;recvfrom&#34;</span>, <span style="color:#e6db74">&#34;rt_sigaction&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;rt_sigprocmask&#34;</span>, <span style="color:#e6db74">&#34;rt_sigreturn&#34;</span>, <span style="color:#e6db74">&#34;sched_yield&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;sendto&#34;</span>, <span style="color:#e6db74">&#34;setsockopt&#34;</span>, <span style="color:#e6db74">&#34;sigaltstack&#34;</span>, <span style="color:#e6db74">&#34;socket&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;tgkill&#34;</span>, <span style="color:#e6db74">&#34;write&#34;</span>
</span></span><span style="display:flex;"><span>      ],
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;action&#34;</span>: <span style="color:#e6db74">&#34;SCMP_ACT_ALLOW&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Apply it in your pod spec:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">seccompProfile</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Localhost</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">localhostProfile</span>: <span style="color:#ae81ff">profiles/api-server.json</span>
</span></span></code></pre></div><p>This is defense in depth in the truest sense. Even if an attacker gets arbitrary code execution, they can&rsquo;t call <code>ptrace</code>, can&rsquo;t load kernel modules, can&rsquo;t mount filesystems. The kernel says no. I picked this habit up from national cyber-defense work &ndash; you don&rsquo;t rely on one layer. You assume each layer will fail and build the next one.</p>
<p>Building these profiles by hand is tedious. We generate them by running the app under <code>strace</code> in a staging environment, collecting the syscalls it actually uses, and then adding a small buffer. Not glamorous. Works.</p>
<h2 id="network-policy-because-default-kubernetes-networking-is-terrifying">Network policy: because default Kubernetes networking is terrifying</h2>
<p>Out of the box, every pod in a Kubernetes cluster can talk to every other pod. Let that sink in. Your frontend can talk to your payment service. Your batch job can reach your secrets manager. It&rsquo;s a flat network with no segmentation.</p>
<p>Network policies fix this. Here&rsquo;s what we apply to every namespace at Decloud:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">NetworkPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">default-deny-all</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">podSelector</span>: {}
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">policyTypes</span>: [<span style="color:#e6db74">&#34;Ingress&#34;</span>, <span style="color:#e6db74">&#34;Egress&#34;</span>]
</span></span></code></pre></div><p>This denies everything by default. Then we whitelist specific flows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">NetworkPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-policy</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">podSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">policyTypes</span>: [<span style="color:#e6db74">&#34;Ingress&#34;</span>, <span style="color:#e6db74">&#34;Egress&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ingress</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">from</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">podSelector</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">app</span>: <span style="color:#ae81ff">gateway</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">protocol</span>: <span style="color:#ae81ff">TCP</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">port</span>: <span style="color:#ae81ff">8080</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">egress</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">to</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">podSelector</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">app</span>: <span style="color:#ae81ff">postgres</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">protocol</span>: <span style="color:#ae81ff">TCP</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">port</span>: <span style="color:#ae81ff">5432</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">to</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">namespaceSelector</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kube-system</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">protocol</span>: <span style="color:#ae81ff">UDP</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">port</span>: <span style="color:#ae81ff">53</span>
</span></span></code></pre></div><p>That last rule is DNS. People forget DNS and then spend an hour wondering why their app can&rsquo;t resolve anything. Ask me how I know.</p>
<p>The egress rules are the important ones for runtime security. If a compromised container can&rsquo;t make outbound connections, the attacker can&rsquo;t exfiltrate data or download tools. It&rsquo;s boring. It works.</p>
<h2 id="falco-watching-what-your-containers-actually-do">Falco: watching what your containers actually do</h2>
<p>Falco hooks into the kernel via eBPF (or a kernel module) and watches syscalls in real time. It&rsquo;s the closest thing to &ldquo;what is this container actually doing right now.&rdquo;</p>
<p>Here are the rules we run in production at Decloud:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">rule</span>: <span style="color:#ae81ff">Shell in non-shell container</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">desc</span>: <span style="color:#ae81ff">Detect shell spawned in a container that shouldn&#39;t have one</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">condition</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    spawned_process and container
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and proc.name in (sh, bash, zsh, dash, csh)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and not container.image.repository in (debug-tools, ops-shell)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">output</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Shell started in production container
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    (user=%user.name container=%container.name
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    image=%container.image.repository command=%proc.cmdline)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">priority</span>: <span style="color:#ae81ff">CRITICAL</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">rule</span>: <span style="color:#ae81ff">Write below /bin or /usr</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">desc</span>: <span style="color:#ae81ff">Detect new binary written to system paths</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">condition</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    open_write and container
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and (fd.name startswith /bin/ or fd.name startswith /usr/bin/
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    or fd.name startswith /usr/sbin/)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">output</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Binary written in container
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    (file=%fd.name container=%container.name image=%container.image.repository)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">priority</span>: <span style="color:#ae81ff">CRITICAL</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">rule</span>: <span style="color:#ae81ff">Outbound connection to unexpected port</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">desc</span>: <span style="color:#ae81ff">Detect container connecting to non-standard ports</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">condition</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    outbound and container
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and not fd.sport in (80, 443, 5432, 6379, 8080, 8443, 9090)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and not container.image.repository in (monitoring-agent)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">output</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Unexpected outbound connection
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    (container=%container.name port=%fd.sport dest=%fd.sip)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">priority</span>: <span style="color:#ae81ff">WARNING</span>
</span></span></code></pre></div><p>Two things I&rsquo;ve learned the hard way about Falco rules:</p>
<p><strong>Be specific.</strong> Generic rules like &ldquo;alert on any process spawn&rdquo; will bury you in noise within hours. You&rsquo;ll disable the alerts and then you&rsquo;re back to flying blind.</p>
<p><strong>Tune per workload.</strong> Your Nginx sidecar legitimately spawns worker processes. Your Go API server doesn&rsquo;t. Same rule, different context. We maintain per-service rule overrides and review them quarterly.</p>
<h2 id="incident-response-what-happens-when-something-fires">Incident response: what happens when something fires</h2>
<p>Detection without response is a hobby, not a security program. Here&rsquo;s our playbook at Decloud:</p>
<ol>
<li>
<p><strong>CRITICAL alert fires.</strong> PagerDuty wakes someone up. Shell in a production container or binary write to system paths &ndash; someone looks at it within minutes.</p>
</li>
<li>
<p><strong>Capture state before killing.</strong> <code>kubectl logs</code>, <code>kubectl describe pod</code>, network connection dumps. If you kill the pod first, you&rsquo;ve destroyed your forensics.</p>
</li>
<li>
<p><strong>Network isolate.</strong> Apply a deny-all network policy to the specific pod. The container is still running but can&rsquo;t talk to anything. This buys time.</p>
</li>
<li>
<p><strong>Investigate.</strong> Was it a legitimate deploy? A developer debugging? Or actual compromise? Most of our critical alerts turn out to be config mistakes. That&rsquo;s fine. I&rsquo;d rather investigate 10 false positives than miss one real incident.</p>
</li>
<li>
<p><strong>Kill and rotate.</strong> If it&rsquo;s real, kill the pod, rotate every credential it had access to, and check the blast radius.</p>
</li>
</ol>
<p>One rule we follow strictly: no automated kills on low-confidence alerts. I&rsquo;ve seen teams auto-restart pods on anomaly detection. They ended up killing healthy services during a deploy because the new version had slightly different syscall patterns. Humans in the loop for destructive actions.</p>
<h2 id="the-compounding-effect">The compounding effect</h2>
<p>None of these controls is a silver bullet on its own. A seccomp profile doesn&rsquo;t prevent credential theft. Network policy doesn&rsquo;t stop a shell spawn. Falco doesn&rsquo;t harden your kernel surface.</p>
<p>But stack them together and the attacker&rsquo;s job gets exponentially harder:</p>
<ul>
<li>Scratch image: no tools to use post-exploitation.</li>
<li>Non-root, dropped capabilities: can&rsquo;t escalate privileges.</li>
<li>Read-only filesystem: can&rsquo;t persist or install anything.</li>
<li>Seccomp: can&rsquo;t make dangerous syscalls.</li>
<li>Network policy: can&rsquo;t phone home or move laterally.</li>
<li>Falco: any anomalous behavior triggers an alert.</li>
</ul>
<p>Each layer assumes the previous one failed. That&rsquo;s not paranoia. That&rsquo;s engineering.</p>
<p>From my national cyber-defense days: you don&rsquo;t build a castle with one wall. You build concentric rings where each ring assumes the outer one is already breached. Container runtime security is the same idea, just with YAML instead of stone.</p>
]]></content:encoded></item><item><title>Serverless vs Containers: Where the Math Stops Working</title><link>https://lawzava.com/blog/2020-06-22-serverless-at-scale/</link><pubDate>Mon, 22 Jun 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-06-22-serverless-at-scale/</guid><description>Serverless is great until it isn&amp;amp;rsquo;t. A comparison of serverless and containers at different traffic scales, with actual numbers on where the economics flip.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Serverless wins at low, bursty traffic. Containers win at sustained load. The crossover happens sooner than most people think. I&rsquo;ve run workloads on both sides of that line and the difference in cost can be 3-5x if you pick wrong.</p>
<hr>
<p>Everyone&rsquo;s doing serverless now. Every conference talk. Every blog post. Every startup pitch deck mentions Lambda like it&rsquo;s a personality trait.</p>
<p>I get it. At  <a href="https://decloud.io"
   
   
target="_blank" rel="noopener">Decloud</a>
 we help companies sort out their cloud infrastructure, and serverless comes up in almost every conversation. Half the time it&rsquo;s the right call. The other half, someone read a blog post and now their entire API runs on Lambda with 400ms cold starts and a monthly bill that makes no sense.</p>
<p>So here&rsquo;s the honest breakdown. No hype, no anti-hype. Just the math.</p>
<h2 id="the-comparison-nobody-wants-to-make">The comparison nobody wants to make</h2>
<p>People treat serverless vs containers like a religious debate. It&rsquo;s not. It&rsquo;s arithmetic.</p>
<table>
  <thead>
      <tr>
          <th>Factor</th>
          <th>Serverless (Lambda)</th>
          <th>Containers (ECS/Fargate)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Traffic &lt; 100K req/day</strong></td>
          <td>Cheap. Often free tier.</td>
          <td>Overkill. Paying for idle.</td>
      </tr>
      <tr>
          <td><strong>Traffic 100K-1M req/day</strong></td>
          <td>Still reasonable. Watch concurrency.</td>
          <td>Starting to make sense.</td>
      </tr>
      <tr>
          <td><strong>Traffic &gt; 1M req/day, steady</strong></td>
          <td>Expensive. Very expensive.</td>
          <td>Clear winner on cost.</td>
      </tr>
      <tr>
          <td><strong>Bursty (0 to 10K in seconds)</strong></td>
          <td>Handles it natively.</td>
          <td>Needs autoscaling config. Lag.</td>
      </tr>
      <tr>
          <td><strong>Cold start tolerance</strong></td>
          <td>200-800ms typical (JVM: seconds)</td>
          <td>Zero. Already running.</td>
      </tr>
      <tr>
          <td><strong>Max execution time</strong></td>
          <td>15 minutes hard cap</td>
          <td>No limit</td>
      </tr>
      <tr>
          <td><strong>Connection pooling</strong></td>
          <td>Painful. Each instance = new connection.</td>
          <td>Normal. Pool lives with the process.</td>
      </tr>
      <tr>
          <td><strong>Deployment complexity</strong></td>
          <td>Low per function. High at 50+ functions.</td>
          <td>Medium, consistent.</td>
      </tr>
      <tr>
          <td><strong>Debugging in production</strong></td>
          <td>Distributed tracing or suffer</td>
          <td>SSH in if desperate. Logs are normal.</td>
      </tr>
  </tbody>
</table>
<p>That last row matters more than people admit.</p>
<h2 id="where-serverless-genuinely-wins">Where serverless genuinely wins</h2>
<p>I&rsquo;ll give credit where it&rsquo;s due. For certain workloads, serverless is unbeatable.</p>
<p><strong>Event processing.</strong> S3 upload triggers a function, function processes the file, done. No server sitting around waiting. This is the original Lambda use case and it&rsquo;s still the best one. We use this pattern at Decloud for processing customer infrastructure snapshots.</p>
<p><strong>Webhooks and integrations.</strong> Glue code between services. Receives a payload, transforms it, passes it along. Runs maybe 200ms. Happens a few thousand times a day. Perfect fit. Running a container for this is like hiring a full-time employee to check the mailbox.</p>
<p><strong>Cron jobs that run under 15 minutes.</strong> Cleanup tasks, report generation, health checks. A Lambda on a CloudWatch schedule is simpler than managing a cron server or scheduling containers.</p>
<p><strong>Genuinely unpredictable traffic.</strong> If you can&rsquo;t forecast whether you&rsquo;ll get 10 requests or 10,000 in the next hour, serverless handles that gracefully. Containers need lead time to scale.</p>
<h2 id="where-serverless-falls-apart">Where serverless falls apart</h2>
<p>Here&rsquo;s the part that gets me uninvited from serverless meetups.</p>
<p><strong>Sustained API traffic.</strong> If your API handles steady traffic &ndash; say 500+ requests per second, consistently &ndash; you&rsquo;re paying a premium for Lambda that buys you nothing. The per-invocation cost adds up fast. I&rsquo;ve seen teams cut their compute bill by 60-70% by moving a stable API from Lambda to Fargate. Not a theoretical number. Actual invoices.</p>
<p><strong>Anything that needs database connections.</strong> This one drives me crazy. Lambda spins up instances independently. Each one opens its own database connection. You go from 10 concurrent executions to 500 during a traffic spike and suddenly your Postgres is drowning in connections. Yes, RDS Proxy exists now. It helps. It&rsquo;s also another managed service you&rsquo;re paying for to solve a problem containers don&rsquo;t have.</p>
<p><strong>Latency-sensitive paths.</strong> Cold starts. I know, provisioned concurrency exists. But provisioned concurrency is just&hellip; running a container with extra steps. You&rsquo;re paying to keep Lambda instances warm. At that point, what are you even doing?</p>
<p><strong>Complex request processing.</strong> If your function needs to do three API calls, a database write, and a cache update, that 200ms function becomes 800ms. You&rsquo;re paying for all that wall-clock time. A container doing the same work with persistent connections and warm caches does it in 150ms.</p>
<h2 id="the-real-cost-comparison">The real cost comparison</h2>
<p>Let me get specific. Rough numbers for a simple API endpoint, US East, mid-2020 pricing:</p>
<p><strong>1 million requests/day, 200ms average duration, 256MB memory:</strong></p>
<ul>
<li>Lambda: ~$250/month (invocations + duration)</li>
<li>Fargate (2 tasks, 0.5 vCPU, 1GB): ~$60/month</li>
</ul>
<p>That&rsquo;s 4x. At 5 million requests/day with the same profile, the gap widens.</p>
<p><strong>10,000 requests/day, bursty, same specs:</strong></p>
<ul>
<li>Lambda: ~$3/month</li>
<li>Fargate (1 task minimum): ~$30/month</li>
</ul>
<p>Flipped completely. Serverless is 10x cheaper at low volume.</p>
<p>The crossover point for a typical web API sits somewhere around 200K-500K requests per day, depending on duration and memory. Below that, serverless. Above that, containers. This isn&rsquo;t gospel &ndash; measure your own workload &ndash; but it&rsquo;s a reasonable starting point.</p>
<h2 id="the-real-problem-is-lock-in">The real problem is lock-in</h2>
<p>Something nobody talks about enough: at 50+ Lambda functions with API Gateway, Step Functions, SQS triggers, DynamoDB streams, and EventBridge rules, you haven&rsquo;t built an application. You&rsquo;ve built an AWS application. Every piece of business logic is coupled to a specific AWS service.</p>
<p>Containers running your own code with standard libraries? Move them to GCP, Azure, your own hardware, whatever. The portability isn&rsquo;t theoretical. I&rsquo;ve done it.</p>
<p>With Decloud we see this regularly. Companies come to us wanting to optimize or migrate, and the ones running serverless-heavy architectures have a much harder time. Not impossible. Just harder and more expensive to change.</p>
<h2 id="my-actual-recommendation">My actual recommendation</h2>
<p>Stop asking &ldquo;should we use serverless?&rdquo; and start asking &ldquo;what does the traffic look like for this specific endpoint?&rdquo;</p>
<ul>
<li>Bursty, low-volume, event-driven? Lambda. Don&rsquo;t overthink it.</li>
<li>Steady traffic above a few hundred requests per second? Containers.</li>
<li>Mixed? Use both. Nobody said you have to pick one.</li>
</ul>
<p>The teams that do this well treat serverless as a tool, not an architecture. They use it where the math works and containers where it doesn&rsquo;t. No ideology. Just invoices.</p>
<p>That&rsquo;s the whole secret. Look at the bill. Do the math. Deploy accordingly.</p>
]]></content:encoded></item><item><title>Container Security in 2018: What Actually Changed</title><link>https://lawzava.com/blog/2018-08-20-container-security-beyond-basics/</link><pubDate>Mon, 20 Aug 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-08-20-container-security-beyond-basics/</guid><description>Eight months after my first container security post: what moved at the fintech startup and in the ecosystem — PodSecurityPolicy, image signing, scratch images.</description><content:encoded><![CDATA[<p>Last December I wrote about container security fundamentals — slim images, dropped capabilities, network policies. That post got more traction than I expected. I got a fair amount of &ldquo;we do none of this&rdquo; in my inbox. So here&rsquo;s where things stand eight months later, both in the ecosystem and at the fintech startup.</p>
<h3 id="podsecuritypolicy-is-real-now">PodSecurityPolicy is real now</h3>
<p>When I wrote the first post, PodSecurityPolicy existed, but almost nobody used it. That changed fast. We rolled it out at the fintech startup in Q1, and it caught issues immediately. A dev tried to deploy a container with <code>hostNetwork: true</code> for a quick port-forwarding hack. PSP rejected it. Before PSP, that would have gone through and sat there until someone noticed — or didn&rsquo;t.</p>
<p>The enforcement model is rough. You define policies, bind them via RBAC, and hope the interaction between multiple policies does what you think it does. We spent a solid week debugging why certain pods wouldn&rsquo;t schedule. But the alternative — trusting every deploy not to escalate privileges — is worse.</p>
<h3 id="we-moved-past-alpine">We moved past Alpine</h3>
<p>In the 2017 post I showed an Alpine-based final image. We&rsquo;ve since moved most Go services to <code>scratch</code> or <code>distroless</code>. No shell. No package manager. No <code>/etc/passwd</code>. An attacker who gets code execution inside one of these containers gets almost nothing. They can&rsquo;t spawn a shell because there isn&rsquo;t one.</p>
<p>The tradeoff is debugging. You can&rsquo;t <code>kubectl exec</code> into a scratch container and poke around. We handle this with ephemeral debug containers and better structured logging. Worth it.</p>
<h3 id="image-signing-went-from-nice-to-have-to-required">Image signing went from nice-to-have to required</h3>
<p>Docker Content Trust plus Notary. We gate all production pulls on signature verification now. This came out of a near-miss: a CI pipeline pulled from an internal registry without verifying the image hadn&rsquo;t been tampered with. Nobody tampered with it. But nobody would have known if they had.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>export DOCKER_CONTENT_TRUST<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>docker trust sign registry.internal/api:2.4.1
</span></span></code></pre></div><p>Simple. We should have done it a year ago.</p>
<h3 id="network-policies-still-matter-more-than-service-meshes">Network policies still matter more than service meshes</h3>
<p>Istio is getting a lot of attention. We&rsquo;re still not running it. NetworkPolicy with Calico does what we need: default-deny, explicit allows, and it doesn&rsquo;t add another control plane to babysit. When Istio matures, maybe. For now, we keep it simple.</p>
<h3 id="the-thing-nobody-talks-about">The thing nobody talks about</h3>
<p>Secrets. We switched from Kubernetes Secrets to Vault-backed dynamic credentials for database access. Short-lived tokens that rotate automatically. The ops overhead was real — Vault isn&rsquo;t a simple system — but the security posture improvement was immediate. No more long-lived database passwords sitting in etcd.</p>
<h3 id="where-we-are">Where we are</h3>
<p>Eight months of tightening. Every service runs non-root, read-only filesystem, all capabilities dropped. PSP enforces it cluster-wide. Images are signed. Network policies are default-deny. Secrets come from Vault.</p>
<p>None of it&rsquo;s glamorous. Most of it&rsquo;s YAML and policy files. But the gap between &ldquo;we know what to do&rdquo; and &ldquo;we actually do it everywhere&rdquo; is where breaches live. We&rsquo;re closing that gap, one deploy at a time.</p>
]]></content:encoded></item><item><title>Two Years of Kubernetes in Production — The Boring Parts Are the Hard Parts</title><link>https://lawzava.com/blog/2018-01-22-kubernetes-in-production-lessons-learned/</link><pubDate>Mon, 22 Jan 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-01-22-kubernetes-in-production-lessons-learned/</guid><description>Year two of Kubernetes at the fintech startup: networking, resource tuning, and the operational grunt work nobody blogs about.</description><content:encoded><![CDATA[<p>Last Tuesday at 3 AM, I got paged because a node drain took out our entire API tier. All three replicas. Same node. No PodDisruptionBudget. We&rsquo;d been running Kubernetes in production for two years and I still hadn&rsquo;t set one up for our most critical service. That&rsquo;s the thing about year two. You think you know the platform. You don&rsquo;t. You just know a different set of things than you did in year one.</p>
<p>I wrote about our  <a href="/blog/2017-01-16-kubernetes-in-production-year-of-lessons/"
   
   >first year with Kubernetes</a>
 a year ago. The tone was &ldquo;we survived and it was worth it.&rdquo; That&rsquo;s still true. But year two taught me that survival and maturity are very different things.</p>
<h3 id="the-networking-tax">The networking tax</h3>
<p>I said networking was hard last year. Understatement.</p>
<p>At the fintech startup we started with a basic CNI and no network policies. Fine for a while. Then we added more services, more teams started deploying, and suddenly every pod could talk to every other pod. No segmentation. No audit trail of who talks to what. Fixing this retroactively was brutal. You can&rsquo;t just flip on network policies without mapping every legitimate connection first. We spent two weeks doing traffic analysis before we could write a single policy.</p>
<p>DNS was the other trap. Kube-dns worked great at our initial scale. Ten services, light traffic, no complaints. Then we added a data pipeline that hammered DNS with lookups on every request. Intermittent failures. Slow responses that looked like application bugs. Took us days to trace it back to DNS resolution under load. The fix was trivial — ndots configuration and a local DNS cache. The debugging wasn&rsquo;t.</p>
<p>Treat networking as its own project. First-class. With a budget. Not something you&rsquo;ll &ldquo;figure out later.&rdquo;</p>
<h3 id="resource-requests-stopped-being-guesses">Resource requests stopped being guesses</h3>
<p>Year one, our resource requests were vibes. Somebody would eyeball a service, pick a number, ship it. We got away with it because we had plenty of headroom.</p>
<p>Year two, the cluster got denser. Services started competing for resources. OOMKills on one service, wasted capacity on another. The scheduler was doing exactly what we told it to — the problem was that we told it garbage.</p>
<p>We started profiling every service under realistic load in staging. Measured actual memory and CPU usage over a week. Then set requests based on the P95, not the average, not a guess. Kept memory limits close to requests. Dropped CPU limits entirely unless a service had a known runaway pattern.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;256Mi&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;100m&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;512Mi&#34;</span>
</span></span></code></pre></div><p>That YAML block looks boring. It&rsquo;s also the difference between a stable cluster and a 3 AM page. The numbers matter. Profile them.</p>
<h3 id="stateful-workloads-still-nope-mostly">Stateful workloads: still nope (mostly)</h3>
<p>We tried running Elasticsearch inside the cluster for about a month. Storage behavior was unpredictable. Backup and restore required custom tooling. Recovery after a node failure was slow and manual. We moved it back to managed infrastructure and haven&rsquo;t looked back.</p>
<p>Postgres and Redis stayed outside the cluster all of year two. I know people run stateful workloads on Kubernetes successfully. I also know it requires an operational investment we weren&rsquo;t ready to make. Managed services aren&rsquo;t free — they just shift the cost from your on-call to your invoice. For us, that trade was obviously correct.</p>
<h3 id="poddisruptionbudgets-the-lesson-i-learned-the-hard-way">PodDisruptionBudgets: the lesson I learned the hard way</h3>
<p>Back to that 3 AM page. A single node drain shouldn&rsquo;t be able to take down a production service. That&rsquo;s the whole point of running multiple replicas. But without a PodDisruptionBudget, Kubernetes will happily drain every pod from a node regardless of what service they belong to. If all your replicas happen to land on the same node — and the scheduler will do this if your anti-affinity rules are weak — one drain takes you to zero.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">policy/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">PodDisruptionBudget</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-pdb</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">minAvailable</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">api</span>
</span></span></code></pre></div><p>We now treat PDBs as part of the deployment contract. You don&rsquo;t get to ship a production service without one. Period.</p>
<h3 id="upgrades-arent-routine">Upgrades aren&rsquo;t routine</h3>
<p>Upgraded the cluster twice in year two. Both times were more work than expected.</p>
<p>Deprecations broke manifests. API versions we were using got dropped with one minor release warning. Node draining surfaced resource issues we&rsquo;d been ignoring. Control plane changes rippled into unexpected places — one upgrade changed default admission controller behavior and three deploys failed before we figured out why.</p>
<p>The process that worked: read every release note cover to cover. Rehearse in staging with production-like load. Not &ldquo;run the tests&rdquo; — actually push traffic through it. Schedule a real maintenance window. Don&rsquo;t YOLO a cluster upgrade on a Friday afternoon. (We did this once. Never again.)</p>
<p>Multiple clusters helped. We moved to a setup where we could upgrade one cluster while the other kept serving. Longer to provision, but the blast radius dropped to near zero.</p>
<h3 id="git-as-the-source-of-truth">Git as the source of truth</h3>
<p>This was already working in year one, but by year two it became non-negotiable culture. Every manifest in version control. Every deployment triggered by a merge. No <code>kubectl apply</code> from someone&rsquo;s laptop. No exceptions.</p>
<p>The payoff isn&rsquo;t just auditability. It&rsquo;s rollbacks. Something breaks, you revert a commit. No guessing what changed. No &ldquo;who ran what command in production.&rdquo; The answer is always in the git log. This single practice eliminated more incidents than any other operational improvement we made.</p>
<h3 id="rbac-and-secrets-the-wake-up-call">RBAC and secrets: the wake-up call</h3>
<p>Year one, everyone had cluster-admin. Year two, we finally locked it down. Created service accounts per application, scoped access narrowly, and spent an uncomfortable afternoon discovering how many things broke when we removed broad permissions. Every one of those breakages was a security hole we&rsquo;d been ignoring.</p>
<p>Secrets were the other wake-up call. Base64 isn&rsquo;t encryption. It&rsquo;s encoding. Anyone with API read access could decode every secret in the cluster. We moved to pulling secrets from Vault at deploy time and encrypting etcd at rest. Should have done it in month one. Didn&rsquo;t.</p>
<h3 id="java-dns-and-autoscaling">Java, DNS, and autoscaling</h3>
<p>Some workloads need special treatment. Our Java services ignored container memory limits by default and allocated memory like they owned the whole node. Took a few OOMKills to learn that the JVM needs explicit container-aware flags.</p>
<p>DNS bit us again here. Services making too many lookups created a bottleneck that looked like network latency. Horizontal pod autoscaling only worked correctly once our base resource requests were accurate — garbage in, garbage scaling decisions out.</p>
<h3 id="what-id-tell-someone-starting-today">What I&rsquo;d tell someone starting today</h3>
<p>Use managed Kubernetes. Don&rsquo;t run your own control plane unless you have a very specific reason. The managed offerings are mature enough now and the time you save goes directly into product work.</p>
<p>Invest in developer experience from day one. The platform can be rock solid and still feel terrible if developers can&rsquo;t get a fast local feedback loop. Minikube, namespaced dev environments, whatever works — budget time for it early.</p>
<p>Don&rsquo;t touch service mesh until the basics are boring. I&rsquo;ve seen teams adopt Istio before they had working health checks. Get networking, observability, and resource management right first. Then decide if you need the complexity.</p>
<h3 id="year-three">Year three</h3>
<p>The cluster is boring now. Boring in the good way. Deploys happen multiple times a day without drama. Incidents are rarer and smaller. The platform does what we need it to do.</p>
<p>But it took two full years of grinding — profiling resources, debugging DNS, writing network policies, learning the hard way about PDBs, locking down RBAC, building upgrade processes. None of that&rsquo;s glamorous. None of it makes for a good conference talk. It&rsquo;s just the work.</p>
<p>Kubernetes is a platform that rewards the teams willing to operate it seriously. Two years in, I&rsquo;m convinced it&rsquo;s the right foundation. I&rsquo;m also convinced that most of the value comes from the boring operational work that nobody wants to do.</p>
]]></content:encoded></item><item><title>Your Containers Aren't Secure. Here's What to Actually Do About It.</title><link>https://lawzava.com/blog/2017-12-04-container-security-beyond-the-basics/</link><pubDate>Mon, 04 Dec 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-12-04-container-security-beyond-the-basics/</guid><description>Containers give you process isolation, not a security boundary. How we hardened images, locked down runtimes, and segmented networks at the fintech startup.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Containers ship code. They don&rsquo;t ship security. Harden the image, lock the runtime, segment the network, or accept that one escape owns your entire node.</p>
<h3 id="containers-arent-a-security-boundary">Containers Aren&rsquo;t a Security Boundary</h3>
<p>I need to say this bluntly because I keep hearing it wrong: a container isn&rsquo;t a VM. It isn&rsquo;t a sandbox. It shares a kernel with the host. If something breaks out, the blast radius is the whole node. Full stop.</p>
<p>At the fintech startup we learned this the hard way during an early Kubernetes migration. We had a service running as root inside its container — &ldquo;just for debugging,&rdquo; someone said — and a misconfigured volume mount gave it read access to the host&rsquo;s <code>/etc</code>. Nobody exploited it. We caught it in review. But it sat there for two weeks before anyone noticed, and that scared me more than an actual breach would have.</p>
<p>A principle from national cyber-defense that applies everywhere: assume the perimeter is already broken. Then design so that a breach in one layer doesn&rsquo;t cascade. Containers are just another layer. Treat them that way.</p>
<h3 id="ship-less-expose-less">Ship Less, Expose Less</h3>
<p>The single highest-ROI thing you can do for container security is shrink your images. Every package you include is attack surface. Every binary is a tool an attacker can use post-exploitation. Multi-stage builds are your best friend here.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">golang:1.9</span> <span style="color:#66d9ef">AS</span> <span style="color:#e6db74">builder</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">WORKDIR</span> <span style="color:#e6db74">/app</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> . .<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> CGO_ENABLED<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> GOOS<span style="color:#f92672">=</span>linux go build -o service .<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">alpine:3.6</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> adduser -D appuser<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">USER</span> <span style="color:#e6db74">appuser</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> --from<span style="color:#f92672">=</span>builder /app/service /service<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENTRYPOINT</span> [<span style="color:#e6db74">&#34;/service&#34;</span>]<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>This is almost exactly what we use at the fintech startup for our Go services. Builder stage compiles everything. Final image has Alpine, a non-root user, and the binary. That&rsquo;s it. No curl, no bash, no wget. If an attacker gets code execution inside this container, they have almost nothing to work with.</p>
<p>Two more things that matter:</p>
<p><strong>Pin to digests, not tags.</strong> <code>alpine:3.6</code> can change underneath you. <code>alpine@sha256:abc123...</code> can&rsquo;t. We had a CI build pass on Tuesday and fail on Thursday because the base image got a patch that broke our libc expectations. Tags are lies. Digests are truth.</p>
<p><strong>Scan continuously.</strong> Not just at build time. Vulnerabilities get published weeks or months after an image ships. We run Clair against our registry on a cron. Anything with a critical CVE gets flagged and the owning team has 48 hours to patch or explain why not.</p>
<h3 id="runtime-where-mistakes-become-incidents">Runtime: Where Mistakes Become Incidents</h3>
<p>A clean image means nothing if you run it with <code>--privileged</code>. I&rsquo;ve seen production deployments where containers had <code>SYS_ADMIN</code> capabilities because someone copy-pasted a Stack Overflow answer during a late-night deploy. That one capability basically gives you root on the host.</p>
<p>Strip everything. Start from zero and add back only what you need.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">runAsNonRoot</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">readOnlyRootFilesystem</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">capabilities</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">drop</span>: [<span style="color:#e6db74">&#34;ALL&#34;</span>]
</span></span></code></pre></div><p>Three lines of YAML. That&rsquo;s it. <code>runAsNonRoot</code> stops the &ldquo;just for debugging&rdquo; excuse. <code>readOnlyRootFilesystem</code> prevents an attacker from dropping binaries. Dropping all capabilities means no mount, no raw sockets, no ptrace. If your app genuinely needs a specific capability — like <code>NET_BIND_SERVICE</code> to bind port 443 — add that one back explicitly and document why.</p>
<p>Pair this with seccomp profiles if your runtime supports them. Default Docker seccomp blocks around 44 syscalls. For most web services, you can block even more. We profiled our API gateway&rsquo;s syscall usage over a week, built a whitelist, and cut the available syscall surface by 60%.</p>
<p>The other half is detection. You need to know when something unexpected happens inside a container. A new shell process. An outbound connection to an IP that isn&rsquo;t in your dependency list. A binary that wasn&rsquo;t in the original image. We keep this simple — sysdig + alerting rules. Nothing fancy. But it works because we actually look at the alerts.</p>
<h3 id="network-segmentation-kill-flat-networks">Network Segmentation: Kill Flat Networks</h3>
<p>Default Kubernetes networking is flat. Every pod can talk to every other pod. This is the container equivalent of putting every server in the same VLAN with no firewall rules. Insane, but it&rsquo;s the default.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">NetworkPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-allow-frontend</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">podSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">policyTypes</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">Ingress</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ingress</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">from</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">podSelector</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">app</span>: <span style="color:#ae81ff">frontend</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">port</span>: <span style="color:#ae81ff">8080</span>
</span></span></code></pre></div><p>This policy says: the API pod accepts traffic from frontend pods on port 8080. Everything else is denied. Lateral movement goes from trivial to impossible unless the attacker also compromises the frontend service.</p>
<p>At the fintech startup we started by mapping every service-to-service dependency, then wrote deny-all policies and added explicit allows. Took about a week for our cluster. Found three services that were talking to things they had no business talking to — leftover from an old feature that got half-removed. Network policies are a security tool and an architecture audit tool.</p>
<p>Service meshes with mutual TLS are starting to appear — Istio, Linkerd — but they&rsquo;re early. We&rsquo;re watching, not adopting yet. For secrets, keep them out of your images. Kubernetes Secrets with encryption at rest, or Vault if you want something more robust. Never <code>ENV</code> a database password into a Dockerfile. I&rsquo;ve seen it. In production. At companies you&rsquo;ve heard of.</p>
<h3 id="rbac-and-audit-logging">RBAC and Audit Logging</h3>
<p>Kubernetes RBAC isn&rsquo;t optional. Neither is PodSecurityPolicy. These are the controls that prevent a developer with deploy access from accidentally (or intentionally) escalating to cluster-admin. Scope permissions to namespaces. Give teams access to their stuff and nothing else.</p>
<p>Audit logging closes the loop. When something goes wrong — and it will — you need to answer: who did what, when, and to which resource. Without audit logs, incident response is guesswork. With them, it&rsquo;s a timeline.</p>
<h3 id="the-real-point">The Real Point</h3>
<p>None of this is exotic. Slim images, pinned builds, non-root containers, read-only filesystems, dropped capabilities, network policies, RBAC, audit logs. It&rsquo;s a checklist, not a research project. The hard part isn&rsquo;t knowing what to do. It&rsquo;s actually doing it consistently, across every service, on every deploy, even when someone says &ldquo;we&rsquo;ll fix it after launch.&rdquo;</p>
<p>Stack these controls and a container escape becomes a dead end instead of a highway to your entire infrastructure.</p>
]]></content:encoded></item><item><title>A Year Running Kubernetes in Production — What Actually Happened</title><link>https://lawzava.com/blog/2017-01-16-kubernetes-in-production-year-of-lessons/</link><pubDate>Mon, 16 Jan 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-01-16-kubernetes-in-production-year-of-lessons/</guid><description>After a year of Kubernetes in production: the wins are real, but the sharp edges drew blood first. What paid off, what bit us, and what I&amp;amp;rsquo;d do differently.</description><content:encoded><![CDATA[<p>We moved our first workload to Kubernetes about a year ago at the fintech startup. The pitch was simple: declarative deployments, self-healing services, no more SSH-into-prod-and-pray. A year later, I can confirm all of that&rsquo;s true. I can also confirm that the path there involved a DNS outage at 2am, a deployment that silently ate 4GB of RAM, and me reading the kube-proxy source code on a Saturday.</p>
<p>Worth it? Yes. Painless? Not even close.</p>
<h3 id="the-stuff-that-pays-off-immediately">The stuff that pays off immediately</h3>
<p>The declarative model is the real win. You describe what you want, Kubernetes converges toward it. No more runbooks for &ldquo;what if the service dies on box 3.&rdquo; It just comes back. Deployments get boring, which is exactly what you want deployments to be.</p>
<p>Resource requests and limits are the other quick win, but only once you actually set them properly. We ran for weeks without limits on one of our data ingestion services. It worked great until it didn&rsquo;t, and then it took down two neighbors on the same node.</p>
<p>Don&rsquo;t skip this part:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;250m&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;256Mi&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;500m&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;512Mi&#34;</span>
</span></span></code></pre></div><p>Those numbers are a starting point. Profile under real load. Revisit quarterly. The defaults are lies.</p>
<p>Rolling updates deserve a mention too. Going from &ldquo;schedule maintenance window, hold breath, deploy, pray&rdquo; to incremental rollouts with automatic rollback changed how the whole team thinks about shipping. People deploy more often because it feels safe. That feedback loop alone justified the migration.</p>
<h3 id="where-it-bites-you">Where it bites you</h3>
<p><strong>Networking.</strong> Every single time.</p>
<p>Service discovery looks dead simple in the docs. Then you hit DNS caching issues, or a network policy blocks something you didn&rsquo;t expect, or your ingress controller does something creative with header forwarding. Kubernetes hides the network behind a nice abstraction until that abstraction leaks, and then you&rsquo;re reading iptables rules at midnight.</p>
<p>We had a bug at the fintech startup where intermittent 503s on one service turned out to be a kube-dns cache TTL mismatch with our upstream provider. Took three days to find. The fix was two lines of config. That&rsquo;s Kubernetes networking in a nutshell.</p>
<p><strong>Resource tuning</strong> is the slow burn. Set requests too low and the scheduler packs your nodes like a clown car — then starts evicting pods when things get tight. Set limits too low and your service gets CPU-throttled under load, which looks exactly like an application bug from the outside. The only way through is profiling under production traffic patterns and adjusting. There&rsquo;s no shortcut here.</p>
<p><strong>Stateful workloads</strong> are possible but honestly, we kept Postgres and Redis outside the cluster for most of the year. Storage classes, PV lifecycle, backup and restore — it all works, but the operational surface area is large. If you&rsquo;re just getting started, keep your databases where they are. Move them in later when you actually understand what you&rsquo;re signing up for.</p>
<p><strong>Debugging gets weird.</strong> Something fails and the cause could be the application code, the container image, the scheduler, the network plugin, the node itself, or some combination. I&rsquo;ve seen a deployment fail because a node had a full disk from container image garbage collection not running. Good luck finding that without decent logging and tracing. <code>kubectl describe</code> is your best friend, but sometimes your best friend doesn&rsquo;t know the answer either.</p>
<h3 id="the-hard-lessons">The hard lessons</h3>
<p>Treating Kubernetes like a black box works until you need to upgrade the cluster. Or recover from an etcd failure. Or figure out why the scheduler won&rsquo;t place a pod. Someone on your team needs to actually understand the control plane. Not &ldquo;watched a conference talk&rdquo; understand. &ldquo;Can read the API server logs and make sense of them&rdquo; understand.</p>
<p>Running your own control plane is expensive. Not in money — in attention. If your cloud provider offers managed Kubernetes, take the deal. We spent months on control plane operations that could have been spent on product work. The managed offerings are solid enough now. Pay the tax, move on.</p>
<p>YAML sprawl is the death of a thousand paper cuts. You start with clean manifests for three services. Six months later you have forty files that are 80% identical and nobody remembers which copy is canonical. Helm or Kustomize help, but be careful — I&rsquo;ve seen teams replace YAML sprawl with template sprawl, which is somehow worse because now you need to understand Go templates to deploy a config change.</p>
<p>Secrets handling is genuinely bad out of the box. Kubernetes Secrets are base64-encoded. That&rsquo;s encoding, not encryption. Anyone with API access can read them. We ended up pulling secrets from Vault at deploy time and treating the built-in Secrets as a transport layer. If you&rsquo;re storing database passwords as Kubernetes Secrets and calling it done, stop.</p>
<p>Local development is the thing nobody budgets time for. Your cluster isn&rsquo;t a laptop. Developers need fast feedback without blowing up shared environments. Minikube and isolated namespaces help, but they need investment. We lost a solid week of productivity before we built a decent local dev story.</p>
<h3 id="habits-that-actually-stuck">Habits that actually stuck</h3>
<p>GitOps, before we called it that. Every manifest in version control. Deployments triggered by merges. No <code>kubectl apply</code> from someone&rsquo;s laptop in production. This gives you an audit trail, makes rollbacks trivial, and kills snowflake configurations dead.</p>
<p>Health checks as production code. A bad liveness probe will kill your service faster than a bad deploy. We had a probe that checked a database connection with a 1-second timeout. Database had a slow moment, probe failed, Kubernetes restarted the pod, pod came up and hit the still-slow database, probe failed again. Restart loop. Cascading failure from a health check. Write your probes carefully.</p>
<p>Kubernetes-aware monitoring. Watching application metrics is necessary but not sufficient. You need to see what the scheduler is doing, how the nodes look, what the control plane health is. Without that, incidents are guesswork. With it, incidents become analysis.</p>
<h3 id="should-you-adopt-it">Should you adopt it?</h3>
<p>If you have multiple services, deploy frequently, and need real scheduling and resilience — yes. The investment pays off.</p>
<p>If you have a small team, a monolith, or two services behind a load balancer — probably not yet. Kubernetes doesn&rsquo;t remove operational complexity. It trades one kind for another, and the new kind requires specific expertise. A team that&rsquo;s struggling with basic deployments won&rsquo;t be saved by Kubernetes. They&rsquo;ll just struggle with more abstraction layers.</p>
<h3 id="a-year-later">A year later</h3>
<p>Kubernetes feels less like magic now and more like a system I can reason about. That&rsquo;s the real milestone. Not &ldquo;it works&rdquo; but &ldquo;I understand why it works, and I understand why it breaks.&rdquo;</p>
<p>The teams I&rsquo;ve seen succeed treat Kubernetes as a product they operate — with on-call, with runbooks, with regular investment. The teams that struggle treat it like something they installed once and forgot about. Same as any other infrastructure, really. The tool doesn&rsquo;t care about your intentions. It cares about your configuration.</p>
]]></content:encoded></item><item><title>2016: The Year I Stopped Fighting Infrastructure</title><link>https://lawzava.com/blog/2016-12-28-year-in-review-technology-trends-2016/</link><pubDate>Wed, 28 Dec 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-12-28-year-in-review-technology-trends-2016/</guid><description>A personal look back at 2016 &amp;amp;ndash; Docker going mainstream, Kubernetes momentum, Go adoption, and lessons from building at a mobility startup and a fintech startup.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Docker won. Kubernetes is next. Go keeps earning its place. I was wrong about serverless timing and right about containers. Building two startups simultaneously taught me more about operational discipline than any conference talk ever could.</p>
<p>2016 felt like the year the industry collectively decided to stop debating containers and start running them. For me it was more specific than that. It was the year I stopped treating infrastructure as a thing to fight and started treating it as a thing to design.</p>
<h3 id="docker-finally-stopped-being-a-toy">Docker finally stopped being a toy</h3>
<p>I spent most of 2015 defending Docker to skeptics. &ldquo;It works on my machine&rdquo; jokes were still the dominant reaction. By mid-2016, that conversation was over. Docker went mainstream. Not just for dev environments &ndash; for production workloads, across teams that had never touched a Dockerfile before.</p>
<p>At a mobility startup, we containerized everything. Location tracking, payment processing, fleet management. Docker Compose got us off the ground fast, and when we outgrew single-host scheduling, the migration path was clear. The tooling matured enough in 2016 that containers stopped being a bet and started being a default.</p>
<p>What changed wasn&rsquo;t Docker itself &ndash; it was the ecosystem around it. Registries got better. Logging and monitoring caught up. CI pipelines learned to build and push images without a week of YAML wrestling. The boring parts got boring, which is when you know adoption is real.</p>
<h3 id="kubernetes-momentum-was-undeniable">Kubernetes momentum was undeniable</h3>
<p>I wrote about this earlier this year after evaluating Swarm, Kubernetes, and Mesos side by side. My conclusion then: Kubernetes is going to win, but the operational tax in late 2016 is genuinely painful.</p>
<p>That assessment held up. We ran Swarm at the mobility startup for three months before migrating to Kubernetes, and both decisions were correct. Swarm gave us speed. Kubernetes gave us a real platform. The CNCF formation, Google&rsquo;s backing, and the velocity of community tooling made the trajectory obvious.</p>
<p>What I underestimated was how fast managed Kubernetes would become available. GKE was already solid. By early 2017, the &ldquo;Kubernetes is too hard to operate&rdquo; argument will start losing force. That&rsquo;s a good thing.</p>
<h3 id="go-kept-earning-its-place">Go kept earning its place</h3>
<p>We chose Go for our backend services at both the mobility startup and the fintech startup, and 2016 validated that decision repeatedly. The language grew in adoption. The tooling stayed excellent. The deployment story &ndash; single static binary, no runtime dependencies &ndash; kept saving us time in production.</p>
<p>Go 1.7 shipped with context in the standard library, which was a practical win for anyone building HTTP services with cancellation and timeouts. The ecosystem filled in noticeably. More battle-tested libraries for common patterns. Better database drivers. gRPC support matured.</p>
<p>The trade-offs I wrote about earlier remain real. No generics. Verbose error handling. A younger ecosystem than Java or Python. But for the kind of services we build &ndash; concurrent, networked, performance-sensitive &ndash; Go in 2016 was the right tool. Not the trendiest. The right one.</p>
<h3 id="what-i-got-wrong">What I got wrong</h3>
<p>I thought serverless would matter more by now. Lambda launched in 2014, and by early 2016 I expected it to be a serious architecture option for startups. It wasn&rsquo;t. The tooling was immature, cold starts were painful, and the debugging experience was bad enough to kill productivity. Serverless in 2016 was interesting to watch but not interesting to use for anything beyond simple event handlers.</p>
<p>I also underestimated how long microservices confusion would persist. I assumed the &ldquo;monolith first&rdquo; message from DHH and others would settle things. It didn&rsquo;t. Teams kept splitting services prematurely, then drowning in operational complexity they weren&rsquo;t staffed to handle. This is still happening as I write this.</p>
<h3 id="what-i-got-right">What I got right</h3>
<p>Containers as the default deployment unit. Called it in 2015, and 2016 delivered. Kubernetes as the orchestration winner. Called it in October, and the trajectory only accelerated. Go as a serious backend language for startups. We bet on it early and it paid off consistently.</p>
<p>The one I&rsquo;m most satisfied about: investing in operational discipline over feature velocity. At both the mobility startup and the fintech startup, I pushed hard for automated deploys, rollback capability, and monitoring before we added features. That discipline saved us multiple times this year. One outage at the mobility startup that would have been a multi-hour firefight turned into a three-minute rollback because we had the infrastructure in place.</p>
<h3 id="the-fintech-angle">The fintech angle</h3>
<p>At the fintech startup, the ML side of the business grew significantly in 2016. TensorFlow matured, and our team started using it for financial content relevance scoring. The accessibility of ML tooling improved noticeably &ndash; pre-trained models, cloud GPUs, better documentation. But the gap between &ldquo;can run a model&rdquo; and &ldquo;understands what the model is doing&rdquo; remained wide. We hired for ML literacy, not just ML capability, and that distinction mattered.</p>
<h3 id="looking-forward">Looking forward</h3>
<p>2017 will be about consolidation. Kubernetes will keep winning and the operational burden will drop as managed offerings improve. Go will gain traction in infrastructure tooling &ndash; I expect to see more CLIs, more Kubernetes operators, more network services written in Go. Serverless will get better but will remain niche for another year at least.</p>
<p>The thing I care about most heading into 2017: security as a first-class engineering concern, not a compliance checkbox. GDPR is coming. The breach headlines keep getting worse. At both companies I pushed for security review in the development workflow, not as a gate at the end. That investment will compound.</p>
<p>Discipline over heroics. That was the lesson of 2016. The teams that shipped reliably weren&rsquo;t the ones with the best engineers. They were the ones with the best habits.</p>
]]></content:encoded></item><item><title>Container Orchestration: Docker Swarm vs Kubernetes vs Mesos</title><link>https://lawzava.com/blog/2016-10-17-container-orchestration-swarm-kubernetes-mesos/</link><pubDate>Mon, 17 Oct 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-10-17-container-orchestration-swarm-kubernetes-mesos/</guid><description>Swarm, Kubernetes, and Mesos compared side by side after running all three at a mobility startup. Kubernetes is going to win, but the operational tax is real.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>We tested all three at a mobility startup. Kubernetes won on capability but nearly lost on operational cost. If you have a small team and need something running next week, pick Swarm. If you&rsquo;re building for the next two years, accept the pain and invest in Kubernetes now.</p>
<h3 id="the-evaluation">The evaluation</h3>
<p>At the mobility startup we run location-tracking services, payment processing, and a fleet management backend. All containerized. When we outgrew single-host Docker Compose, I needed an orchestrator that could schedule across hosts, handle rolling deploys, and not require a dedicated platform team to babysit.</p>
<p>I spent three weeks running Swarm, Kubernetes, and Mesos with Marathon side by side on identical three-node clusters. Same workload: our core API, a background worker, and a Redis instance. Here is what I found.</p>
<h3 id="the-comparison">The comparison</h3>
<table>
  <thead>
      <tr>
          <th>Dimension</th>
          <th>Docker Swarm</th>
          <th>Kubernetes</th>
          <th>Mesos + Marathon</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Setup time</strong></td>
          <td>~30 minutes. <code>docker swarm init</code> and you&rsquo;re running.</td>
          <td>Hours to days depending on how you bootstrap. kubeadm helps but isn&rsquo;t production-ready yet.</td>
          <td>Half a day minimum. ZooKeeper, Mesos masters, Marathon framework. Lots of moving parts.</td>
      </tr>
      <tr>
          <td><strong>Learning curve</strong></td>
          <td>Low. If you know Docker, you know Swarm.</td>
          <td>Steep. Pods, services, deployments, ingress, RBAC, namespaces. Real concepts, but many of them.</td>
          <td>Medium-high. The two-layer model (Mesos offers resources, Marathon schedules) is elegant but unfamiliar.</td>
      </tr>
      <tr>
          <td><strong>Rolling deploys</strong></td>
          <td>Basic. Works, but limited control over rollout speed and health checks.</td>
          <td>Excellent. Deployment objects give you fine-grained rollout control, pause, and rollback.</td>
          <td>Good. Marathon handles health-check-gated rolling restarts well.</td>
      </tr>
      <tr>
          <td><strong>Service discovery</strong></td>
          <td>Built-in DNS. Simple, sufficient for most cases.</td>
          <td>Built-in DNS plus label selectors. Powerful, composable.</td>
          <td>Mesos-DNS or external tools. More wiring required.</td>
      </tr>
      <tr>
          <td><strong>Networking</strong></td>
          <td>Overlay network out of the box. Straightforward.</td>
          <td>CNI plugins. Flexible but you&rsquo;re choosing and configuring a network plugin on day one.</td>
          <td>Depends on configuration. Bridge, host, or CNI. Less opinionated, more work.</td>
      </tr>
      <tr>
          <td><strong>Ecosystem &amp; community</strong></td>
          <td>Tied to Docker Inc. Smaller community, fewer third-party tools.</td>
          <td>Growing fast. Google backing, CNCF formation, new tooling every week.</td>
          <td>Mature at scale (Twitter, Apple), but losing mindshare to Kubernetes in the container-specific space.</td>
      </tr>
      <tr>
          <td><strong>Operational overhead</strong></td>
          <td>Low. Few components, familiar tooling.</td>
          <td>High. etcd cluster, API server, scheduler, controller manager, kube-proxy, kubelet. Each one can fail.</td>
          <td>High. ZooKeeper quorum, Mesos masters, Marathon instances. Proven, but operationally heavy.</td>
      </tr>
      <tr>
          <td><strong>Scaling ceiling</strong></td>
          <td>Fine for dozens of services. Unclear beyond that in late 2016.</td>
          <td>Designed for thousands of pods. Already proven at Google-scale lineage.</td>
          <td>Built for tens of thousands of tasks. This is where Mesos genuinely excels.</td>
      </tr>
      <tr>
          <td><strong>Maturity</strong></td>
          <td>Young. Swarm mode shipped months ago in Docker 1.12.</td>
          <td>Young but accelerating. v1.4 is solid, v1.5 incoming. Rapid release cycle.</td>
          <td>Mature. Years of production use at large organizations.</td>
      </tr>
  </tbody>
</table>
<h3 id="my-take">My take</h3>
<p>Kubernetes is going to win this market. The abstraction model is right. Pods, services, and declarative desired-state are the correct primitives for running containers at scale. The community momentum is unmistakable. Every infrastructure vendor is building Kubernetes integrations, not Swarm or Mesos ones.</p>
<p>But the operational cost in late 2016 is genuinely high. I burned two full days debugging etcd split-brain on a three-node cluster. Networking with flannel required reading source code, not documentation. RBAC is immature. Monitoring the control plane itself is a project unto itself.</p>
<p>Swarm is the honest answer for small teams today. We used it at the mobility startup for three months before migrating to Kubernetes, and those three months were productive. The API is Docker&rsquo;s API. The mental model is simple. You can ship with it and migrate later without regret.</p>
<p>Mesos is the right choice if you already run it or if you genuinely need to schedule containers alongside Spark jobs and batch workloads on the same cluster. For a startup running pure container workloads, Mesos is overbuilt. The ZooKeeper dependency alone adds operational surface area that a small team doesn&rsquo;t need.</p>
<h3 id="what-we-actually-did">What we actually did</h3>
<p>We started on Swarm, shipped to production, and ran it for three months. When we hit the limits of Swarm&rsquo;s rolling deploy controls and needed better service discovery for inter-service communication, we migrated to Kubernetes. The migration was painful but contained. It took about a week of focused work.</p>
<p>I would make the same decision again. Swarm bought us time to ship features instead of fighting infrastructure. Kubernetes gave us a platform we could grow into. Trying to start with Kubernetes on day one with our team size would have been a mistake.</p>
<h3 id="pick-for-your-team-not-the-hype">Pick for your team, not the hype</h3>
<p>Pick based on your team, not the feature matrix. Swarm if you need to ship now with minimal operational burden. Kubernetes if you can absorb the learning curve and want the platform that will matter in two years. Mesos if you&rsquo;re already running it or have workloads that genuinely require its scheduling model.</p>
<p>Kubernetes is going to dominate. That doesn&rsquo;t mean you have to adopt it today.</p>
]]></content:encoded></item><item><title>Docker in Production: What We Learned Running Containers at Scale</title><link>https://lawzava.com/blog/2016-02-08-docker-in-production-lessons-from-running-containers-at-scale/</link><pubDate>Mon, 08 Feb 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-02-08-docker-in-production-lessons-from-running-containers-at-scale/</guid><description>Running Docker in production at a mobility startup forced us to get serious about image builds, networking, log aggregation, and security. What actually worked.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Docker won&rsquo;t fix your ops. It will make bad ops louder and faster. But if you treat images like release artifacts, lock down your Dockerfiles, and build real observability around your containers, it&rsquo;s the best deployment tool available right now.</p>
<h3 id="we-broke-production-three-times-in-the-first-month">We Broke Production Three Times in the First Month</h3>
<p>At a mobility startup, we moved our backend services into Docker containers because we were tired of deploy scripts that worked on one machine and failed on another. The promise was simple: build once, run anywhere. That part was true. What nobody told us was how many new ways we would find to break things.</p>
<p>The first week, a developer pushed a container built on their laptop. It had a different glibc than the host. Segfault in production. The second week, we ran out of disk because nobody was cleaning up old images. The third week, a container ran as root and wrote a config file to a bind mount that the host process couldn&rsquo;t read back. Three incidents, three categories of failure, all in 30 days.</p>
<p>Every one of those was our fault. Docker did exactly what we told it to do. We just hadn&rsquo;t learned to tell it the right things yet.</p>
<h3 id="dockerfiles-that-actually-work">Dockerfiles That Actually Work</h3>
<p>Most Dockerfile tutorials produce images that are 800MB and run as root. That&rsquo;s fine for a demo. It isn&rsquo;t fine when you&rsquo;re pushing 15 deploys a day across 4 services.</p>
<p>Here is the pattern we settled on for our Go services at the mobility startup:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">golang:1.6-alpine</span> <span style="color:#66d9ef">AS</span> <span style="color:#e6db74">builder</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apk add --no-cache git ca-certificates<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">WORKDIR</span> <span style="color:#e6db74">/src</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> go.sum go.src ./<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> go get -d ./...<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> . .<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> CGO_ENABLED<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> GOOS<span style="color:#f92672">=</span>linux go build -ldflags<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;-s -w&#34;</span> -o /app ./cmd/server<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">alpine:3.3</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apk add --no-cache ca-certificates tzdata <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> adduser -D -u <span style="color:#ae81ff">1000</span> appuser<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> --from<span style="color:#f92672">=</span>builder /app /usr/local/bin/app<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">USER</span> <span style="color:#e6db74">appuser</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">EXPOSE</span> <span style="color:#e6db74">8080</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENTRYPOINT</span> [<span style="color:#e6db74">&#34;app&#34;</span>]<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>A few things to notice. The multi-stage build keeps the final image small &ndash; under 20MB for most of our services. We copy dependency files first and run <code>go get</code> before copying the rest of the source. That means Docker caches the dependency layer, and rebuilds only take seconds when application code changes. We create a non-root user. We strip debug symbols with <code>-ldflags=&quot;-s -w&quot;</code>. We use <code>ENTRYPOINT</code> instead of <code>CMD</code> so the container process is PID 1 and receives signals properly.</p>
<p>For our Python services (we had one analytics pipeline that wasn&rsquo;t worth rewriting), the pattern looked different:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">python:3.5-slim</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> groupadd -r app <span style="color:#f92672">&amp;&amp;</span> useradd -r -g app -d /app -s /sbin/nologin app<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">WORKDIR</span> <span style="color:#e6db74">/app</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> requirements.txt .<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> pip install --no-cache-dir -r requirements.txt<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> . .<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> chown -R app:app /app<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">USER</span> <span style="color:#e6db74">app</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">EXPOSE</span> <span style="color:#e6db74">5000</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">CMD</span> [<span style="color:#e6db74">&#34;gunicorn&#34;</span>, <span style="color:#e6db74">&#34;--bind&#34;</span>, <span style="color:#e6db74">&#34;0.0.0.0:5000&#34;</span>, <span style="color:#e6db74">&#34;--workers&#34;</span>, <span style="color:#e6db74">&#34;2&#34;</span>, <span style="color:#e6db74">&#34;wsgi:app&#34;</span>]<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Same principles. Dependencies first. Non-root user. Minimal base image. Explicit port. No <code>latest</code> tag anywhere.</p>
<h3 id="image-tagging-latest-isnt-a-version">Image Tagging: latest Isn&rsquo;t a Version</h3>
<p>We tag every image with the git commit SHA. Not the branch name, not <code>latest</code>, not a date string. The commit SHA.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker build -t acme/location-svc:<span style="color:#66d9ef">$(</span>git rev-parse --short HEAD<span style="color:#66d9ef">)</span> .
</span></span><span style="display:flex;"><span>docker push acme/location-svc:<span style="color:#66d9ef">$(</span>git rev-parse --short HEAD<span style="color:#66d9ef">)</span>
</span></span></code></pre></div><p>When something breaks at 2 AM, the first question is: what is running right now? With SHA tags, you can answer that in seconds. You can diff the exact code. You can roll back to the previous SHA. You can see who merged what. <code>latest</code> tells you nothing.</p>
<p>We store image metadata as labels too:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">LABEL</span> build.sha<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>GIT_SHA<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>      build.date<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>BUILD_DATE<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>      build.ci<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;jenkins&#34;</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>It sounds like overkill. It isn&rsquo;t. During an incident last month, we traced a memory leak to a specific commit because the container label told us exactly what was deployed.</p>
<h3 id="docker-compose-for-local-development">docker-compose for Local Development</h3>
<p>Our services talk to PostgreSQL, Redis, and each other. Getting all of that running locally used to mean a two-page setup guide that was always out of date. Now it&rsquo;s a single file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">version</span>: <span style="color:#e6db74">&#39;2&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">location-svc</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">build</span>: <span style="color:#ae81ff">./services/location</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;8080:8080&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">DATABASE_URL=postgres://app:dev@db:5432/app?sslmode=disable</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">REDIS_URL=redis://cache:6379</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">db</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">cache</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">trip-svc</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">build</span>: <span style="color:#ae81ff">./services/trip</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;8081:8080&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">DATABASE_URL=postgres://app:dev@db:5432/app?sslmode=disable</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">LOCATION_SVC_URL=http://location-svc:8080</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">db</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">location-svc</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">db</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">postgres:9.5</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">POSTGRES_USER=app</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">POSTGRES_PASSWORD=dev</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">POSTGRES_DB=app</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">pgdata:/var/lib/postgresql/data</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cache</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">redis:3.0-alpine</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pgdata</span>:
</span></span></code></pre></div><p>New engineers run <code>docker-compose up</code> and have a working environment in under two minutes. No installing Go. No configuring Postgres. No hunting for the right Redis version. The compose file is checked into the repo, so it stays current.</p>
<p>One thing we learned the hard way: <code>depends_on</code> doesn&rsquo;t mean &ldquo;wait until the service is ready.&rdquo; It means &ldquo;start this container after that one.&rdquo; Postgres takes a few seconds to accept connections. Your application will crash if it tries to connect immediately. We added retry logic to every service&rsquo;s database connection code. Not elegant, but reliable.</p>
<h3 id="networking-stop-hardcoding-ips">Networking: Stop Hardcoding IPs</h3>
<p>Docker&rsquo;s default bridge network assigns IPs dynamically. If you hardcode an IP in a config file, it will work until it doesn&rsquo;t, and then you will spend an hour figuring out why service A can&rsquo;t find service B.</p>
<p>On a single host, Docker Compose gives you DNS resolution for free. Service names resolve to container IPs. <code>http://location-svc:8080</code> just works. That&rsquo;s good enough for development and for small production setups.</p>
<p>For multi-host, the story is more complicated. We use overlay networks, which let containers on different hosts communicate as if they were on the same network. It works, but it adds latency. We measured an extra 1-2ms per hop on our setup. For most services that&rsquo;s noise. For the real-time bike location tracking, it was noticeable. We kept that service on a single host with host networking.</p>
<h3 id="logs-stdout-or-nothing">Logs: stdout or Nothing</h3>
<p>Every one of our services logs to stdout. No log files inside the container. No custom logging directories. stdout.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">SetOutput</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">SetFlags</span>(<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Ldate</span> | <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Ltime</span> | <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Lmicroseconds</span> | <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">LUTC</span>)
</span></span></code></pre></div><p>Docker captures stdout and makes it available via <code>docker logs</code>. We run a log forwarder on each host that ships container logs to a central ELK stack. Each log line includes the container ID, image name, and service name as structured fields. When something goes wrong, we can filter by service, time range, and container in Kibana within seconds.</p>
<p>The mistake we made early was letting one service log at DEBUG level in production. It wrote 2GB of logs in an hour, filled up the Docker log driver&rsquo;s buffer, and slowed down every other container on the host. We now enforce log level via environment variables and default to WARN in production.</p>
<h3 id="resource-limits-arent-optional">Resource Limits Aren&rsquo;t Optional</h3>
<p>A container without resource limits will happily consume all available memory on the host and take everything else down with it. We learned this when our analytics service hit a data spike and OOM-killed the Postgres container running on the same host.</p>
<p>Every container gets explicit limits:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">location-svc</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">mem_limit</span>: <span style="color:#ae81ff">256m</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">memswap_limit</span>: <span style="color:#ae81ff">256m</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cpu_shares</span>: <span style="color:#ae81ff">512</span>
</span></span></code></pre></div><p>The <code>memswap_limit</code> matching <code>mem_limit</code> prevents the container from using swap. If it runs out of memory, it dies. That sounds harsh, but a container thrashing on swap is worse than a container that restarts cleanly. Our health checks pick up the restart within seconds.</p>
<h3 id="security-run-as-non-root-or-dont-ship">Security: Run as Non-Root or Don&rsquo;t Ship</h3>
<p>I mentioned this in the Dockerfile section but it bears repeating. Every container runs as a non-root user. No exceptions. If a library or tool requires root, we find a different library or tool.</p>
<p>Beyond the user, we also:</p>
<ul>
<li>Use <code>--read-only</code> filesystem where possible, mounting only specific writable directories for temp files</li>
<li>Drop all capabilities and add back only what is needed: <code>--cap-drop=ALL --cap-add=NET_BIND_SERVICE</code></li>
<li>Never mount the Docker socket into a container (I&rsquo;ve seen this in tutorials and it terrifies me)</li>
<li>Scan images for known vulnerabilities before pushing to our private registry</li>
</ul>
<p>Our private registry runs behind TLS with basic auth. Every push and pull is authenticated. The registry itself runs in a container, which is a fun bit of recursion, but it works.</p>
<h3 id="health-checks-and-restarts">Health Checks and Restarts</h3>
<p>Every service exposes a <code>/healthz</code> endpoint that returns 200 if the service can reach its dependencies:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">healthHandler</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">Ping</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">StatusServiceUnavailable</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprintf</span>(<span style="color:#a6e22e">w</span>, <span style="color:#e6db74">&#34;db: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">StatusOK</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprint</span>(<span style="color:#a6e22e">w</span>, <span style="color:#e6db74">&#34;ok&#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Our load balancer polls this endpoint every 5 seconds. If a container fails three consecutive checks, it gets pulled from the rotation. Docker&rsquo;s restart policy (<code>--restart=on-failure:5</code>) handles restarting the process. If it fails 5 times in a row, it stays down and pages us. At that point, something is fundamentally wrong and automatic restarts won&rsquo;t fix it.</p>
<h3 id="what-i-would-tell-myself-six-months-ago">What I Would Tell Myself Six Months Ago</h3>
<p>Start with one service. Not the most important one and not the least important one. Pick something that deploys frequently and has good test coverage. Containerize it, run it in production for two weeks, and fix every sharp edge you find. Then do the next one.</p>
<p>Don&rsquo;t try to containerize your database early. We keep Postgres on a dedicated host with proper backups, WAL archiving, and monitoring. The application containers are stateless and disposable. The database is neither of those things.</p>
<p>Write your Dockerfiles like they will be read by someone debugging a production incident at 3 AM. Because they will be.</p>
<p>Treat image builds as CI artifacts. Build them in CI, tag them with the commit, push them to a registry, deploy from the registry. Never build on a developer laptop and push to production. We burned ourselves on that one.</p>
<p>Docker changed how we ship software at the mobility startup. Deploys went from 20 minutes of SSH-and-pray to 45 seconds of pull-and-start. But the speed only came after we did the unglamorous work of writing good Dockerfiles, setting up log aggregation, adding health checks, and enforcing resource limits. The container is the easy part. The ops around it&rsquo;s the job.</p>
]]></content:encoded></item></channel></rss>