From f68920dd13ef3e320524199ee97393fff07187bf Mon Sep 17 00:00:00 2001 From: rivassec Date: Fri, 9 Oct 2026 22:40:09 -0700 Subject: [PATCH 1/2] ci: add guard against horizontal-rule section dividers in posts scripts/check_section_dividers.py flags CommonMark thematic breaks (and setext --- underlines) in post bodies, skipping frontmatter, fenced code, tables and list items. Wired as a pre-commit hook and a CI workflow, with unit tests. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JoTNmbmc8o4TJDUGG2gd3P --- .github/workflows/section-divider-check.yml | 27 ++++ .pre-commit-config.yaml | 9 ++ scripts/check_section_dividers.py | 146 +++++++++++++++++++ tests/test_check_section_dividers.py | 147 ++++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 .github/workflows/section-divider-check.yml create mode 100755 scripts/check_section_dividers.py create mode 100644 tests/test_check_section_dividers.py diff --git a/.github/workflows/section-divider-check.yml b/.github/workflows/section-divider-check.yml new file mode 100644 index 00000000..b6886722 --- /dev/null +++ b/.github/workflows/section-divider-check.yml @@ -0,0 +1,27 @@ +name: No section dividers + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + check: + name: scan markdown for section dividers + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + + - name: Run section-divider check on all tracked posts + run: scripts/check_section_dividers.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a714ccc7..f51ebdd9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,15 @@ repos: # so the script's "if argv: scan those" path takes over. # No additional args needed. + - id: no-section-dividers + name: no horizontal-rule section dividers in posts + entry: scripts/check_section_dividers.py + language: python + types: [markdown] + files: ^content/ + # Pre-commit passes only the staged files, so the script scans + # those; the script itself skips vendored/generated content docs. + - id: paradigm-shift-warn name: warn on antithesis / paradigm-shift sentences entry: scripts/check_paradigm_shift.py diff --git a/scripts/check_section_dividers.py b/scripts/check_section_dividers.py new file mode 100755 index 00000000..1730d709 --- /dev/null +++ b/scripts/check_section_dividers.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""Fail if any post body uses a horizontal-rule section divider. + +Used by: +- .pre-commit-config.yaml (runs against *staged* post .md files) +- .github/workflows/section-divider-check.yml (runs against *all* posts) + +Why: house style separates sections with headings only (14 of 22 posts +did, before the stragglers were cleaned up). A `---` divider renders as an +
that duplicates the heading right after it, adds visual noise, and +drifts the style from post to post. Use a `## ` heading instead. + +A "divider" is a CommonMark thematic break: a line of 3+ `-`, `*` or `_` +(optionally space-separated, up to 3 leading spaces). Not flagged: Pelican +frontmatter (everything before the first blank line), fenced code blocks +(``` and ~~~), table separator rows, list items. A `---` directly under a +text line is a setext H2 underline in CommonMark; this blog does not use +that heading style, so it is flagged too, with its own message. + +Scope: posts and pages under content/. Two things are excluded because we +do not author them here: content/tools/ (vendored third-party docs such as +VENDORED.md) and content/pages/about.md (regenerated from the GitHub profile +README by scripts/fetch_about.sh on every deploy, so edits would be lost). + +Exits 1 with a file:line:col pointer for every hit. Exits 0 on clean. + +Args: + files (positional, optional) - restrict to these paths. + If empty, scan all tracked post *.md. +""" +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +# CommonMark thematic break: 0-3 spaces, then 3+ of the same marker char, +# optionally separated by spaces/tabs, nothing else on the line. +THEMATIC = re.compile(r"^ {0,3}(?:(?:-[ \t]*){3,}|(?:\*[ \t]*){3,}|(?:_[ \t]*){3,})$") +# A contiguous-dash run under text is a setext H2 underline. +SETEXT_UNDERLINE = re.compile(r"^ {0,3}-+[ \t]*$") +FENCE_OPEN = re.compile(r"^ {0,3}(`{3,}|~{3,})") +# Lines that cannot be a paragraph line a setext underline would attach to. +NOT_PARAGRAPH = re.compile(r"^ {0,3}(?:#|>|[-*+][ \t]|\d+[.)][ \t]|\|)") + +MESSAGE = "horizontal-rule section divider; use a ## heading to separate sections instead" +SETEXT_MESSAGE = ( + "'---' under a text line is a setext H2 heading, a style this blog does " + "not use; use a ## heading to separate sections instead" +) + + +def is_post(path: str) -> bool: + """True for content Markdown we author (not vendored tool docs).""" + p = Path(path).as_posix() + if not p.startswith("content/") or not p.endswith(".md"): + return False + # content/tools/** is vendored third-party material (VENDORED.md etc.). + # content/pages/about.md is overwritten by scripts/fetch_about.sh at deploy. + return not p.startswith("content/tools/") and p != "content/pages/about.md" + + +def tracked_markdown() -> list[Path]: + """All post *.md tracked in git.""" + out = subprocess.run( + ["git", "ls-files", "*.md", "**/*.md"], + check=True, + capture_output=True, + text=True, + ).stdout + return [Path(p) for p in out.splitlines() if p and is_post(p)] + + +def scan(path: Path) -> list[tuple[int, str, str]]: + """Return (line_no, line_text, message) for each divider in path.""" + hits: list[tuple[int, str, str]] = [] + try: + text = path.read_text(encoding="utf-8", errors="replace") + except OSError as e: + print(f"WARN: could not read {path}: {e}", file=sys.stderr) + return hits + + in_meta = True + fence: str | None = None + prev = "" + for lineno, line in enumerate(text.splitlines(), start=1): + if in_meta: + # Pelican metadata runs to the first blank line after it starts. + if line.strip() == "" and lineno > 1: + in_meta = False + elif line.strip() == "": + pass + continue + + if fence is not None: + close = re.match(r"^ {0,3}(" + re.escape(fence[0]) + r"{%d,})[ \t]*$" % len(fence), line) + if close: + fence = None + prev = "" + continue + m = FENCE_OPEN.match(line) + if m: + fence = m.group(1) + prev = "" + continue + + if THEMATIC.match(line): + setext = ( + SETEXT_UNDERLINE.match(line) + and prev.strip() != "" + and not NOT_PARAGRAPH.match(prev) + and not prev.startswith(" ") + ) + hits.append((lineno, line.rstrip(), SETEXT_MESSAGE if setext else MESSAGE)) + prev = line + return hits + + +def main(argv: list[str]) -> int: + if len(argv) > 1: + paths = [Path(p) for p in argv[1:] if p.endswith(".md")] + else: + paths = tracked_markdown() + + total = 0 + for path in paths: + if not path.is_file(): + continue + for lineno, line, message in scan(path): + print(f"::error file={path},line={lineno},col=1::{message}") + print(f" {path}:{lineno}:1: {line}") + total += 1 + + if total: + print( + f"\nFound {total} section divider(s). Delete the rule and let a " + f"## heading separate the sections.", + file=sys.stderr, + ) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tests/test_check_section_dividers.py b/tests/test_check_section_dividers.py new file mode 100644 index 00000000..232a766e --- /dev/null +++ b/tests/test_check_section_dividers.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Tests for scripts/check_section_dividers.py. + +The guard keeps horizontal-rule section dividers out of post bodies (house +style: sections are separated by headings only). Each test writes a tiny +post to a temp file and runs the script as a subprocess, like the +check_descriptions tests do. + +Run with: + + python3 -m unittest discover tests +""" +from __future__ import annotations + +import importlib.util +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +SCRIPT = REPO_ROOT / "scripts" / "check_section_dividers.py" + +FRONT = "Title: T\nDate: 2026-01-01\nSummary: s\n\n" + + +class TestCheckSectionDividers(unittest.TestCase): + def run_on(self, text: str) -> subprocess.CompletedProcess: + with tempfile.NamedTemporaryFile( + "w", suffix=".md", delete=False, encoding="utf-8" + ) as f: + f.write(text) + tmp = Path(f.name) + try: + return subprocess.run( + [sys.executable, str(SCRIPT), str(tmp)], + capture_output=True, + text=True, + ) + finally: + tmp.unlink() + + def assertFlags(self, body: str, lineno: int): + proc = self.run_on(FRONT + body) + self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr) + self.assertRegex(proc.stdout, rf"\.md:{lineno}:1:") + self.assertIn("use a ## heading to separate sections instead", proc.stdout + proc.stderr) + return proc + + def assertClean(self, body: str): + proc = self.run_on(FRONT + body) + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + + # --- violations ------------------------------------------------------- + + def test_clean_post_passes(self): + self.assertClean("Intro.\n\n## One\n\nText.\n\n## Two\n\n- item\n- item\n") + + def test_dash_rule_fails(self): + self.assertFlags("Intro.\n\n---\n\n## One\n", 7) + + def test_star_rule_fails(self): + self.assertFlags("Intro.\n\n***\n\n## One\n", 7) + + def test_underscore_rule_fails(self): + self.assertFlags("Intro.\n\n___\n\n## One\n", 7) + + def test_spaced_variants_fail(self): + self.assertFlags("Intro.\n\n- - -\n\nMore.\n", 7) + self.assertFlags("Intro.\n\n* * *\n\nMore.\n", 7) + self.assertFlags("Intro.\n\n_ _ _\n\nMore.\n", 7) + + def test_long_run_fails(self): + self.assertFlags("Intro.\n\n-----\n\nMore.\n", 7) + + def test_up_to_three_leading_spaces_fail(self): + self.assertFlags("Intro.\n\n ---\n\nMore.\n", 7) + + def test_four_leading_spaces_is_code_not_rule(self): + self.assertClean("Intro.\n\n ---\n\nMore.\n") + + def test_setext_h2_underline_fails_with_explanation(self): + proc = self.assertFlags("Some text\n---\n\nMore.\n", 6) + self.assertIn("setext", proc.stdout + proc.stderr) + + def test_multiple_hits_all_reported(self): + proc = self.run_on(FRONT + "A.\n\n---\n\nB.\n\n***\n\nC.\n") + self.assertEqual(proc.returncode, 1) + self.assertRegex(proc.stdout, r"\.md:7:1:") + self.assertRegex(proc.stdout, r"\.md:11:1:") + + # --- non-violations --------------------------------------------------- + + def test_frontmatter_not_scanned(self): + text = "Title: T\n---\nDate: 2026-01-01\n\nBody.\n" + self.assertEqual(self.run_on(text).returncode, 0) + + def test_backtick_fence_ignored(self): + self.assertClean("Intro.\n\n```yaml\n---\nkey: v\n---\n```\n\nEnd.\n") + + def test_tilde_fence_ignored(self): + self.assertClean("Intro.\n\n~~~yaml\n---\nkey: v\n~~~\n\nEnd.\n") + + def test_fence_closes_and_scanning_resumes(self): + self.assertFlags("```\ncode\n```\n\n---\n\nEnd.\n", 9) + + def test_longer_fence_not_closed_by_shorter(self): + self.assertClean("````\n```\n---\n```\n````\n\nEnd.\n") + + def test_table_separator_rows_ignored(self): + self.assertClean("| a | b |\n| --- | --- |\n| 1 | 2 |\n\n|a|b|\n|---|---|\n|1|2|\n") + + def test_list_items_ignored(self): + self.assertClean("- item\n- another\n* star item\n\n-item\n") + + def test_dash_text_not_rule(self): + self.assertClean("Intro.\n\n--- not a rule\n\n-- also not\n\n--\n") + + # --- CLI -------------------------------------------------------------- + + def test_non_md_args_ignored(self): + with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as f: + f.write(FRONT + "A.\n\n---\n") + tmp = Path(f.name) + try: + proc = subprocess.run( + [sys.executable, str(SCRIPT), str(tmp)], capture_output=True, text=True + ) + finally: + tmp.unlink() + self.assertEqual(proc.returncode, 0) + + def test_default_scope_excludes_vendored_tool_docs(self): + spec = importlib.util.spec_from_file_location("check_section_dividers", SCRIPT) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + self.assertTrue(mod.is_post("content/some-post.md")) + self.assertTrue(mod.is_post("content/pages/accessibility.md")) + self.assertFalse(mod.is_post("content/pages/about.md")) + self.assertFalse(mod.is_post("content/tools/iam-blast-radius/VENDORED.md")) + self.assertFalse(mod.is_post("themes/Flex/README.md")) + self.assertFalse(mod.is_post("README.md")) + + +if __name__ == "__main__": + unittest.main() From a9e67dd7052df6fc88e8a17c6ca5c0a88169380a Mon Sep 17 00:00:00 2001 From: rivassec Date: Fri, 9 Oct 2026 22:40:30 -0700 Subject: [PATCH 2/2] content: drop horizontal-rule section dividers from 8 posts Match the house style of separating sections with headings only (50 dividers removed; text and frontmatter unchanged). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JoTNmbmc8o4TJDUGG2gd3P --- content/208-day-kernel-bug-lessons.md | 6 ------ content/hardening-k8s.md | 18 ------------------ content/hiring-discovery-layer-broken.md | 14 -------------- .../iam-blast-radius-architecture-problem.md | 12 ------------ content/leap-second-chaos-2012.md | 10 ---------- content/oom-killer-process-prioritization.md | 12 ------------ content/trust-decay-adversarial-hiring.md | 10 ---------- content/venezuela-twitter-proxy-osint-final.md | 18 ------------------ 8 files changed, 100 deletions(-) diff --git a/content/208-day-kernel-bug-lessons.md b/content/208-day-kernel-bug-lessons.md index aa395850..123e1327 100644 --- a/content/208-day-kernel-bug-lessons.md +++ b/content/208-day-kernel-bug-lessons.md @@ -25,8 +25,6 @@ return ns; Once the computed value exceeded `0xffffffffffffffff`, it wrapped around, leading to undefined behavior in the scheduler and an unrecoverable state requiring a manual reboot. ---- - ## Why This Matters to DevSecOps This bug is more than a curiosity. It's a classic case study in: @@ -37,8 +35,6 @@ This bug is more than a curiosity. It's a classic case study in: Affected systems included RHEL 5.0 through 5.5 and early RHEL 6 versions running kernels below `2.6.32-220.4.*`. Some Debian-based distributions were likely impacted, though documentation was less complete. ---- - ## Takeaways for Modern Systems - **Live patching tools** like Ksplice, KernelCare, and kpatch can reduce reboot pressure @@ -46,8 +42,6 @@ Affected systems included RHEL 5.0 through 5.5 and early RHEL 6 versions running - **Compliance frameworks** often require timely OS patching, and this bug illustrates why - **CI/CD pipelines for OS-level components** should test for edge cases, including time-based and overflow scenarios ---- - Even today, this incident reminds us that uptime isn't always a badge of honor. In some cases, it's a quiet countdown to failure. For another time-math failure at cloud scale, see [The Chaos of the Leap Second (2012)]({filename}leap-second-chaos-2012.md). *Originally inspired by a 2012 analysis of the `sched_clock()` bug affecting Linux systems with prolonged uptime.* diff --git a/content/hardening-k8s.md b/content/hardening-k8s.md index 3857b4d7..4bc1eeb8 100644 --- a/content/hardening-k8s.md +++ b/content/hardening-k8s.md @@ -26,8 +26,6 @@ securityContext: This enforces that containers don't run as UID 0, reducing the blast radius of any compromise. ---- - ### 2. Drop All Linux Capabilities ```yaml @@ -41,8 +39,6 @@ securityContext: Drop all capabilities by default, then add only what you need. ---- - ### 3. Disable Privilege Escalation ```yaml @@ -52,8 +48,6 @@ securityContext: This prevents processes inside the container from gaining additional privileges, even if compromised. ---- - ### 4. Use Read-Only Filesystem ```yaml @@ -63,8 +57,6 @@ securityContext: This blocks attackers from writing malicious files or installing tools inside the container. ---- - ### 5. Avoid Host Access ```yaml @@ -75,8 +67,6 @@ hostIPC: false Avoid `hostPath` volumes unless absolutely required. These settings ensure your workloads remain isolated from the host. ---- - ### 6. Use Trusted Images and Scan Them Use minimal base images (Alpine, Distroless) and trusted registries. Always scan them: @@ -87,8 +77,6 @@ trivy image your-registry/app:tag This helps catch known CVEs before deployment. ---- - ### 7. Handle Secrets via Volumes (Not Env Vars) ```yaml @@ -107,8 +95,6 @@ containers: Mounting secrets as volumes avoids accidental exposure via logs or `/proc`. ---- - ### 8. Restrict Network Traffic with NetworkPolicies ```yaml @@ -124,8 +110,6 @@ spec: Start with a default-deny policy per namespace, then explicitly allow only the traffic your services need. Without NetworkPolicies, any pod can communicate with any other pod in the cluster. ---- - ### 9. Harden ServiceAccount Usage ```yaml @@ -134,8 +118,6 @@ automountServiceAccountToken: false Disable automatic token mounting for pods that don't need API server access. Create dedicated ServiceAccounts with minimal RBAC bindings rather than relying on the `default` account, which often accumulates unnecessary permissions. ---- - ## Final Thoughts Tools matter less than secure defaults. These practices help harden your Kubernetes workloads using the Restricted Pod Security Standard and reduce risks across the board. diff --git a/content/hiring-discovery-layer-broken.md b/content/hiring-discovery-layer-broken.md index 153c854b..63bca154 100644 --- a/content/hiring-discovery-layer-broken.md +++ b/content/hiring-discovery-layer-broken.md @@ -20,8 +20,6 @@ Many of those same engineers struggle to get past the first screen. That gap says something important about the hiring system. Hiring has a discovery problem. The industry has signal. The signal is poorly indexed, poorly routed, and poorly evaluated. ---- - ## Observability everywhere except the hiring funnel Modern engineering organizations spend enormous effort building observability into production systems. They trace requests, measure latency, monitor failure modes, and build dashboards so they can understand what is happening under pressure. @@ -32,8 +30,6 @@ A resume is a lossy compression format for engineering judgment. It takes years That format can find familiar labels. It struggles to find judgment. ---- - ## Senior signal lives in context Senior engineering skill usually shows up in context: @@ -52,8 +48,6 @@ This creates a predictable failure mode. Candidates optimize resumes for search. The system then calls the result a talent shortage. ---- - ## Some shortages are real. This one is mostly routing. Some shortages are real. Security, infrastructure, identity, Kubernetes, cloud architecture, incident response, and compliance automation all require people who have made real decisions under constraint. Those people are hard to find. @@ -70,8 +64,6 @@ If your hiring process cannot distinguish operational judgment from keyword opti A process that screens mostly for tool names will undervalue people who understand failure modes. A funnel that treats context as noise will discard the signal senior roles depend on. ---- - ## The fix starts with calibration Hiring managers need to give recruiters sharper signal than a list of tools. A good intake should include examples of real problems the person will solve, failure modes the team cares about, and evidence that would prove competence. @@ -86,8 +78,6 @@ For example: Those questions produce better screens than a wall of product names. ---- - ## Better artifacts than resumes Candidates also need better artifacts. @@ -100,8 +90,6 @@ Most hiring systems ignore them because they are not structured for automated pa The shape works both ways. My own writeup of a program that produced measurable outcomes plus the political stall I caused myself, [Adoption Is a Security Control]({filename}paved-road-adoption-as-control.md), is the kind of artifact I would rather be evaluated on than a bullet list. The complement to this post is [The Trust Decay]({filename}trust-decay-adversarial-hiring.md), which frames the same pipeline from the candidate side. ---- - ## What hiring teams should actually ask for Teams should look for direct proof. @@ -115,8 +103,6 @@ Teams should look for direct proof. These questions surface judgment. Keyword filters surface vocabulary. The two are not the same signal. ---- - ## The routing failure has a name The hiring market feels broken because discovery is broken. The people exist. The work exists. The evidence exists. The pipeline loses too much signal before it reaches the people qualified to evaluate it. diff --git a/content/iam-blast-radius-architecture-problem.md b/content/iam-blast-radius-architecture-problem.md index 83078d07..22334666 100644 --- a/content/iam-blast-radius-architecture-problem.md +++ b/content/iam-blast-radius-architecture-problem.md @@ -21,8 +21,6 @@ At that point, the policy review is often just cleanup. Useful cleanup, but stil The real IAM question is not only, "Does this policy look least privileged?" The real question is, "What happens when this identity is compromised?" ---- - ## Policies as documents vs identities as architecture A lot of IAM programs stay shallow because they focus on permissions as isolated documents instead of treating identities as part of the system architecture. A policy can avoid wildcards and still create an unacceptable failure domain. A role can look narrow in isolation and still become dangerous because of who can assume it, what it can mutate, or what other paths it unlocks. @@ -36,8 +34,6 @@ Least privilege is not just fewer actions. Least privilege is smaller failure do This is why reviewing IAM as JSON hygiene misses the point. ---- - ## Better questions than "does this policy contain `*`?" The dangerous question is rarely, "Does this policy contain `Action: *`?" @@ -58,8 +54,6 @@ Those questions move IAM reviews out of the policy document and back into the ar I turned those questions into a browser tool: [IAM Blast Radius analyzer](/tools/iam-blast-radius/) checks a pasted IAM policy for escalation paths, role-assumption reach, and data-access exposure locally, grading each finding by certainty - potential blast radius, not effective permissions. ---- - ## Attackers experience the environment as a graph Attackers do not experience your environment as a set of clean policy JSONs. They experience it as a graph. Trust relationships, deployment pipelines, secrets, buckets, queues, roles, runners, clusters, and logs all become edges in that graph. @@ -74,8 +68,6 @@ A permission that looks harmless in one context can become serious in another: IAM design has to account for that graph. ---- - ## Where compliance checklists fall short Compliance checklists can tell you whether a policy contains obvious broad permissions. They can tell you whether MFA is required. They can tell you whether a role was unused for 90 days. @@ -86,8 +78,6 @@ They do not always tell you whether a compromised CI runner can mutate productio The checklist verifies the shape of the policy. It does not verify the shape of the blast radius. ---- - ## Good IAM architecture starts with containment Containment is a design choice made before the policy review begins: @@ -104,8 +94,6 @@ This is slower than rubber-stamping a policy PR. It is also the work that actual The safer default is easier to enforce when it is baked into the tooling. A small library like [iam-safe-defaults]({filename}iam-safe-defaults-fail-loud.md) can move the argument from "should this role have a permissions boundary" to "why is this role opting out of one," which is where you want the conversation to live. ---- - ## Trust made visible Beyond access control, IAM is one of the main ways cloud architecture expresses trust. Every role, policy, permission boundary, service account, and deployment credential says something about what the system believes can safely happen. diff --git a/content/leap-second-chaos-2012.md b/content/leap-second-chaos-2012.md index d44f5c2c..fa94a776 100644 --- a/content/leap-second-chaos-2012.md +++ b/content/leap-second-chaos-2012.md @@ -17,8 +17,6 @@ On June 30, 2012, a **leap second** was inserted into atomic time via NTP to kee What followed were 500 errors, high latency, and CPU usage spikes that crippled backend services. ---- - ## Why Did It Break? Though seemingly minor, the leap second broke systems in subtle and severe ways: @@ -28,8 +26,6 @@ Though seemingly minor, the leap second broke systems in subtle and severe ways: - **Cloud Weakness Exposure:** An Amazon EC2 outage the day before had already left infrastructure strained. With fewer available instances, systems were more vulnerable when the leap second hit. - **Limited Real-World Testing:** Simulating leap seconds under actual load, in full-stack distributed systems, proved nearly impossible. Pre-patch validations missed edge behavior. ---- - ## Real-World Impact This bug hit nearly every high-scale Java-based system: @@ -40,8 +36,6 @@ This bug hit nearly every high-scale Java-based system: In many cases, the kernel *didn't fail*. The chaos came from how services processed time at runtime. ---- - ## Mitigation & Takeaways ### Immediate Fixes in 2012 @@ -70,8 +64,6 @@ sudo /etc/init.d/ntp start - **Design for Temporal Anomalies**: Distributed systems should assume wall-clock time can regress, freeze, or desync, and gracefully degrade when it does. - **Simulated Testing Isn't Enough**: Always combine synthetic load with chaos testing under unusual real-world conditions (e.g., leap seconds, DNS failures, NTP skew). ---- - ## Epilogue The 2012 leap second chaos wasn't caused by incompetence. Many teams patched, prepared, and tested. But the leap second hit during degraded cloud capacity, exposed fragile JVM behavior, and stressed assumptions in time-sensitive code. @@ -80,8 +72,6 @@ A single second exposed fault lines in the foundations of the modern internet. In 2022, the CGPM (General Conference on Weights and Measures) voted to abolish leap seconds by 2035, largely driven by incidents like this one. Until then, the mitigations above remain essential for any system that touches wall-clock time. ---- - **What other "just time" failures have caught you off guard in production? Let's share war stories.** Related reading: the [208.5-day kernel bug]({filename}208-day-kernel-bug-lessons.md) - another case where a piece of time math, left unpatched, was a silent countdown to an outage. diff --git a/content/oom-killer-process-prioritization.md b/content/oom-killer-process-prioritization.md index 9bd81c97..fc4e4c75 100644 --- a/content/oom-killer-process-prioritization.md +++ b/content/oom-killer-process-prioritization.md @@ -15,8 +15,6 @@ In resource-constrained environments (especially virtual private servers, CI age The OOM Killer uses heuristics (like memory usage and the `oom_score_adj` value) to select processes it deems less essential. But you don't have to leave that critical decision entirely to the kernel's default logic. ---- - ## The Incident Years ago, I had to recover a VPS via remote console. A quick dive into `/var/log/messages` showed that the OOM Killer had struck, terminating critical services. The culprit? A perfect storm: @@ -27,8 +25,6 @@ Years ago, I had to recover a VPS via remote console. A quick dive into `/var/lo This combination overwhelmed system memory. Without process priority tuning, the OOM Killer started targeting processes based on its heuristics, which felt indiscriminate from an operational view as it even took down `sshd`. ---- - ## The Mitigation Strategy You can significantly influence OOM Killer decisions using the `/proc//oom_score_adj` setting for a process. This value ranges from -1000 to +1000. The kernel uses this score, combined with memory usage, to decide kill priority; a lower score makes the process less likely to be chosen relative to others. @@ -98,8 +94,6 @@ done < "$CONFIG" echo "OOM score adjustment complete." ``` ---- - ## Running the Script You can run this periodically via cron or on boot with systemd. For example: @@ -126,8 +120,6 @@ sudo systemctl daemon-reload sudo systemctl enable --now oom-adjuster.service ``` ---- - ## Security Considerations From a [DevSecOps perspective](devsecops-guide.html), OOM prioritization is a security hardening technique as much as an uptime one: @@ -139,16 +131,12 @@ From a [DevSecOps perspective](devsecops-guide.html), OOM prioritization is a se Misconfigured systems where critical daemons (like `iptables`, `auditd`, `sshd`, or VPN tunnels) are killed first expose themselves to avoidable downtime and security gaps. ---- - ## Modern Use Cases - **Kubernetes nodes**: Influence OOM behavior via Quality of Service (QoS) classes (set by defining resource requests/limits in pod specs), or apply node-level tuning using methods like the script above for critical node components (e.g., kubelet, container runtime). For the pod-side of the same problem, see [Hardening Kubernetes Deployments]({filename}hardening-k8s.md). - **CI/CD runners**: Protect build agents or essential runner services from being killed during resource-intensive test suites or concurrent builds. - **Shared hosting / VPS**: Prioritize core services (web server, database, SSH) over potentially less critical user processes or background tasks. ---- - ## Conclusion The OOM Killer is an essential part of the Linux kernel, but leaving process termination order purely to default heuristics can be risky in production. By strategically assigning `oom_score_adj` values based on business continuity and security priorities, you can significantly reduce recovery time and harden your systems against memory pressure scenarios. diff --git a/content/trust-decay-adversarial-hiring.md b/content/trust-decay-adversarial-hiring.md index 87b74ee2..2f72783b 100644 --- a/content/trust-decay-adversarial-hiring.md +++ b/content/trust-decay-adversarial-hiring.md @@ -16,8 +16,6 @@ The tech job market is currently defined by a jarring paradox. On one side, elit The hiring pipeline has ceased to be a discovery engine designed to find talent. It has evolved into a defensive perimeter designed to mitigate risk in a low-trust environment. ---- - ## From Discovery to Defense: The Death of Honest Inputs Historically, recruitment operated on the assumption of "manageable honesty." You received a stack of resumes, assumed most were reasonably accurate, and searched for the best fit. @@ -30,8 +28,6 @@ That model has collapsed. Today, hiring systems are bombarded by "strategically From a systems engineering perspective, the pipeline is now facing adversarial inputs. When a system is flooded with high-volume, low-integrity data, it naturally shifts its posture from "open" to "fortified." ---- - ## How the System Defends Itself When trust in incoming data drops, the system compensates with three defensive maneuvers: @@ -48,16 +44,12 @@ As presentation becomes commoditized through AI, "looking the part" no longer se Because the public pipeline is compromised, hiring teams are retreating to "pre-validated" channels. This explains the heavy reliance on internal referrals and known networks. Less cronyism than architectural necessity: it is how teams find signal in a sea of noise. ---- - ## The Feedback Loop of Friction We are trapped in a recursive cycle. Candidates optimize harder to bypass filters; in response, filters become more draconian. This creates a "Degraded Trust Loop" where the system's own success at filtering further incentivizes candidates to game the system. Ultimately, the pipeline stops being a way to find people and becomes a way to manage risk. ---- - ## The New Strategy: Proof Over Presentation If the market is a low-trust system, "Presentation" (how you describe yourself) is losing its value. What remains valuable is Evidence - signals that are computationally or socially expensive to fake. @@ -70,8 +62,6 @@ To bypass the defensive perimeter, engineers must move beyond the resume. The go - **Tangible Artifacts:** Real-world contributions - whether through open-source modules, infrastructure-as-code repos, or documented homelabs - serve as proof-of-work. The artifact that survives your attention is the one that names the failure modes inline, not just the success path; the longest case study I have of that is [The Teensy That Failed in Public]({filename}teensy-efi-bruteforce-hours-late.md). - **Impact-Oriented Signaling:** Shift from "tasks completed" to "business outcomes achieved." Hard numbers on risk reduction, latency improvements, or cost savings are much harder to hallucinate effectively. ---- - ## A DevSecOps View of the Career If we treat the job market as a security problem, the solution becomes clear. The hiring pipeline is a system with exposed endpoints and high validation costs. As any security professional knows, when you can't trust the input, you lean on Multi-Factor Authentication. In hiring, your "factors" are your network, your public evidence, and your verifiable history. diff --git a/content/venezuela-twitter-proxy-osint-final.md b/content/venezuela-twitter-proxy-osint-final.md index bb2759c1..52341990 100644 --- a/content/venezuela-twitter-proxy-osint-final.md +++ b/content/venezuela-twitter-proxy-osint-final.md @@ -9,14 +9,10 @@ Author: Oliver Rivas Summary: In 2012 I traced a state-aligned Twitter proxy tied to Venezuela's ruling party. OSINT lessons for spotting subtle, credential-phishing nation-state infra. Cover: images/covers/venezuela-twitter-proxy-osint.png ---- - ## Situation In the lead-up to Venezuela's 2012 regional elections, I observed unusual behavior around Twitter access within the country. What began as anecdotal reports of DNS outages evolved into a deeper investigation that revealed a state-aligned proxy infrastructure potentially capable of phishing Twitter credentials. ---- - ## Key Finding A subdomain under `chavezcandanga.org.ve` (the official handle of then-President Hugo Chávez) was hosting a **transparent proxy to Twitter**. @@ -29,8 +25,6 @@ While it initially showed no malicious behavior, it was: - Promoted through state-controlled media and bot accounts - Served from the same IP as a political messaging app ---- - ## OSINT Breakdown ### 1. DNS Resolution @@ -49,8 +43,6 @@ This IP **served Twitter content** without being operated by Twitter Inc. It's unclear whether this redirection was caused by ISP DNS override, local resolver poisoning, or upstream hijack, but the net effect was consistent: Twitter domains were silently redirected to non-Twitter infrastructure under state control. ---- - ### 2. WHOIS and Hosting Clues ```bash @@ -62,8 +54,6 @@ Revealed that the domain was registered to **PSUV** (*Partido Socialista Unido d ![WHOIS output showing chavezcandanga.org.ve registered to PSUV]({static}/images/who-is-chavezcandanga-com.webp) *Figure: WHOIS lookup confirms chavezcandanga.org.ve is registered to PSUV, with administrative and technical contacts using @psuv.org.ve emails.* ---- - ### 3. Application Infrastructure The same server IP hosted: @@ -76,8 +66,6 @@ The same server IP hosted: At the time of discovery, this site **did not contain malicious code**, but the potential for **credential harvesting** during peak election activity was substantial. The authentication flow mimicked Twitter's branding and prompted users to log in, creating a window for silent credential capture, token misuse, or targeted amplification based on follower behavior. ---- - ## Threat Model & Implications - **Credential Harvesting Risk**: Even without malware, a proxy to Twitter login enables password theft. @@ -86,8 +74,6 @@ At the time of discovery, this site **did not contain malicious code**, but the - **Infrastructure trust erosion**: Even minor state-level interference with DNS or TLS undermines confidence in web authentication across the board. - **Evasion of International Scrutiny**: By mimicking Twitter directly, users could be deceived into trusting a controlled proxy. ---- - ## Lessons for DevSecOps & Threat Intelligence Today - **Verify SSL and domain trust chains** during high-risk periods like elections. The minimum mental model for what "trust chain" means in TLS is in [TLS Has Three Jobs]({filename}tls-three-jobs.md): hostname binding, validity window, signature chain to a trusted root, revocation, and key usage. Each checked in order, each a place a state actor can intercept. @@ -97,16 +83,12 @@ At the time of discovery, this site **did not contain malicious code**, but the - Document and archive suspicious infra using tools like the Wayback Machine. - Phishing infrastructure can be **state-sponsored and subtle**, so early detection matters. ---- - ## Epilogue The proxy remained active until at least December 2012, shortly before elections. To this day, the archived proxy content and WHOIS records serve as a warning about the ease with which social media can be co-opted in hostile environments. This investigation was one of the earliest times I realized how fragile trusted infrastructure becomes in the hands of a motivated actor, and how critical open-source techniques are in defending it. ---- - **Have you ever spotted unusual network redirections or infrastructure anomalies? What tools or tactics helped you confirm your suspicions?** *Originally published in 2012 and revisited in 2025 to reflect current DevSecOps and threat intelligence practices.*