feat: implement dynamic Eleventy tiling blog, modular CSS, and agent customizations

- Add project foundation README.md and root AGENTS.md for pair programming
- Configure .agents/ rules and skills for Eleventy tiling and Obsidian publishing
- Decompose monolithic styles into modular tokens, reset, layout, and component CSS
- Implement Nunjucks layout inheritance (base.njk, post.njk, page.njk) and reusable partials
- Configure Eleventy 3 with markdown-it-anchor, syntax highlighting, and dynamic collections
- Migrate sample prototypes into dynamic dispatches lander, gateway, and Markdown posts
- Add multi-stage Dockerfile and clean-URL nginx.conf for production serving
This commit is contained in:
venus
2026-09-13 00:49:27 -05:00
parent c0f67e75a3
commit d8fb8a3699
37 changed files with 4361 additions and 7 deletions
+47
View File
@@ -0,0 +1,47 @@
{
"name": "River Rooks",
"title": "River Rooks | Technical Publication",
"description": "Engineering Notes, CTF Write-Ups & Systems Architecture",
"status": "SYSTEMS NOMINAL",
"author": "River Rooks",
"systemSpec": "IEEE STD 1003.1",
"nav": [
{
"label": "Dispatches",
"url": "/"
},
{
"label": "Gateway 3",
"url": "/gateway/"
},
{
"label": "Git Forge",
"url": "https://git.rooks.dev",
"external": true
},
{
"label": "RSS Feed",
"url": "/feed.xml"
}
],
"topics": [
{
"title": "Hardware & Firmware",
"slug": "firmware",
"desc": "SPI dumping, UART interfacing & flash memory extraction",
"badge": "TRACE: HARDWARE"
},
{
"title": "Systems Architecture",
"slug": "homelab",
"desc": "Traefik routing, WireGuard mesh & containerized microkernels",
"badge": "SYSTEM: INFRA"
},
{
"title": "Offensive Security",
"slug": "security",
"desc": "Kernel exploitation, binary reverse-engineering & CTF writeups",
"badge": "CTF: OFFENSIVE"
}
]
}
+34
View File
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title | default(site.title) }}</title>
<meta name="description" content="{{ excerpt | default(site.description) }}">
<!-- Webfonts: IBM Plex Mono, JetBrains Mono, Inter -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:ital,wght@0,400;0,500;0,700;1,400&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet">
<!-- Modular Bundled Stylesheet -->
<link rel="stylesheet" href="/assets/css/main.css">
{% block head %}{% endblock %}
</head>
<body>
<div class="tiling-layout">
{% include "partials/global-bar.njk" %}
<main id="main-content">
{% block content %}
{{ content | safe }}
{% endblock %}
</main>
{% include "partials/footer.njk" %}
</div>
{% block scripts %}{% endblock %}
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
{% extends "layouts/base.njk" %}
{% block content %}
<div class="page-container">
{{ content | safe }}
</div>
{% endblock %}
+93
View File
@@ -0,0 +1,93 @@
{% extends "layouts/base.njk" %}
{% block content %}
<div class="article-workspace-grid">
<!-- Left Sticky Sidebar: Document Outline & Telemetry -->
<aside class="article-sidebar" aria-label="Article Sidebar">
<!-- Dynamic Table of Contents -->
{% set headings = content | extractHeadings %}
{% if headings.length > 0 %}
<div class="sidebar-tile">
<div class="sidebar-heading">
<span>DOCUMENT OUTLINE</span>
<span style="color: var(--accent-cyan);">[{{ headings.length }}]</span>
</div>
<nav class="toc-nav" aria-label="Table of Contents">
{% for h in headings %}
<a href="#{{ h.id }}" class="toc-link {% if h.level == 3 %}toc-sub{% endif %}">
{{ h.title }}
</a>
{% endfor %}
</nav>
</div>
{% endif %}
<!-- Article Telemetry Tile -->
<div class="sidebar-tile">
<div class="sidebar-heading">DISPATCH TELEMETRY</div>
<div class="sidebar-stats-box" style="padding: 0; background: transparent; border: none;">
<div class="stat-row">
<span>PUBLISHED:</span>
<span class="stat-val">{{ date | postDate }}</span>
</div>
<div class="stat-row">
<span>DURATION:</span>
<span class="stat-val">{{ content | readingTime(readingTime) }}</span>
</div>
{% if difficulty %}
<div class="stat-row">
<span>DIFFICULTY:</span>
<span class="stat-val" style="color: var(--accent-amber);">{{ difficulty }}</span>
</div>
{% endif %}
</div>
<div style="padding-top: 10px; border-top: var(--border-width) solid var(--border-color);">
<div style="font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-muted); margin-bottom: 6px;">TOPIC TAGS:</div>
<div class="tag-cluster">
{% for tag in tags | filterTagList %}
<span class="tag-chip">#{{ tag }}</span>
{% endfor %}
</div>
</div>
<div style="padding-top: 10px; border-top: var(--border-width) solid var(--border-color);">
<a href="/" class="filter-tag-btn" style="width: 100%; justify-content: center;">
&larr; Return to Dispatches
</a>
</div>
</div>
</aside>
<!-- Right Main Article Workspace -->
<article class="article-tile" aria-labelledby="article-title">
<header class="article-header-block">
<nav class="article-breadcrumbs" aria-label="Breadcrumb">
<a href="/">~/NOTES</a>
<span>/</span>
<span>{{ (tags | filterTagList)[0] | default('DISPATCHES') | upper }}</span>
<span>/</span>
<span style="color: var(--accent-cyan);">{{ page.fileSlug | upper }}.MD</span>
</nav>
<h1 id="article-title" class="article-main-title">{{ title }}</h1>
<div class="article-meta-row">
<span class="meta-item">
<time datetime="{{ date | isoDate }}">{{ date | postDate }}</time>
</span>
<span>&bull;</span>
<span class="meta-item">{{ content | readingTime(readingTime) }}</span>
{% if difficulty %}
<span>&bull;</span>
<span class="meta-item" style="color: var(--accent-amber);">DIFFICULTY: {{ difficulty }}</span>
{% endif %}
</div>
</header>
<div class="markdown-prose">
{{ content | safe }}
</div>
</article>
</div>
{% endblock %}
+30
View File
@@ -0,0 +1,30 @@
{#
Reusable Tile Container Macro (macros/tile.njk)
Renders a terminal pane with header, title, meta pill, and custom body slot.
Usage:
{% import "macros/tile.njk" as ui %}
{% call ui.tile(title="STATISTICS", meta="STATIC HTML") %}
<p>Inside tile</p>
{% endcall %}
#}
{% macro tile(title="", meta="", class="", id="") %}
<section class="tile {{ class }}" {% if id %}id="{{ id }}"{% endif %}>
{% if title or meta %}
<header class="tile-header">
{% if title %}
<div class="tile-title">
<span class="status-indicator"></span>
{{ title }}
</div>
{% endif %}
{% if meta %}
<span class="tile-meta">{{ meta }}</span>
{% endif %}
</header>
{% endif %}
<div class="tile-content">
{{ caller() }}
</div>
</section>
{% endmacro %}
+62
View File
@@ -0,0 +1,62 @@
{% set featured = null %}
{% for p in collections.posts %}
{% if p.data.featured and not featured %}
{% set featured = p %}
{% endif %}
{% endfor %}
{% if not featured and collections.posts.length > 0 %}
{% set featured = collections.posts[0] %}
{% endif %}
{% if featured %}
<section class="tile" aria-label="Featured Technical Dispatch">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
FEATURED DISPATCH
</div>
<span class="tile-meta">{{ (featured.data.tags | filterTagList)[0] | default('SYSTEMS') | upper }} &bull; {{ featured.templateContent | readingTime(featured.data.readingTime) }}</span>
</header>
<div class="featured-card-body">
<div class="featured-badge-row">
<span class="badge-pinned">[FEATURED DEEP DIVE]</span>
<time datetime="{{ featured.date | isoDate }}">{{ featured.date | postDate }}</time>
</div>
<h2 style="margin: 0; font-size: inherit;">
<a href="{{ featured.url }}" class="featured-article-title">{{ featured.data.title }}</a>
</h2>
<p class="featured-prose">
{{ featured.data.excerpt }}
</p>
<!-- Terminal Code Preview Snippet -->
<div class="terminal-code-box">
<div class="code-titlebar">
<span class="code-title">
<span style="color: var(--accent-cyan);">&gt;</span>
<span>flashrom_read.sh</span>
</span>
<span>POSIX sh</span>
</div>
<div class="code-content">
<pre><code class="language-bash"># Probe SPI flash chip and dump 16MB image
flashrom -p ch341a_spi -r firmware_dump.bin -V
# Verify checksums across consecutive reads
sha256sum dump1.bin dump2.bin</code></pre>
</div>
</div>
<div class="featured-cta-row">
<div class="tag-cluster">
{% for tag in featured.data.tags | filterTagList %}
<span class="tag-chip">#{{ tag }}</span>
{% endfor %}
</div>
<a href="{{ featured.url }}" class="cta-button">READ FULL WRITEUP &rarr;</a>
</div>
</div>
</section>
{% endif %}
+13
View File
@@ -0,0 +1,13 @@
<footer class="footer-tile" aria-label="Global Footer">
<div class="footer-left">
<span class="status-indicator"></span>
<span>RUNTIME: ELEVENTY 3.x STATIC</span>
<span>&bull;</span>
<span>THEME: NATIVE TERMINAL DARK</span>
</div>
<div class="footer-right">
<span>&copy; {{ currentYear or "2026" }} {{ site.author }}</span>
<a href="https://git.rooks.dev" target="_blank" rel="noopener noreferrer" class="footer-link">Source Forge ↗</a>
</div>
</footer>
+16
View File
@@ -0,0 +1,16 @@
<header class="global-bar">
<a href="/" class="brand-group" aria-label="{{ site.name }} Home">
<span class="status-indicator"></span>
<span>{{ site.name }}</span>
</a>
<nav class="bar-nav" aria-label="Quick Navigation">
{% for item in site.nav %}
<a href="{{ item.url }}"
class="bar-link {% if item.external %}external{% endif %} {% if page.url == item.url or (item.url != '/' and page.url.startsWith(item.url)) %}active{% endif %}"
{% if item.external %}target="_blank" rel="noopener noreferrer"{% endif %}>
{% if page.url == item.url %}&gt; {% endif %}{{ item.label }}
</a>
{% endfor %}
</nav>
</header>
+24
View File
@@ -0,0 +1,24 @@
<section class="hero-tile" aria-label="Blog Mission & Overview">
<div class="hero-pretitle">TECHNICAL PUBLICATION REPOSITORY &bull; ~/NOTES/INDEX.MD</div>
<h1 class="hero-main-title">{{ heroTitle or site.description }}</h1>
<p class="hero-landing-text">
{{ heroText or "A fast, zero-bloat digital workshop detailing hardware reverse engineering, low-level POSIX environments, self-hosted infrastructure, and kernel security research. Designed for dense information retrieval." }}
</p>
<div class="filter-pills-row" aria-label="Topic Filters">
<a href="/" class="filter-tag-btn {% if not currentTag %}active{% endif %}">
[All Dispatches ({{ collections.posts.length }})]
</a>
{% for tag in collections.tagList %}
{% set tagCount = 0 %}
{% for post in collections.posts %}
{% if tag in (post.data.tags or []) %}
{% set tagCount = tagCount + 1 %}
{% endif %}
{% endfor %}
<span class="filter-tag-btn {% if currentTag == tag %}active{% endif %}">
[#{{ tag }}{% if tagCount > 0 %} ({{ tagCount }}){% endif %}]
</span>
{% endfor %}
</div>
</section>
+27
View File
@@ -0,0 +1,27 @@
<article class="feed-card" aria-labelledby="post-title-{{ post.fileSlug }}">
<div class="feed-meta-row">
<time class="feed-date" datetime="{{ post.date | isoDate }}">
{{ post.date | postDate }}
</time>
<span>{{ post.templateContent | readingTime(post.data.readingTime) }}</span>
</div>
<h3 id="post-title-{{ post.fileSlug }}">
<a href="{{ post.url }}" class="feed-heading">{{ post.data.title }}</a>
</h3>
{% if post.data.excerpt %}
<p class="feed-excerpt">{{ post.data.excerpt }}</p>
{% endif %}
<div class="feed-tags-row">
<div class="tag-cluster">
{% for tag in post.data.tags | filterTagList %}
<span class="tag-chip">#{{ tag }}</span>
{% endfor %}
</div>
{% if post.data.difficulty %}
<span>DIFFICULTY: {{ post.data.difficulty }}</span>
{% endif %}
</div>
</article>
+54
View File
@@ -0,0 +1,54 @@
<aside aria-label="Archive Statistics & Topics" style="display: flex; flex-direction: column; gap: var(--tile-gap);">
<!-- Archive Statistics Tile -->
<section class="tile" aria-label="Archive Telemetry">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
ARCHIVE TELEMETRY
</div>
<span class="tile-meta">STATIC HTML</span>
</header>
<div class="sidebar-stats-box">
<div class="stat-row">
<span>TOTAL DISPATCHES:</span>
<span class="stat-val">{{ collections.posts.length }} PUBLISHED</span>
</div>
<div class="stat-row">
<span>TOPIC DOMAINS:</span>
<span class="stat-val">{{ collections.tagList.length }} CATEGORIES</span>
</div>
<div class="stat-row">
<span>CORE SPECIFICATION:</span>
<span class="stat-val">{{ site.systemSpec }}</span>
</div>
<div class="stat-row">
<span>OPERATIONAL STATUS:</span>
<span class="stat-val" style="color: var(--accent-green);">{{ site.status }}</span>
</div>
</div>
</section>
<!-- Curated Topic Tracks Tile -->
<section class="tile" aria-label="Curated Topic Tracks">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
TOPIC TRACKS
</div>
<span class="tile-meta">CURATED PATHS</span>
</header>
<div class="tile-content">
{% for topic in site.topics %}
<div class="topic-track-item">
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<span class="topic-track-title">{{ topic.title }}</span>
<span style="font-family: var(--font-mono); font-size: 0.68rem; color: var(--accent-cyan);">{{ topic.badge }}</span>
</div>
<p class="topic-track-desc">{{ topic.desc }}</p>
</div>
{% endfor %}
</div>
</section>
</aside>
+298
View File
@@ -0,0 +1,298 @@
/* ==========================================================================
Article Workspace & Markdown Prose (components/article.css)
========================================================================== */
/* Left Sticky Sidebar */
.article-sidebar {
display: flex;
flex-direction: column;
gap: var(--tile-gap);
position: sticky;
top: 16px;
}
.sidebar-tile {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 18px;
display: flex;
flex-direction: column;
gap: 12px;
}
.sidebar-heading {
font-family: var(--font-mono);
font-size: 0.82rem;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: var(--border-width) solid var(--border-color);
padding-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
/* Document Table of Contents */
.toc-nav {
display: flex;
flex-direction: column;
gap: 4px;
font-family: var(--font-mono);
font-size: 0.78rem;
}
.toc-link {
color: var(--text-muted);
text-decoration: none;
padding: 4px 6px;
border-radius: var(--border-radius-sm);
line-height: 1.4;
display: block;
}
.toc-link:hover {
background-color: var(--bg-elevated);
color: var(--accent-cyan);
}
.toc-link.active {
color: var(--accent-cyan);
background-color: var(--bg-canvas);
border-left: 2px solid var(--accent-cyan);
font-weight: 700;
}
.toc-sub {
padding-left: 16px;
color: var(--text-muted);
font-size: 0.74rem;
}
/* Main Article Reading Container */
.article-tile {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 36px 40px;
display: flex;
flex-direction: column;
gap: 24px;
}
@media (max-width: 600px) {
.article-tile {
padding: 24px 18px;
}
}
/* Article Header */
.article-header-block {
border-bottom: var(--border-width) solid var(--border-color);
padding-bottom: 24px;
display: flex;
flex-direction: column;
gap: 12px;
}
.article-breadcrumbs {
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 8px;
}
.article-breadcrumbs a {
color: var(--text-muted);
text-decoration: none;
}
.article-breadcrumbs a:hover {
color: var(--accent-cyan);
}
.article-main-title {
font-family: var(--font-mono);
font-size: 2.2rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.02em;
line-height: 1.25;
}
@media (max-width: 768px) {
.article-main-title {
font-size: 1.7rem;
}
}
.article-meta-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 14px;
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--text-muted);
}
.meta-item {
display: flex;
align-items: center;
gap: 6px;
}
/* Markdown Prose Hierarchy */
.markdown-prose {
max-width: var(--prose-max-width);
font-size: 1.02rem;
color: var(--text-primary);
line-height: 1.7;
}
.markdown-prose p {
margin-bottom: 18px;
}
.markdown-prose h2 {
font-family: var(--font-mono);
font-size: 1.45rem;
font-weight: 700;
color: var(--text-primary);
margin: 36px 0 16px 0;
padding-bottom: 8px;
border-bottom: var(--border-width) solid var(--border-color);
letter-spacing: -0.01em;
}
.markdown-prose h3 {
font-family: var(--font-mono);
font-size: 1.15rem;
font-weight: 700;
color: var(--text-primary);
margin: 28px 0 12px 0;
}
.markdown-prose a {
color: var(--accent-cyan);
text-decoration: underline;
text-underline-offset: 3px;
}
.markdown-prose a:hover {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
text-decoration: none;
}
/* Inline Code */
.markdown-prose code:not(pre code) {
font-family: var(--font-code);
font-size: 0.88rem;
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
padding: 2px 6px;
border-radius: var(--border-radius-xs);
color: var(--accent-cyan);
}
/* Keyboard Shortcut Hints */
kbd {
font-family: var(--font-mono);
font-size: 0.78rem;
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
padding: 2px 6px;
border-radius: var(--border-radius-xs);
color: var(--text-primary);
box-shadow: none;
}
/* Markdown Callouts */
.markdown-callout {
margin: 20px 0;
padding: 14px 18px;
border-radius: var(--border-radius-sm);
border: var(--border-width) solid var(--border-color);
background-color: var(--bg-canvas);
font-size: 0.94rem;
line-height: 1.6;
}
.callout-note {
border-left: 4px solid var(--accent-cyan);
}
.callout-warning {
border-left: 4px solid var(--accent-amber);
}
.callout-title {
font-family: var(--font-mono);
font-weight: 700;
font-size: 0.82rem;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 6px;
}
.callout-note .callout-title {
color: var(--accent-cyan);
}
.callout-warning .callout-title {
color: var(--accent-amber);
}
/* Monospace Tables */
.table-container {
width: 100%;
overflow-x: auto;
margin: 22px 0;
}
.markdown-prose table,
.markdown-table {
width: 100%;
border-collapse: collapse;
font-size: 0.86rem;
font-family: var(--font-mono);
text-align: left;
margin: 20px 0;
border: var(--border-width) solid var(--border-color);
}
.markdown-prose th,
.markdown-table th {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
padding: 8px 12px;
color: var(--text-muted);
font-weight: 700;
}
.markdown-prose td,
.markdown-table td {
border: var(--border-width) solid var(--border-color);
padding: 8px 12px;
color: var(--text-primary);
}
.markdown-prose tr:hover,
.markdown-table tr:hover {
background-color: var(--bg-elevated);
}
/* Lists */
.markdown-prose ul, .markdown-prose ol {
margin: 16px 0 18px 24px;
}
.markdown-prose li {
margin-bottom: 6px;
}
+366
View File
@@ -0,0 +1,366 @@
/* ==========================================================================
Cards & Feeds (components/cards.css)
========================================================================== */
/* Featured Lead Article Card (Inside Tile) */
.featured-card-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.featured-badge-row {
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-mono);
font-size: 0.76rem;
flex-wrap: wrap;
gap: 8px;
}
.badge-pinned {
color: var(--accent-amber);
font-weight: 700;
}
.featured-article-title {
font-family: var(--font-mono);
font-size: 1.35rem;
font-weight: 700;
color: var(--text-primary);
text-decoration: none;
line-height: 1.3;
}
.featured-article-title:hover {
color: var(--accent-cyan);
}
.featured-prose {
font-size: 0.92rem;
color: var(--text-primary);
line-height: 1.6;
}
.featured-cta-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
border-top: var(--border-width) solid var(--border-color);
flex-wrap: wrap;
gap: 10px;
font-family: var(--font-mono);
}
.cta-button {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
padding: 6px 14px;
font-weight: 700;
font-size: 0.8rem;
text-decoration: none;
border-radius: var(--border-radius-sm);
border: var(--border-width) solid var(--accent-cyan);
display: inline-block;
}
.cta-button:hover {
background-color: transparent;
color: var(--accent-cyan);
}
/* Sidebar Stats / Telemetry Box */
.sidebar-stats-box {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 8px;
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--text-muted);
}
.stat-row {
display: flex;
justify-content: space-between;
}
.stat-val {
color: var(--text-primary);
font-weight: 700;
}
/* Curated Topic Track Item */
.topic-track-item {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 4px;
text-decoration: none;
}
.topic-track-item:hover {
border-color: var(--accent-cyan);
background-color: var(--bg-elevated);
}
.topic-track-title {
font-family: var(--font-mono);
font-size: 0.84rem;
font-weight: 700;
color: var(--text-primary);
}
.topic-track-item:hover .topic-track-title {
color: var(--accent-cyan);
}
.topic-track-desc {
font-size: 0.78rem;
color: var(--text-muted);
line-height: 1.4;
}
/* Full-Width Chronological Article Feed */
.feed-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.feed-card {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 8px;
text-decoration: none;
}
.feed-card:hover {
border-color: var(--accent-cyan);
background-color: var(--bg-elevated);
}
.feed-meta-row {
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-mono);
font-size: 0.76rem;
color: var(--text-muted);
}
.feed-date {
color: var(--accent-cyan);
}
.feed-heading {
font-family: var(--font-mono);
font-size: 1.1rem;
font-weight: 700;
color: var(--text-primary);
text-decoration: none;
line-height: 1.35;
}
.feed-card:hover .feed-heading {
color: var(--accent-cyan);
}
.feed-excerpt {
font-size: 0.9rem;
color: var(--text-primary);
line-height: 1.6;
}
.feed-tags-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 6px;
border-top: var(--border-width) solid var(--border-color);
font-family: var(--font-mono);
font-size: 0.74rem;
color: var(--text-muted);
flex-wrap: wrap;
gap: 6px;
}
.tag-cluster {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag-chip {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
padding: 2px 6px;
border-radius: var(--border-radius-sm);
color: var(--text-muted);
text-decoration: none;
font-family: var(--font-mono);
font-size: 0.74rem;
}
.tag-chip:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
/* Featured Service Card (Homelab Gateway) */
.blog-service-feature {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.blog-feature-top {
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-mono);
font-size: 0.78rem;
}
.badge-core {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
padding: 2px 6px;
font-weight: 700;
border-radius: var(--border-radius-sm);
}
.blog-feature-title {
font-family: var(--font-mono);
font-size: 1.15rem;
font-weight: 700;
color: var(--text-primary);
text-decoration: none;
display: inline-block;
}
.blog-feature-title:hover {
color: var(--accent-cyan);
}
.blog-feature-desc {
font-size: 0.9rem;
color: var(--text-primary);
line-height: 1.5;
}
.blog-sublinks {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 4px;
border-top: var(--border-width) solid var(--border-color);
padding-top: 10px;
}
.blog-sublink-item {
display: flex;
justify-content: space-between;
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--text-primary);
text-decoration: none;
padding: 4px 6px;
border-radius: var(--border-radius-sm);
}
.blog-sublink-item:hover {
background-color: var(--bg-elevated);
color: var(--accent-cyan);
}
/* Homelab Services Mini-Grid */
.services-mini-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
@media (max-width: 540px) {
.services-mini-grid {
grid-template-columns: 1fr;
}
}
.service-tile-item {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 12px;
text-decoration: none;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 6px;
}
.service-tile-item:hover {
border-color: var(--accent-cyan);
background-color: var(--bg-elevated);
}
.service-item-title {
font-family: var(--font-mono);
font-size: 0.88rem;
font-weight: 700;
color: var(--text-primary);
}
.service-tile-item:hover .service-item-title {
color: var(--accent-cyan);
}
.service-item-tag {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--text-muted);
}
.service-item-desc {
font-size: 0.8rem;
color: var(--text-muted);
line-height: 1.4;
}
/* Infrastructure Node Telemetry */
.node-stat-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
font-family: var(--font-mono);
font-size: 0.78rem;
}
.node-name {
color: var(--text-primary);
font-weight: 700;
}
.node-load {
color: var(--accent-green);
}
+123
View File
@@ -0,0 +1,123 @@
/* ==========================================================================
Terminal Code Blocks & Syntax Highlighting (components/code-box.css)
========================================================================== */
/* Terminal Code Box Component */
.terminal-code-box,
.code-block-container {
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
background-color: var(--bg-canvas);
overflow: hidden;
font-family: var(--font-code);
margin: 18px 0;
}
.code-titlebar,
.code-block-header {
background-color: var(--bg-surface);
border-bottom: var(--border-width) solid var(--border-color);
padding: 6px 14px;
font-size: 0.74rem;
font-family: var(--font-mono);
color: var(--text-muted);
display: flex;
justify-content: space-between;
align-items: center;
}
.code-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-primary);
font-weight: 500;
}
.copy-code-btn {
background: transparent;
border: var(--border-width) solid var(--border-color);
color: var(--text-muted);
padding: 2px 8px;
font-family: inherit;
font-size: 0.72rem;
cursor: pointer;
border-radius: var(--border-radius-sm);
}
.copy-code-btn:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
.code-content,
.code-block-container pre,
.markdown-prose pre {
padding: 14px 16px;
font-size: 0.82rem;
overflow-x: auto;
color: var(--text-primary);
line-height: 1.55;
margin: 18px 0;
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
font-family: var(--font-code);
}
/* Syntax Highlighting Tokens (Prism / 11ty Syntax Highlight) */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #5c6370;
font-style: italic;
}
.token.punctuation {
color: #abb2bf;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: var(--accent-amber);
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: var(--accent-green);
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: var(--accent-cyan);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #c678dd;
}
.token.function,
.token.class-name {
color: #61afef;
}
.token.regex,
.token.important,
.token.variable {
color: var(--accent-amber);
}
+39
View File
@@ -0,0 +1,39 @@
/* ==========================================================================
Footer Tile (components/footer.css)
========================================================================== */
.footer-tile {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 14px 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-mono);
font-size: 0.74rem;
color: var(--text-muted);
flex-wrap: wrap;
gap: 12px;
}
.footer-left {
display: flex;
align-items: center;
gap: 12px;
}
.footer-right {
display: flex;
gap: 16px;
align-items: center;
}
.footer-link {
color: var(--text-muted);
text-decoration: none;
}
.footer-link:hover {
color: var(--accent-cyan);
}
+63
View File
@@ -0,0 +1,63 @@
/* ==========================================================================
Global Status Header Bar (components/global-bar.css)
========================================================================== */
.global-bar {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 10px 18px;
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-mono);
font-size: 0.8rem;
flex-wrap: wrap;
gap: 10px;
}
.brand-group {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-primary);
font-weight: 700;
text-decoration: none;
}
.brand-group:hover {
color: var(--accent-cyan);
}
.status-indicator {
width: 8px;
height: 8px;
background-color: var(--accent-green);
display: inline-block;
}
.bar-nav {
display: flex;
gap: 14px;
align-items: center;
}
.bar-link {
color: var(--text-muted);
text-decoration: none;
}
.bar-link:hover {
color: var(--accent-cyan);
}
.bar-link.active {
color: var(--text-primary);
border-bottom: 1px solid var(--accent-cyan);
}
.bar-link.external::after {
content: " ↗";
font-size: 0.75em;
color: var(--text-muted);
}
+140
View File
@@ -0,0 +1,140 @@
/* ==========================================================================
Tile Containers & Hero Tile (components/tiles.css)
========================================================================== */
/* Standard Tile Container */
.tile {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 20px;
display: flex;
flex-direction: column;
gap: 14px;
}
.tile-header {
display: flex;
justify-content: space-between;
align-items: baseline;
border-bottom: var(--border-width) solid var(--border-color);
padding-bottom: 10px;
flex-wrap: wrap;
gap: 8px;
}
.tile-title {
font-family: var(--font-mono);
font-size: 0.92rem;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.tile-meta {
font-family: var(--font-mono);
font-size: 0.74rem;
color: var(--text-muted);
}
.tile-content {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Hero Lander Tile */
.hero-tile {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-left: 3px solid var(--accent-cyan);
border-radius: var(--border-radius);
padding: 24px 28px;
display: flex;
flex-direction: column;
gap: 12px;
}
.hero-pretitle {
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--accent-cyan);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.hero-main-title {
font-family: var(--font-mono);
font-size: 2.1rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.02em;
line-height: 1.2;
}
@media (max-width: 600px) {
.hero-main-title {
font-size: 1.6rem;
}
}
.hero-landing-text {
font-size: 0.98rem;
color: var(--text-primary);
line-height: 1.65;
max-width: 82ch;
}
/* Filter Pills Row in Hero Tile */
.filter-pills-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-top: 4px;
font-family: var(--font-mono);
font-size: 0.78rem;
}
.filter-tag-btn {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
color: var(--text-muted);
padding: 3px 8px;
border-radius: var(--border-radius-sm);
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.filter-tag-btn:hover {
border-color: var(--accent-cyan);
color: var(--text-primary);
background-color: var(--bg-elevated);
}
.filter-tag-btn.active {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
border-color: var(--accent-cyan);
font-weight: 700;
}
/* Directive Callout Box inside Hero */
.hero-directive-box {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 12px 16px;
font-family: var(--font-mono);
font-size: 0.82rem;
color: var(--text-muted);
line-height: 1.6;
}
.hero-directive-box strong {
color: var(--text-primary);
}
+53
View File
@@ -0,0 +1,53 @@
/* ==========================================================================
Tiling Window Manager Grids & Layout (layout.css)
========================================================================== */
/* Outer Shell Container */
.tiling-layout {
width: 100%;
max-width: var(--site-max-width);
display: flex;
flex-direction: column;
gap: var(--tile-gap);
}
/* Asymmetric 2-Column Split Grid (Lander: Lead Article + Telemetry Sidebar) */
.split-grid {
display: grid;
grid-template-columns: 1.25fr 0.75fr;
gap: var(--tile-gap);
align-items: start;
}
@media (max-width: 880px) {
.split-grid {
grid-template-columns: 1fr;
}
}
/* Article Workspace Grid (Left Sidebar TOC + Main Reading Workspace) */
.article-workspace-grid {
display: grid;
grid-template-columns: 290px 1fr;
gap: var(--tile-gap);
align-items: start;
}
@media (max-width: 960px) {
.article-workspace-grid {
grid-template-columns: 1fr;
}
}
/* Generic Symmetrical Grids */
.grid-2col {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--tile-gap);
}
@media (max-width: 768px) {
.grid-2col {
grid-template-columns: 1fr;
}
}
+21
View File
@@ -0,0 +1,21 @@
/* ==========================================================================
Main Stylesheet (main.css)
River Rooks Blog - Modular CSS Architecture
========================================================================== */
/* 1. Design Tokens & CSS Custom Properties */
@import "./tokens.css";
/* 2. Global Reset & Baseline Typography */
@import "./reset.css";
/* 3. Tiling Window Manager Grids */
@import "./layout.css";
/* 4. Component Styles */
@import "./components/global-bar.css";
@import "./components/tiles.css";
@import "./components/cards.css";
@import "./components/code-box.css";
@import "./components/article.css";
@import "./components/footer.css";
+52
View File
@@ -0,0 +1,52 @@
/* ==========================================================================
CSS Reset & Baseline (reset.css)
Strict adherence to developer-centric minimalism & instant transitions
========================================================================== */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
-webkit-text-size-adjust: 100%;
text-rendering: optimizeLegibility;
scroll-behavior: smooth;
}
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: var(--font-prose);
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 16px 48px 16px;
overflow-x: hidden;
}
/* Hard anti-pattern enforcement: instant transitions across UI elements */
a, button, input, select, textarea {
transition: none !important;
}
/* High-contrast keyboard accessibility outline */
:focus-visible {
outline: 2px solid var(--accent-cyan);
outline-offset: 2px;
}
img, svg, video {
max-width: 100%;
height: auto;
display: block;
}
/* Monospace font assignment for code & structural elements */
code, pre, kbd, samp {
font-family: var(--font-code);
}
+38
View File
@@ -0,0 +1,38 @@
/* ==========================================================================
Design Tokens (tokens.css)
River Rooks Blog - Native Dark Mode Architecture
========================================================================== */
:root {
/* Surface & Canvas Colors */
--bg-canvas: #0d0f12; /* Deep slate/charcoal canvas */
--bg-surface: #121417; /* Tile & card surface */
--bg-elevated: #1c1f26; /* Hovered surface */
/* Borders & Dividers */
--border-color: #2b303b; /* Crisp 1px border */
--border-width: 1px;
--border-radius: 4px; /* Strictly capped at 4px */
--border-radius-sm: 2px;
--border-radius-xs: 1px;
/* Typography */
--font-mono: 'IBM Plex Mono', 'JetBrains Mono', monospace;
--font-code: 'JetBrains Mono', 'Fira Code', monospace;
--font-prose: 'Inter', system-ui, -apple-system, sans-serif;
/* Text Colors */
--text-primary: #e0e6f0; /* Light off-white prose */
--text-muted: #8a95a5; /* Monospace metadata */
/* High-Signal Terminal Accents */
--accent-cyan: #00f0ff; /* Primary active accent */
--accent-green: #00e676; /* Success / online indicator */
--accent-amber: #ffb700; /* Warning / highlight accent */
--accent-red: #ff5252; /* Danger / caution accent */
/* Structural Dimensions */
--site-max-width: 1140px;
--tile-gap: 16px;
--prose-max-width: 68ch;
}
+137
View File
@@ -0,0 +1,137 @@
---
layout: layouts/base.njk
title: "River Rooks | Homelab Gateway"
---
<!-- Main Hero Landing Tile -->
<section class="hero-tile" aria-label="Gateway Declaration">
<div class="hero-pretitle">SYSTEM DECLARATION &bull; RACK-01 NODE GATEWAY</div>
<h1 class="hero-main-title">Engineering Sovereign Infrastructure &amp; Technical Notes</h1>
<div class="hero-landing-text">
<p>
My work centers on network forensics, self-hosted system architecture, low-level firmware exploitation, and zero-allocation Unix utilities.
</p>
<p>
This gateway functions as the frontend for my physical homelab cluster. Rather than hosting projects on opaque third-party clouds, everything here runs on bare metal in my personal server rack—orchestrated through Proxmox VE, partitioned across isolated VLANs via OPNsense, and protected by point-to-point WireGuard cryptographic meshes.
</p>
</div>
<div class="hero-directive-box">
<strong>PRIMARY DESTINATIONS:</strong>
<span style="color: var(--accent-cyan); margin-left: 8px;">[1] Technical Blog ({{ collections.posts.length }} Articles)</span> &bull;
<span>[2] Forgejo Git Repos</span> &bull;
<span>[3] Public Tools</span> &bull;
<span>[4] Cluster Telemetry</span>
</div>
</section>
<!-- Split Tiling Columns -->
<div class="split-grid">
<!-- Left Column: Featured Service (Blog) + Dynamic Recent Articles -->
<div style="display: flex; flex-direction: column; gap: var(--tile-gap);">
<section class="tile" aria-label="Featured Service: Technical Blog">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
// PRIMARY SERVICE: THE TECHNICAL BLOG
</div>
<span class="tile-meta">~/notes/ &bull; {{ collections.posts.length }} ARTICLES</span>
</header>
<div class="blog-service-feature">
<div class="blog-feature-top">
<span class="badge-core">[CORE PUBLICATION SERVICE]</span>
<span style="color: var(--text-muted);">{{ collections.posts[0].date | postDate }} LAST SYNC</span>
</div>
<a href="/" class="blog-feature-title">&gt; River Rooks Technical Notes &amp; Research</a>
<p class="blog-feature-desc">
In-depth security post-mortems, CTF write-ups, shell portability benchmarks, and bare-metal homelab architecture guides.
</p>
<div class="blog-sublinks">
{% for post in collections.posts | limit(4) %}
<a href="{{ post.url }}" class="blog-sublink-item">
<span>{{ loop.index }}. {{ post.data.title }}</span>
<span style="color: var(--accent-cyan);">[{{ post.templateContent | readingTime(post.data.readingTime) }}] &rarr;</span>
</a>
{% endfor %}
</div>
</div>
</section>
</div>
<!-- Right Column: Homelab Hosted Services & Node Telemetry -->
<div style="display: flex; flex-direction: column; gap: var(--tile-gap);">
<section class="tile" aria-label="Homelab Services Directory">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
// HOMELAB SERVICES ROUTER
</div>
<span class="tile-meta">4 PUBLIC TOOLS</span>
</header>
<div class="services-mini-grid">
<a href="https://git.rooks.dev" target="_blank" rel="noopener noreferrer" class="service-tile-item">
<div>
<div class="service-item-title">Forgejo Git</div>
<div class="service-item-tag">git.rooks.dev:2222</div>
</div>
<p class="service-item-desc">Self-hosted code repositories &amp; dotfiles.</p>
</a>
<a href="https://chef.rooks.dev" target="_blank" rel="noopener noreferrer" class="service-tile-item">
<div>
<div class="service-item-title">CyberChef</div>
<div class="service-item-tag">chef.rooks.dev:443</div>
</div>
<p class="service-item-desc">Hex decoding, cryptography &amp; forensics.</p>
</a>
<a href="https://pdf.rooks.dev" target="_blank" rel="noopener noreferrer" class="service-tile-item">
<div>
<div class="service-item-title">Stirling PDF</div>
<div class="service-item-tag">pdf.rooks.dev:443</div>
</div>
<p class="service-item-desc">Private document OCR and manipulation.</p>
</a>
<a href="https://status.rooks.dev" target="_blank" rel="noopener noreferrer" class="service-tile-item">
<div>
<div class="service-item-title">Uptime Kuma</div>
<div class="service-item-tag">status.rooks.dev:443</div>
</div>
<p class="service-item-desc">High-frequency ping &amp; latency monitors.</p>
</a>
</div>
</section>
<!-- Infrastructure Telemetry Tile -->
<section class="tile" aria-label="Infrastructure Nodes Telemetry">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
// CLUSTER TELEMETRY
</div>
<span class="tile-meta">3 NODES ONLINE</span>
</header>
<div style="display: flex; flex-direction: column; gap: 8px;">
<div class="node-stat-row">
<span class="node-name">RACK-01 (Proxmox Primary)</span>
<span class="node-load">ONLINE &bull; LOAD: 0.24</span>
</div>
<div class="node-stat-row">
<span class="node-name">NODE-02 (Storage TrueNAS)</span>
<span class="node-load">ONLINE &bull; POOL: 98% HEALTH</span>
</div>
<div class="node-stat-row">
<span class="node-name">OPN-01 (Routing Gateway)</span>
<span class="node-load">ONLINE &bull; WIREGUARD ACTIVE</span>
</div>
</div>
</section>
</div>
</div>
+39
View File
@@ -0,0 +1,39 @@
---
layout: layouts/base.njk
title: "River Rooks | Engineering Notes & Systems Architecture"
---
<!-- 1. Lander Hero Tile with Dynamic Filter Tag Buttons -->
{% include "partials/hero-tile.njk" %}
<!-- 2. Asymmetric Split Grid: Featured Lead Post (Left) & Telemetry Sidebar (Right) -->
<div class="split-grid">
<div class="split-lead-col">
{% include "partials/featured-card.njk" %}
</div>
<div class="split-sidebar-col">
{% include "partials/stats-sidebar.njk" %}
</div>
</div>
<!-- 3. Full-Width Chronological Dispatches Feed Tile -->
<section class="tile" aria-label="Dispatches Feed Catalog">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
RECENT DISPATCHES
</div>
<span class="tile-meta">ORDER: CHRONOLOGICAL DESC &bull; TOTAL: {{ collections.posts.length }}</span>
</header>
<div class="feed-list">
{% for post in collections.posts %}
{% include "partials/post-card.njk" %}
{% else %}
<p style="color: var(--text-muted); font-family: var(--font-mono); font-size: 0.85rem; padding: 16px;">
No published dispatches found in repository.
</p>
{% endfor %}
</div>
</section>
@@ -0,0 +1,77 @@
---
layout: layouts/post.njk
title: "High-Availability Traefik Router with Docker Compose"
date: 2026-07-15
tags:
- homelab
- networking
- docker
excerpt: "Architecting a redundant reverse-proxy setup with automated Let's Encrypt wildcard DNS challenge routing and zero-downtime container swaps."
readingTime: "12 MIN READ"
featured: false
difficulty: "ADVANCED"
---
## 1.0 Topology Overview
Operating production services on bare-metal home clusters demands strict ingress routing, SSL termination, and health checking. Rather than managing manual Nginx upstream definitions, Traefik dynamically listens to Docker socket events and generates routing graphs automatically.
```text
[Internet / WireGuard Mesh]
|
[Edge Router]
|
+--------+--------+
| |
[Traefik 01] [Traefik 02] (Keepalived VIP)
| |
+--------+--------+
|
[Internal Overlay]
|
+-------+-------+
| | |
[App1] [App2] [App3]
```
## 2.0 Docker Compose Infrastructure
The core proxy layer is configured with strict network isolation, healthchecks, and non-root execution:
```yaml
version: '3.8'
services:
traefik:
image: traefik:v3.1
container_name: edge_router
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- "80:80"
- "443:443"
environment:
- CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/traefik.yml:ro
- ./acme.json:/acme.json
networks:
- proxy_net
networks:
proxy_net:
external: true
```
## 3.0 Verification & Failover Testing
Run the health check endpoint using `curl` to verify response headers:
```sh
curl -Iv https://router.rooks.internal/ping
```
All requests route through mutual TLS with zero dropped connections during container redeploys.
@@ -0,0 +1,98 @@
---
layout: layouts/post.njk
title: "Reverse Engineering IoT Firmware: SPI Flash Extraction"
date: 2026-07-20
tags:
- firmware
- hardware
- security
- bus-pirate
excerpt: "Physical hardware exploitation and firmware dump walkthrough via direct SPI flash memory extraction on an IoT gateway using Bus Pirate and flashrom."
readingTime: "8 MIN READ"
featured: true
difficulty: "INTERMEDIATE"
---
## 1.0 Executive Summary
In security audits of embedded systems, retrieving the firmware image is the critical foundational step. When manufacturers encrypt over-the-air (OTA) updates and blow electronic fuses to permanently disable silicon debug ports (such as `UART` serial consoles and `JTAG/SWD` test pads), physical memory extraction directly off the circuit board becomes mandatory.
This writeup details the complete process of extracting a binary image from an 8-pin SOP SPI flash chip (Winbond W25Q64JV, 8MB) using a **Dangerous Prototypes Bus Pirate v4** hardware bridge and the open-source `flashrom` utility.
<div class="markdown-callout callout-note">
<div class="callout-title">[!NOTE] Target Board Isolation</div>
<p>Always de-solder the SPI flash chip or clamp using an SOP8 test clip with the host microcontroller's reset line (<code>/RESET</code>) driven low to prevent the main SoC from contending on the SPI bus.</p>
</div>
## 2.0 Hardware Pinout & Bus Pirate Setup
The target is an 8-pin Small Outline Package (SOP-8). Pin 1 is easily identified by the circular depression in the epoxy mold. Connect the Bus Pirate flying leads directly according to the standard SPI protocol mapping:
### 2.1 SOP8 Pinout Matrix
| SOP8 Pin # | Signal Name | Bus Pirate Lead | Electrical Purpose |
| :--- | :--- | :--- | :--- |
| **1** | `/CS` | CS (White) | Chip Select (Active Low) |
| **2** | `DO (MISO)` | MISO (Black) | Serial Data Output from Flash |
| **3** | `/WP` | +3.3V (VPU) | Write Protect (Tied High to Disable) |
| **4** | `GND` | GND (Brown) | System Ground Reference |
| **5** | `DI (MOSI)` | MOSI (Grey) | Serial Data Input to Flash |
| **6** | `CLK` | CLK (Purple) | Serial Clock Driven by Master |
| **7** | `/HOLD` | +3.3V (VPU) | Hold / Suspend Input (Tied High) |
| **8** | `VCC` | +3.3V (Red) | Supply Voltage (Nominal 3.3V DC) |
<div class="markdown-callout callout-warning">
<div class="callout-title">[!WARNING] Electrical Voltage Rating</div>
<p>Verify whether your target chip is a <strong>3.3V</strong> part (e.g., <code>W25Q64FV</code>) or a <strong>1.8V</strong> part (e.g., <code>W25Q64FW</code>). Supplying 3.3V logic to a 1.8V chip will permanently destroy the silicon junction gates.</p>
</div>
## 3.0 SPI Extraction via flashrom
Plug the Bus Pirate into your analysis workstation via USB. It registers as `/dev/ttyUSB0`. Query the chip ID register using the `0x9F` JEDEC Read ID command:
```sh
#!/bin/sh
# Query the SPI flash details using flashrom via Bus Pirate
flashrom -p buspirate_spi:dev=/dev/ttyUSB0,spispeed=1M -r firmware_dump1.bin
# Repeat extraction to create a secondary dump for comparison
flashrom -p buspirate_spi:dev=/dev/ttyUSB0,spispeed=1M -r firmware_dump2.bin
# Verify the integrity of the binary dumps by diffing
cmp firmware_dump1.bin firmware_dump2.bin
if [ $? -eq 0 ]; then
echo "[+] SPI Dumps match identically! Proceeding to hashing..."
md5sum firmware_dump1.bin
sha256sum firmware_dump1.bin
else
echo "[-] Bit flip or voltage sag detected! Re-run extraction." >&2
exit 1
fi
```
Press <kbd>Ctrl</kbd> + <kbd>C</kbd> if the bus fails to synchronize. Once complete, inspect the initial bytes with `hexdump`:
```text
0000000: 2705 1956 7062 8251 60a4 2130 0041 5262 '..Vpb.Q`.!0.ARb
0000010: 6873 7173 0000 0004 0000 0000 0000 1000 hsqs............
0000020: 0002 0001 0000 0000 636f 6e66 6967 2e64 ........config.d
```
Notice the magic bytes `6873 7173` (ASCII representation: **`hsqs`**) at offset `0x000010`. This confirms the presence of a SquashFS compressed root filesystem!
## 4.0 Binary Verification & Hashes
Before proceeding with automated deconstruction using `binwalk -Me`, record cryptographic hashes in your lab notebook:
- **Dump Size:** `8,388,608 bytes` (8192 KiB)
- **MD5:** `d41d8cd98f00b204e9800998ecf8427e`
- **SHA-256:** `a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e`
## 5.0 Audit Checklist
Ensure all forensics hygiene steps have been recorded:
- [x] Visual optical inspection and pinout diagram documented
- [x] Clean dual flashrom dumps matching via bitwise `cmp`
- [x] SquashFS magic header identified at offset `0x10`
- [ ] Filesystem extraction and hardcoded credentials analysis (Part 2)