<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://mamercad.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mamercad.github.io/" rel="alternate" type="text/html" /><updated>2026-08-09T00:14:06+00:00</updated><id>https://mamercad.github.io/feed.xml</id><title type="html">Cloudmason Notes</title><subtitle>Field notes for infrastructure, homelab routes, keys, services, and the small operational rituals that keep systems reachable.</subtitle><entry><title type="html"></title><link href="https://mamercad.github.io/2026/08/09/2026-04-25-agentic-second-brain-primer.html" rel="alternate" type="text/html" title="" /><published>2026-08-09T00:14:06+00:00</published><updated>2026-08-09T00:14:06+00:00</updated><id>https://mamercad.github.io/2026/08/09/2026-04-25-agentic-second-brain-primer</id><content type="html" xml:base="https://mamercad.github.io/2026/08/09/2026-04-25-agentic-second-brain-primer.html"><![CDATA[<p>The phrase <em>agentic second brain</em> can sound like marketing, but the underlying idea is simple. It is a <strong>local-first</strong> place—files, notes, and tools on hardware you control—where both you and automated agents can <strong>remember</strong> what you decided, <strong>act</strong> in the workspace, <strong>inspect</strong> what actually happened, and <strong>improve</strong> the next run. It is <em>not</em> the same as “saving more chat history.” A transcript is raw signal; a second brain is the structured layer you keep on purpose.</p>

<p>If you do not keep the parts distinct, they collapse into a single blob: everything is “the assistant said it,” and nothing is a durable artifact. The sections below are a calm way to split the work so you can name what you are storing and why.</p>

<h2 id="memory">Memory</h2>

<p>Memory is the layer of <strong>facts, preferences, and decisions</strong> you expect to care about more than once. A good memory entry is small enough to reread in a minute and specific enough to act on. It should be <strong>traceable</strong>—a link to a ticket, a commit, a doc, a dated log entry—so the entry is not only a vibe from last Tuesday. If you cannot point to a source, treat the entry as a hypothesis until you can.</p>

<h2 id="skills-and-runbooks">Skills and runbooks</h2>

<p>Skills, playbooks, and runbooks are <strong>procedures that worked in the real environment</strong>, not idealized checklists. When you write one, include the exact sequence you would hand to a careful colleague: prerequisites, the commands or UI steps, the checks that mean “success,” and a short note on when <strong>not</strong> to use this path (for example, when it would trample a production setting). A skill is not “AI wisdom”; it is <em>repeatable</em> work you have already paid for once with attention.</p>

<h2 id="sessions-and-outcomes">Sessions and outcomes</h2>

<p>Sessions are the record of <em>what you tried</em> and <em>what changed</em>. The useful form is not a dump of the whole conversation, but a <strong>searchable outcome</strong>: what was the task, what did you change in the repo or the system, what failed, and what you would do differently. Future you, and any agent, can scan outcomes far faster than they can re-derive context from a hundred turns of back-and-forth. When something ships, the outcome should point to <strong>concrete artifacts</strong>: a branch, a diff, a test run, a PR, a URL, a log line.</p>

<h2 id="workspace-and-shipping">Workspace and shipping</h2>

<p>The workspace is where <strong>truth lives in files</strong>: branches, config, test output, build artifacts, dashboards. A second brain that never touches the workspace is just notes. A healthy setup makes the path from “we decided this” to “it is in the tree or on the service” short and visible. If the only record of a change is a message in a thread, you have not really stored it; you have buried it in noise.</p>

<h2 id="runtime-and-security">Runtime and security</h2>

<p><strong>Runtime</strong> is the machinery that runs models, tools, cron jobs, subagents, and gateways. It is easy to treat remote access to your machines or repos as a convenience; it is better to treat it as a <strong>security design</strong> problem. Anything that can mutate files, run shells, or reach your network from outside should have an obvious <strong>audit surface</strong>: what ran, on whose behalf, with what permissions, and what the next human should verify before merging or deploying. Hidden authority is the failure mode where things change faster than the notes can keep up.</p>

<h2 id="what-usually-goes-wrong">What usually goes wrong</h2>

<p><strong>Memory bloat</strong> happens when you save every intermediate thought but never compress it into a small durable fact or decision. The fix is to prefer short pages and occasional deletion over infinite append-only capture.</p>

<p><strong>Hidden authority</strong> happens when jobs or agents change systems without leaving a visible state change you can review. The fix is to make activity and approvals inspectable, not to ban automation.</p>

<p><strong>Context laundering</strong> happens when private or operational details drift into “general” pages because it was fast to paste them there. The fix is a one-line <strong>privacy</strong> or <strong>scope</strong> label at the top of mixed pages, and a habit of moving sensitive detail to a restricted place.</p>

<p><strong>Dashboard sprawl</strong> happens when you add another pane every time you feel busy, but never answer “what must I look at <em>today</em>?” The fix is fewer surfaces with clearer meaning, not more tiles.</p>

<h2 id="a-simple-default-to-adopt-first">A simple default to adopt first</h2>

<p>If you do nothing else, keep <strong>public-source material</strong> and <strong>private or operational material</strong> in separate places or with explicit headers. When you promote something from “rough note” to “we rely on this,” add <strong>one</strong> link or pointer that answers <em>why you believe it</em>. The next person—human or software—can then choose to trust, verify, or replace that claim on purpose, instead of guessing from tone.</p>]]></content><author><name></name></author></entry><entry><title type="html"></title><link href="https://mamercad.github.io/2026/08/09/2026-04-25-autoresearch-toml-baseline-run.html" rel="alternate" type="text/html" title="" /><published>2026-08-09T00:14:06+00:00</published><updated>2026-08-09T00:14:06+00:00</updated><id>https://mamercad.github.io/2026/08/09/2026-04-25-autoresearch-toml-baseline-run</id><content type="html" xml:base="https://mamercad.github.io/2026/08/09/2026-04-25-autoresearch-toml-baseline-run.html"><![CDATA[<p>People hear “experiment” and picture GPUs and training jobs, but a lot of software work has the same shape: you have a <em>candidate change</em>, a <em>way to measure it</em>, and a <em>repeated procedure</em> to keep comparisons fair. A <strong>local-first</strong> experiment runner is one way to make that contract explicit. You write a spec (often a small <strong>TOML</strong> file) that says what to run, what paths the run may change, and how success is read from a metrics file. You run a <strong>baseline</strong> or a batch of <strong>repeats</strong>, store the noisy details under a directory you <strong>do not commit</strong>, and only promote a winning change through a normal <strong>pull request</strong> and human review. The runner does not replace judgment; it makes “what we compared” and “how we knew” less ambiguous.</p>

<h2 id="what-you-are-setting-up-step-by-step">What you are setting up, step by step</h2>

<p><strong>First, describe the experiment in the spec file.</strong> The spec should name the command that performs evaluation—often a script or a small program—and the list of <strong>mutable</strong> paths the experiment is allowed to touch, alongside <strong>protected</strong> paths that must not change. If a run reaches outside its allowed set, the harness should <strong>fail early</strong> with a clear message, not halfway through a silent edit.</p>

<p><strong>Second, run the eval with a run id and capture structured metrics.</strong> The evaluation command is responsible for writing JSON (or a format your tool understands) to a path the runner provides, for example through an environment variable such as <code class="language-plaintext highlighter-rouge">AUTORESEARCH_METRICS_FILE</code>. The spec names which metric field matters, and whether <em>higher</em> or <em>lower</em> is better. That is how “better” becomes a file on disk, not a feeling in the console.</p>

<p><strong>Third, keep detailed run output local.</strong> A typical choice is a directory like <code class="language-plaintext highlighter-rouge">.autoresearch/runs/&lt;run-id&gt;/</code> containing per-attempt data, optional stdout summaries, and anything else you need to debug. Treat this tree like developer scratch space: it may contain local paths, prompts, or half-formed ideas, so <strong>default policy is to keep it out of version control</strong> unless you have a defined export and redaction path.</p>

<p><strong>Fourth, use CI to validate the <em>harness</em>, not to auto-merge winners.</strong> A good CI job checks that specs validate, that tests pass, and that a known example can run from a <strong>clean</strong> checkout. It does <em>not</em> need to open pull requests for you. Candidate improvements still flow through branches and review like any other change.</p>

<h2 id="reading-an-experiment-block-in-plain-language">Reading an experiment block in plain language</h2>

<p>In an <code class="language-plaintext highlighter-rouge">[experiment]</code> section, <code class="language-plaintext highlighter-rouge">eval_command</code> is the list of program and arguments that must run to completion. The <code class="language-plaintext highlighter-rouge">metric</code> and <code class="language-plaintext highlighter-rouge">direction</code> fields tell the runner which number in the metrics JSON matters and which direction is “improvement.” The <code class="language-plaintext highlighter-rouge">mutable_paths</code> and <code class="language-plaintext highlighter-rouge">protected_paths</code> fields define the contract between “this experiment is allowed to edit the repo in these places” and “these areas are off limits.”</p>

<p><code class="language-plaintext highlighter-rouge">timeout_seconds</code> and <code class="language-plaintext highlighter-rouge">repeats</code> exist so that a hung process or a flaky environment shows up in the <strong>summary</strong> the same way a bad score does, instead of leaving you to infer failure from a silent shell. The point is to make the failure modes <strong>boring to compare</strong> across machines, not to look dramatic in the terminal.</p>

<h2 id="why-the-runner-should-be-strict-about-bad-outcomes">Why the runner should be strict about bad outcomes</h2>

<p>A useful harness treats a <strong>crash</strong>, a <strong>timeout</strong>, <strong>malformed metrics JSON</strong>, and a <strong>missing metrics file</strong> as <em>named states</em>, not as a single generic “it failed.” When those states are first-class, you can sort runs the way you would sort test results: you know whether you are looking at a logic bug, a network flake, a permissions mistake, or a true regression in the metric. Soft failures that collapse into one exit code are hard to learn from; explicit states are the difference between a one-off and a pattern.</p>

<h2 id="privacy-and-the-local-runs-directory">Privacy and the local runs directory</h2>

<p>Assume run logs and scratch directories can contain <strong>prompts, absolute paths, environment names, and ideas</strong> you are not ready to share. The calm default is: keep <code class="language-plaintext highlighter-rouge">.autoresearch/</code> (or your equivalent) in <strong><code class="language-plaintext highlighter-rouge">.gitignore</code></strong>, and if you need to show a result to someone else, copy out a <em>reduced</em> summary you are willing to stand behind. The workflow is the same as any other local diagnostic output: the machine is allowed to be messy; the repository should stay legible to strangers.</p>

<hr />

<p><em>The commands and field names in your own project may differ; keep the same separation between spec, metrics, and raw logs.</em></p>]]></content><author><name></name></author></entry><entry><title type="html">Traceability and privacy in mixed knowledge bases</title><link href="https://mamercad.github.io/security/notes/documentation/agents/2026/04/25/traceability-privacy-mixed-knowledge-bases.html" rel="alternate" type="text/html" title="Traceability and privacy in mixed knowledge bases" /><published>2026-04-25T18:00:00+00:00</published><updated>2026-04-25T18:00:00+00:00</updated><id>https://mamercad.github.io/security/notes/documentation/agents/2026/04/25/traceability-privacy-mixed-knowledge-bases</id><content type="html" xml:base="https://mamercad.github.io/security/notes/documentation/agents/2026/04/25/traceability-privacy-mixed-knowledge-bases.html"><![CDATA[<p>If you work with code, wikis, and operational runbooks at the same time—especially with agents in the loop—you are not maintaining a single kind of document. You are maintaining a <strong>mixed</strong> knowledge base. Some pages are public or shareable, some are private, some are quick captures of “how the system looks today,” and some are early drafts that were never revisited. <strong>Trust</strong> and <strong>safety</strong> in that world do not come from a perfect taxonomy on day one. They come from a few steady habits you can follow without turning writing into a ceremony.</p>

<h2 id="traceability-or-how-a-reader-knows-what-to-believe">Traceability, or how a reader knows what to believe</h2>

<p>A strong claim—about architecture, about an outage, about a policy—ought to sit next to a <strong>source</strong> a reader can open. That source might be a file path in your <code class="language-plaintext highlighter-rouge">sources/</code> tree, a link to a ticket, a public URL, a commit hash, or a dated private note in a place that is allowed to exist. The question you are answering is not “is this true forever?” The question is “<em>why</em> did we think this, on what day, and from what ground?”</p>

<p>When you <strong>cannot</strong> verify a statement quickly in a maintenance pass, the calm move is to mark the page as <strong>stale</strong>, or to add a short “Open questions” section that says what is uncertain. Two pages that both read as the <strong>current</strong> truth but disagree quietly are more dangerous than one page that admits a gap. An honest <em>unknown</em> gives the next reader permission to re-check; two silent “current” statements create accidents.</p>

<h2 id="privacy-and-what-must-not-become-invisible-background">Privacy, and what must not become invisible background</h2>

<p>If a page might combine <strong>broad, shareable</strong> explanation with <strong>specific</strong> hostnames, internal URLs, family context, or credentials—even in mild form—say so at the top in a few sentences. A <strong>Privacy note</strong> does not have to be legal language. It is enough to state the kinds of information the page may contain, who it is meant for, and whether it is safe to copy into a public issue or a client-facing document.</p>

<p>Pay attention to <strong>context laundering</strong>, which is a slow form of over-sharing, not a dramatic leak. A small operational fact slips into a “general” page because you were in a hurry one evening. A month later, that page is the one everyone links. The fact has become ambient truth in the wrong place. The defense is a light habit: when you add detail that is not in scope for the page’s original audience, <strong>move</strong> it to a restricted note or add scope right then, not “later when we clean up.”</p>

<h2 id="operator-hygiene-or-how-maintenance-stays-small">Operator hygiene, or how maintenance stays small</h2>

<p>Large annual reviews rarely happen on schedule, and they are painful when they do. A more sustainable rhythm is a <strong>rotating</strong> scope: one project page, one pattern, or one source map in a short session, on whatever cadence you can keep—weekly, biweekly, or monthly. The pass is the same in spirit: read for accuracy, add links, soften claims you cannot support, and align contradictions. Small passes that finish <strong>compound</strong>; sweeping passes that stall do not.</p>

<p>Keep a <strong>single</strong> maintenance log, changelog, or append-only file where you record what you touched, what you changed, and what you postponed. The log entry does not need to be long. It should be enough that someone (including you) can tell <em>when</em> a page was last <em>intentionally</em> read, and what you decided to leave for another day. The goal is not perfect documentation. The goal is <strong>reversible, inspectable, and honest</strong> state, so the next person—human or agent—does not have to reconstruct reality from a chat transcript or a hope that a timestamp was right.</p>

<h2 id="what-you-get-in-return">What you get in return</h2>

<p>When traceability and scope are in good shape, a mixed knowledge base stops feeling like a junk drawer. It feels like a <strong>workshop</strong>: tools are where you expect, sharp edges are labeled, and the next action is clear enough that you are not always starting from zero. That outcome is not flashy, but it is the kind of calm infrastructure that makes hard work a little more predictable over months and years.</p>]]></content><author><name></name></author><category term="security" /><category term="notes" /><category term="documentation" /><category term="agents" /><summary type="html"><![CDATA[If you work with code, wikis, and operational runbooks at the same time—especially with agents in the loop—you are not maintaining a single kind of document. You are maintaining a mixed knowledge base. Some pages are public or shareable, some are private, some are quick captures of “how the system looks today,” and some are early drafts that were never revisited. Trust and safety in that world do not come from a perfect taxonomy on day one. They come from a few steady habits you can follow without turning writing into a ceremony.]]></summary></entry><entry><title type="html">Agent multiplexing—one control plane, many workers</title><link href="https://mamercad.github.io/agents/terminal/tmux/workflow/2026/04/25/agent-multiplexing-control-plane.html" rel="alternate" type="text/html" title="Agent multiplexing—one control plane, many workers" /><published>2026-04-25T16:45:00+00:00</published><updated>2026-04-25T16:45:00+00:00</updated><id>https://mamercad.github.io/agents/terminal/tmux/workflow/2026/04/25/agent-multiplexing-control-plane</id><content type="html" xml:base="https://mamercad.github.io/agents/terminal/tmux/workflow/2026/04/25/agent-multiplexing-control-plane.html"><![CDATA[<p>Sometimes the work in front of you is not one long thought in a single chat. It is a set of <strong>independent</strong> tasks, each of which is large enough that you would happily give it its own <strong>CLI agent</strong> or automation session, but small enough that you do not need a new project for each. In that situation, <em>multiplexing</em> is a way to run many workers under <strong>one</strong> mental model: a control plane for starting sessions, reading status, and recovering when a worker dies, instead of you personally babysitting a grid of ad hoc terminals. The point is not to make one “bigger” brain in one window; the point is to make <strong>parallel work</strong> and <strong>observability</strong> first-class so you are not the bottleneck for every attach and detach.</p>

<h2 id="what-the-pattern-looks-like-piece-by-piece">What the pattern looks like, piece by piece</h2>

<p><strong>Durable process containers</strong> are the bedrock. In many setups that means <strong>tmux</strong> sessions, windows, or panes, because they survive SSH drops, let you step away, and let you reattach with context intact. A worker is not a fragile foreground shell you must keep open; it is a named place where a process is expected to live for a while.</p>

<p><strong>Isolation</strong> means one worker, one home in that substrate. If two agents share one directory and one git state without rules, you will get accidental interference. A common step up is a <strong>separate worktree</strong> or workspace per task so that branches, uncommitted work, and build artifacts do not tangle. You are not trying to be fancy; you are trying to make “who owns this tree” obvious.</p>

<p><strong>Coordination</strong> is how you avoid plan drift when multiple sessions run at once. A <strong>task board</strong> or a shared message channel—lightweight, even a text file with locks—is where “claimed,” “in progress,” and “blocked” should live, so it does not exist only in one transcript. The human operator, and any orchestrator, should be able to answer “what is running?” without opening every session.</p>

<p><strong>Observability</strong> is the ability to see health without N separate attach sequences. A simple <strong>dashboard</strong> or a tail on structured logs, even if it is minimal, is enough to know which workers are still alive, which are stuck, and which finished with a non-zero exit. The goal is to reduce surprise when you return after an hour.</p>

<p><strong>Recovery</strong> is the honest admission that processes crash, networks flap, and agents loop. A <strong>watchdog</strong> or a periodic health check can restart a worker, but the dangerous version is a blind restart that replays a destructive or stale action. Good recovery is tied to <strong>task state</strong> you trust: a checkpoint, a branch name, a commit hash, an explicit “do not re-run if already done” flag. The operator should be able to read why a worker restarted, not just that it did.</p>

<h2 id="when-the-extra-structure-earns-its-keep">When the extra structure earns its keep</h2>

<p>If you are making a small, safe edit in a repo you know well, you do not need a multiplexing stack. The overhead pays for itself when <strong>parallelism</strong> really reduces wall-clock time, when <strong>resumability</strong> matters because sessions are long, and when you need <strong>visibility</strong> because more than one person or system is touching the work. In those cases, a little ceremony up front is cheaper than hours lost to “which terminal was that?” and half-merged state.</p>

<h2 id="risks-to-plan-for-in-calm-terms">Risks to plan for, in calm terms</h2>

<p><strong>Drift</strong> is when the task board says <em>done</em> but the git worktree is dirty, or the branch never merged, or the fix only lived in a local patch. The remedy is a tight definition of <em>done</em> that points at the tree: pushed branch, clean status, or an explicit “blocked on X” with a name.</p>

<p><strong>Log leakage</strong> is a quiet problem. Shared boards, shared log files, and shared HTTP endpoints for status can end up with <strong>local paths, prompt text, or secrets</strong> if the tools are chatty. Treat those surfaces the way you would treat app logs: scope them, redact by default, and do not let long-lived storage fill with content nobody owns.</p>

<p><strong>Dumb retries</strong> are automatic restarts that run the same bad command again because the <em>task</em> record never advanced. The remedy is to connect recovery to <strong>sanitized, explicit</strong> state, not to hope the second attempt is luckier. If a step is destructive, the task record should make that step <strong>idempotent</strong> or <strong>gated</strong> by a human.</p>

<p>You do not need a particular brand of tool to apply the pattern. You need a <strong>task contract</strong> that everyone—the operator, the agents, and the future reader of the logs—can follow without improvising. Once that exists, the control plane is simply the place where those contracts are visible.</p>

<hr />

<p><em>The names in your own stack (tmux, custom TUIs, APIs) will differ; the structure above is the part that carries between setups.</em></p>]]></content><author><name></name></author><category term="agents" /><category term="terminal" /><category term="tmux" /><category term="workflow" /><summary type="html"><![CDATA[Sometimes the work in front of you is not one long thought in a single chat. It is a set of independent tasks, each of which is large enough that you would happily give it its own CLI agent or automation session, but small enough that you do not need a new project for each. In that situation, multiplexing is a way to run many workers under one mental model: a control plane for starting sessions, reading status, and recovering when a worker dies, instead of you personally babysitting a grid of ad hoc terminals. The point is not to make one “bigger” brain in one window; the point is to make parallel work and observability first-class so you are not the bottleneck for every attach and detach.]]></summary></entry><entry><title type="html">A weekly “lint” for your second brain</title><link href="https://mamercad.github.io/notes/second-brain/knowledge-management/2026/04/25/karpathy-weekly-brain-lint.html" rel="alternate" type="text/html" title="A weekly “lint” for your second brain" /><published>2026-04-25T13:00:00+00:00</published><updated>2026-04-25T13:00:00+00:00</updated><id>https://mamercad.github.io/notes/second-brain/knowledge-management/2026/04/25/karpathy-weekly-brain-lint</id><content type="html" xml:base="https://mamercad.github.io/notes/second-brain/knowledge-management/2026/04/25/karpathy-weekly-brain-lint.html"><![CDATA[<p>A personal or team wiki does not stay healthy by accident. If you only add pages and never revisit them, you get the same failure mode as a repository full of unreviewed first drafts: the pile grows, trust erodes, and nobody knows which line is still true. A useful way to think about maintenance is the loop people sometimes call a “Karpathy-style” wiki: you ingest raw material, you synthesize it into pages you can navigate, you lint those pages on a schedule, and you log what you did. None of the steps are exotic; the point is to do them in a small, repeated rhythm instead of a heroic annual cleanup.</p>

<h2 id="how-the-loop-works-in-order">How the loop works, in order</h2>

<p><strong>Ingestion</strong> is where you park raw or semi-raw input. A dedicated <code class="language-plaintext highlighter-rouge">sources/</code> tree (or whatever naming you prefer) is enough. When you add something, name its scope: was it copied from a public document, a ticket, a private note, a conversation, or a command transcript? If you can add a date, add it. You are not trying to be formal; you are trying to make future you honest about <em>where</em> a claim entered the system.</p>

<p><strong>Synthesis</strong> is where you turn that material into pages that are meant to be read: project notes, patterns, runbooks, decisions. A synthesis page is allowed to be short, but it should not present a strong or operational claim as if it were obvious unless that claim is tied to something a reader can follow: a path under <code class="language-plaintext highlighter-rouge">sources/</code>, a public URL, or a private note you label explicitly. The habit you are building is traceability, not perfect prose.</p>

<p><strong>Linting</strong> is a scheduled pass. Weekly is a reasonable default; the duration can be as little as fifteen minutes if you keep the scope small. A lint is not a rewrite. Treat it the way you would a code review: you read, you add status labels, you fix or soften broken links, you flag contradictions, and you check that public-facing pages are not quietly accumulating private detail. You are not trying to finish the vault in one sitting.</p>

<p><strong>Logging</strong> is the final step, and it matters more than it sounds. In a line or a short paragraph, record what you opened, what you changed, and what you explicitly postponed. If you deferred something, add a single line explaining why—no shame, no essay. The log is your audit trail; it is how the system stays accountable to itself.</p>

<h2 id="what-lint-means-in-practice">What “lint” means in practice</h2>

<p>When a page <em>sounds</em> authoritative, a reader should be able to see <em>why</em> you believe it. If the page cannot point to a source, either soften the language, mark the claim as uncertain, or add the missing reference.</p>

<p>For privacy, a page that is meant to be broadly shareable should not slowly absorb home-lab hostnames, internal URLs, or one-off tokens just because it was faster to paste them there. If the page is mixed, say so in a short note at the top: what kind of information it may contain, and for whom it is written.</p>

<p>For contradictions, do not let two “current” pages give different default truths about the same system. You can update the one that is wrong, you can narrow each page so the scopes no longer overlap, or you can write an “Open questions” block that names the conflict and points to both pages. Ambiguity in the open is better than a silent war between documents.</p>

<p>For decay, anything that described “how things are <em>right now</em>” a year ago should either be dated, marked stale, or updated. Time-sensitive “current state” is the kind of content that causes the most damage when it is never revisited, because it still reads with full confidence.</p>

<h2 id="a-calm-default-one-small-scope">A calm default: one small scope</h2>

<p>When you are short on time, pick exactly one of the following: a project page you will actually use this week, a pattern you rely on when making decisions, or a source map you suspect is going stale. At the top of the page, set a status line if you do not have one: <code class="language-plaintext highlighter-rouge">Status: first-pass</code> for a shallow or unverified first draft, <code class="language-plaintext highlighter-rouge">Status: current</code> for something you still endorse after this pass, and <code class="language-plaintext highlighter-rouge">Status: stale</code> for anything you would not act on without re-checking. Add one entry to your maintenance log. A narrow pass you finish beats a wide pass you never start.</p>

<h2 id="why-a-schedule-exists">Why a schedule exists</h2>

<p>The failure mode to avoid is <em>ingestion without digestion</em>: a vault that only grows, where first-pass pages sit silently in search results as if they were reviewed truth. A weekly or biweekly lint is the opposite of a dramatic overhaul. It is a small, boring habit that keeps the system compounding, the same way short, regular review keeps a code base from rotting in silence.</p>]]></content><author><name></name></author><category term="notes" /><category term="second-brain" /><category term="knowledge-management" /><summary type="html"><![CDATA[A personal or team wiki does not stay healthy by accident. If you only add pages and never revisit them, you get the same failure mode as a repository full of unreviewed first drafts: the pile grows, trust erodes, and nobody knows which line is still true. A useful way to think about maintenance is the loop people sometimes call a “Karpathy-style” wiki: you ingest raw material, you synthesize it into pages you can navigate, you lint those pages on a schedule, and you log what you did. None of the steps are exotic; the point is to do them in a small, repeated rhythm instead of a heroic annual cleanup.]]></summary></entry><entry><title type="html">Too many passwords</title><link href="https://mamercad.github.io/1password/lastpass/authy/2fa/yubikey/2023/02/05/too-many-passwords.html" rel="alternate" type="text/html" title="Too many passwords" /><published>2023-02-05T10:32:43+00:00</published><updated>2023-02-05T10:32:43+00:00</updated><id>https://mamercad.github.io/1password/lastpass/authy/2fa/yubikey/2023/02/05/too-many-passwords</id><content type="html" xml:base="https://mamercad.github.io/1password/lastpass/authy/2fa/yubikey/2023/02/05/too-many-passwords.html"><![CDATA[<p>Just documenting current password strategy.
Phasing out LastPass in favor of 1Password (I’ve already import into 1Password, but haven’t pulled the “delete” trigger yet).
Both are set up with MFA at Authy (with cloud backup and password) and a YubiKey; kind of nice that they have a desktop application.
Try and keep Authy as only a “top-level” 2FA, and put the rest of the 2FAs into 1Password.
Authy is tied to a phone number, so, have to hang on to that.
It’d be nice if multiple physical tokens could be added to 1Password (to physically vault one).</p>]]></content><author><name></name></author><category term="1password" /><category term="lastpass" /><category term="authy" /><category term="2fa" /><category term="yubikey" /><summary type="html"><![CDATA[Just documenting current password strategy. Phasing out LastPass in favor of 1Password (I’ve already import into 1Password, but haven’t pulled the “delete” trigger yet). Both are set up with MFA at Authy (with cloud backup and password) and a YubiKey; kind of nice that they have a desktop application. Try and keep Authy as only a “top-level” 2FA, and put the rest of the 2FAs into 1Password. Authy is tied to a phone number, so, have to hang on to that. It’d be nice if multiple physical tokens could be added to 1Password (to physically vault one).]]></summary></entry><entry><title type="html">Just 1Password things</title><link href="https://mamercad.github.io/1password/1p/connect/kubernetes/secrets/2023/01/23/just-1password-things.html" rel="alternate" type="text/html" title="Just 1Password things" /><published>2023-01-23T11:46:33+00:00</published><updated>2023-01-23T11:46:33+00:00</updated><id>https://mamercad.github.io/1password/1p/connect/kubernetes/secrets/2023/01/23/just-1password-things</id><content type="html" xml:base="https://mamercad.github.io/1password/1p/connect/kubernetes/secrets/2023/01/23/just-1password-things.html"><![CDATA[<p>It took me a while to get <a href="https://developer.1password.com/docs/connect">Connect server</a> going with Kubernetes, here are my notes.
I haven’t gone very deep on this, I’m just starting with <a href="https://1password.com/">1Password</a> in earnest (beyond the simple use cases).</p>

<p>The first step is to create the connect server; I’m guessing that this is basically an API gateway to your stuff in their cloud.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ op connect server create Kubernetes <span class="nt">--vaults</span> Kubernetes,Homelab <span class="nt">--cache</span>
Set up a Connect server.
UUID: REDACTED
Credentials file: /path/to/where/you/ran/this/1password-credentials.json
</code></pre></div></div>

<p>If you want to see your servers:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ op connect server list
ID                            NAME          STATE
REDACTED                      Kubernetes    ACTIVE
</code></pre></div></div>

<p>The next step is to create an access token for Kubernetes, I’m intentially giving it a short expiration for this example.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ <span class="nb">export </span><span class="nv">CONNECT_TOKEN</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>op connect token create Kubernetes <span class="nt">--server</span> Kubernetes <span class="nt">--vault</span> Kubernetes,rw <span class="nt">--vault</span> Homelab,r <span class="nt">--expires-in</span><span class="o">=</span>24h<span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p>Next, set up the <a href="https://helm.sh/">Helm</a> things:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ helm repo add 1password https://1password.github.io/connect-helm-charts/
❯ helm repo update
</code></pre></div></div>

<p>Lastly, deploy Connect and its operator, I’m keeping everything in the <code class="language-plaintext highlighter-rouge">1password</code> namespace for now.
Be mindful of the path to <code class="language-plaintext highlighter-rouge">1password-credentials.json</code>.
The end result of all of this is that you get syncing (and caching) of your 1Password secrets to your Kubernetes secrets.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ helm <span class="nb">install </span>connect 1password/connect <span class="se">\</span>
  <span class="nt">--namespace</span> <span class="s2">"1password"</span> <span class="nt">--create-namespace</span> <span class="se">\</span>
  <span class="nt">--set-file</span> connect.credentials<span class="o">=</span><span class="s2">"1password-credentials.json"</span> <span class="se">\</span>
  <span class="nt">--set</span> operator.create<span class="o">=</span><span class="s2">"true"</span> <span class="se">\</span>
  <span class="nt">--set</span> operator.token.value<span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">CONNECT_TOKEN</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">--set</span> operator.watchNamespace<span class="o">=</span><span class="s2">"{1password}"</span>
</code></pre></div></div>

<p>I’ve created a test secret in 1Password:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ op item get <span class="nt">--vault</span> Kubernetes test1
ID:          REDACTED
Title:       test1
Vault:       Kubernetes <span class="o">(</span>REDACTED<span class="o">)</span>
Created:     2 hours ago
Updated:     2 hours ago by Mark Mercado
Favorite:    <span class="nb">false
</span>Version:     1
Category:    LOGIN
Fields:
  username:    foo
  password:    bar
</code></pre></div></div>

<p>Let’s define this secret in Kubernetes, note the <code class="language-plaintext highlighter-rouge">itemPath</code> structure (the <code class="language-plaintext highlighter-rouge">vaults</code> and <code class="language-plaintext highlighter-rouge">item</code> path segments are fixed).
Remember that I’ve got the operator configured to only watch the <code class="language-plaintext highlighter-rouge">1password</code> namespace right now.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">❯ cat secrets.yaml</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">onepassword.com/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">OnePasswordItem</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">test1</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">1password</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">itemPath</span><span class="pi">:</span> <span class="s">vaults/Kubernetes/items/test1</span>
</code></pre></div></div>

<p>Go ahead an deploy this manifest with <code class="language-plaintext highlighter-rouge">kubectl apply -f secrets.yaml</code>.
You should see something like this:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ kubectl <span class="nt">-n</span> 1password get onepassworditems test1
NAME    AGE
test1   10s
</code></pre></div></div>

<p>And then the corresponding secret:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ kubectl <span class="nt">-n</span> 1password get secret test1
NAME    TYPE     DATA   AGE
test1   Opaque   2      45s
</code></pre></div></div>

<p>And to make sure it’s working (recall the secret above has two fields, <code class="language-plaintext highlighter-rouge">username</code> is <code class="language-plaintext highlighter-rouge">foo</code> and <code class="language-plaintext highlighter-rouge">password</code> is <code class="language-plaintext highlighter-rouge">bar</code>):</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ kubectl <span class="nt">-n</span> 1password get secret test1 <span class="nt">-o</span> json | jq .data
<span class="o">{</span>
  <span class="s2">"password"</span>: <span class="s2">"YmFy"</span>,
  <span class="s2">"username"</span>: <span class="s2">"Zm9v"</span>
<span class="o">}</span>

❯ <span class="nb">echo </span>Zm9v | <span class="nb">base64</span> <span class="nt">-d</span>
foo

❯ <span class="nb">echo </span>YmFy | <span class="nb">base64</span> <span class="nt">-d</span>
bar
</code></pre></div></div>

<p>Sweet, looks good.</p>]]></content><author><name></name></author><category term="1password" /><category term="1p" /><category term="connect" /><category term="kubernetes" /><category term="secrets" /><summary type="html"><![CDATA[It took me a while to get Connect server going with Kubernetes, here are my notes. I haven’t gone very deep on this, I’m just starting with 1Password in earnest (beyond the simple use cases).]]></summary></entry><entry><title type="html">CI and SSH (and passphrases)</title><link href="https://mamercad.github.io/ci/ssh/passphrases/2022/04/24/ci-and-ssh.html" rel="alternate" type="text/html" title="CI and SSH (and passphrases)" /><published>2022-04-24T14:16:21+00:00</published><updated>2022-04-24T14:16:21+00:00</updated><id>https://mamercad.github.io/ci/ssh/passphrases/2022/04/24/ci-and-ssh</id><content type="html" xml:base="https://mamercad.github.io/ci/ssh/passphrases/2022/04/24/ci-and-ssh.html"><![CDATA[<p>Took me a while to figure out SSH keys (with passphrases) in CI.
I do much of my work (in operations) with <a href="https://github.com/ansible/ansible">Ansible</a>.
Typically, secrets in CI are handled with environment variables.
Suppose you need to SSH from within CI to do some work; the following should do the trick (in a Linux or Linux-like runtime):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">eval</span> <span class="si">$(</span>ssh-agent<span class="si">)</span>
<span class="nb">install</span> <span class="nt">-b</span> <span class="nt">-m</span> 600 /dev/null ssh.key
<span class="nb">echo</span> <span class="s2">"</span><span class="k">${</span><span class="nv">SSH_PRIVATE_KEY</span><span class="k">}</span><span class="s2">"</span> <span class="o">&gt;</span>ssh.key
<span class="nb">echo</span> <span class="s2">"</span><span class="k">${</span><span class="nv">SSH_PASSPHRASE</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
  | <span class="nv">SSH_ASKPASS</span><span class="o">=</span><span class="nb">cat </span>setsid <span class="nt">-c</span> ssh-add ssh.key
ssh-add <span class="nt">-l</span>
</code></pre></div></div>

<p>Hopefully your CI will redact the secrets.
As you can see, you’ll need <code class="language-plaintext highlighter-rouge">ssh-agent</code>, <code class="language-plaintext highlighter-rouge">install</code>, <code class="language-plaintext highlighter-rouge">cat</code>, <code class="language-plaintext highlighter-rouge">setsid</code>, and, <code class="language-plaintext highlighter-rouge">ssh-add</code>.
As far as the secrets go, your private key should be stored in the environment as <code class="language-plaintext highlighter-rouge">SSH_PRIVATE_KEY</code> and the passphrase as <code class="language-plaintext highlighter-rouge">SSH_PASSPHRASE</code>.</p>]]></content><author><name></name></author><category term="ci" /><category term="ssh" /><category term="passphrases" /><summary type="html"><![CDATA[Took me a while to figure out SSH keys (with passphrases) in CI. I do much of my work (in operations) with Ansible. Typically, secrets in CI are handled with environment variables. Suppose you need to SSH from within CI to do some work; the following should do the trick (in a Linux or Linux-like runtime):]]></summary></entry><entry><title type="html">UniFi policy-based routing (PBR)</title><link href="https://mamercad.github.io/unifi/pbr/policy-based/routing/homelab/ubiquiti/2021/10/11/unifi-pbr.html" rel="alternate" type="text/html" title="UniFi policy-based routing (PBR)" /><published>2021-10-11T08:02:53+00:00</published><updated>2021-10-11T08:02:53+00:00</updated><id>https://mamercad.github.io/unifi/pbr/policy-based/routing/homelab/ubiquiti/2021/10/11/unifi-pbr</id><content type="html" xml:base="https://mamercad.github.io/unifi/pbr/policy-based/routing/homelab/ubiquiti/2021/10/11/unifi-pbr.html"><![CDATA[<p>My home is powered by <a href="https://ui.com/">Ubiquiti’s UniFi</a> product line.
I’ve been using the kit for, oh, probably 3 or 4 years now, and it’s been fine (there’s much to be said for things that just work).
I don’t jump on the latest-and-greatest firmware; typically, I’m probably months behind what’s current.
At the time of writing, I’m at <code class="language-plaintext highlighter-rouge">6.0.45</code> and the blurb says that <code class="language-plaintext highlighter-rouge">6.4.54</code> is out.</p>

<p>Here’s a list of what I’m currently running:</p>

<ul>
  <li>CloudKey Gen1 (1)</li>
  <li>USG-3P (1)</li>
  <li>US-8-60W (2)</li>
  <li>US-8 (2)</li>
  <li>UAP-AC-LR (3)</li>
  <li>UAP-AC-Pro (1)</li>
</ul>

<p>My configuration is pretty boring, I currently only have two VLANs, the native one and one for the open guest network.
I’ll get around to adding more some day (I’m looking at you, IoT).
As far as WAN goes, I have two ISPs, <a href="https://xfinity.com">Xfinity</a> Cable and <a href="https://att.com">AT&amp;T</a> ADSL.
Unfortunately, I don’t have fiber options right now.
My partner and I both work in tech, and, our kids are on the internet as well.
So, having good uptime is nice.
A few years ago, my Xfinity wasn’t as reliable as it is today (turns out that squirrels are a thing), which is when I added the AT&amp;T service.
Once a new cable line was run those problems went away.</p>

<p>As far as utilization goes, up until a few days ago, I was running the DSL as failover-only.
So, we’d automatically failover ot AT&amp;T when Xfinity dropped, and, failback when it came back.
This worked out just fine, and the option to set this up is available in the UI.
The other option is weighted load-balancing, which I don’t want – I don’t want my connections alternating, and, the DSL that I have is much slower than my cable.</p>

<p>Unfortunately, doing policy-based routing isn’t available in the UI.
Since I’m paying for DSL, I wanted to get a bit more utilization out of it, but, on my terms.
There are lots of ways that you can go with this, but what I’ve done thus far is route the guest VLAN through it (and, I have a static route set up for something that I’m not going to go into <code class="language-plaintext highlighter-rouge">:smile:</code>).
Static routes can be configured in the UI, but, IPs aren’t always stable; and, again, I don’t want my connections flipping between my providers like this generally.</p>

<p>So, to the UniFI CLI we go (<a href="https://community.ui.com/questions/Relation-between-EdgeOS-and-Vyatta-VyOS/16e71fa1-cc29-4e89-a6e2-268264be25c4">EdgeOS is based on Vyatta</a>).</p>

<p>On the USG, you can show your interfaces like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mamercad@gateway:~<span class="nv">$ </span>show interfaces
Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
Interface    IP Address                        S/L  Description
<span class="nt">---------</span>    <span class="nt">----------</span>                        <span class="nt">---</span>  <span class="nt">-----------</span>
eth0         68.46.132.66/23                   u/u  WAN
eth1         192.168.1.1/24                    u/u  LAN
eth1.15      192.168.15.1/24                   u/u
eth2         162.230.47.152/23                 u/u  WAN2
lo           127.0.0.1/8                       u/u
             ::1/128
</code></pre></div></div>

<p>For me, <code class="language-plaintext highlighter-rouge">WAN</code> is Xfinity and <code class="language-plaintext highlighter-rouge">WAN2</code> is AT&amp;T.</p>

<p>You can show your failover configuration like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mamercad@gateway:~<span class="nv">$ </span>show load-balance status
Group wan2_failover
  interface   : eth0
  carrier     : up
  status      : failover
  gateway     : 68.46.132.1
  route table : 201
  weight      : 0%
  flows
      WAN Out : 0
      WAN In  : 21726
    Local Out : 0

  interface   : eth2
  carrier     : up
  status      : active
  gateway     : 162.230.46.1
  route table : 202
  weight      : 100%
  flows
      WAN Out : 640
      WAN In  : 0
    Local Out : 1122

Group wan_failover
  interface   : eth0
  carrier     : up
  status      : active
  gateway     : 68.46.132.1
  route table : 203
  weight      : 100%
  flows
      WAN Out : 546000
      WAN In  : 0
    Local Out : 0

  interface   : eth2
  carrier     : up
  status      : failover
  gateway     : 162.230.46.1
  route table : 204
  weight      : 0%
  flows
      WAN Out : 3
      WAN In  : 0
    Local Out : 0
</code></pre></div></div>

<p>The configuration that I ended up going with looks like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>configure
<span class="nb">set </span>load-balance group wan2_failover interface eth0 failover-only
<span class="nb">set </span>load-balance group wan2_failover interface eth2
<span class="nb">set </span>firewall modify LOAD_BALANCE rule 2501 action modify
<span class="nb">set </span>firewall modify LOAD_BALANCE rule 2501 modify lb-group wan2_failover
<span class="nb">set </span>firewall modify LOAD_BALANCE rule 2501 <span class="nb">source </span>address 192.168.15.0/24
commit <span class="p">;</span> <span class="nb">exit</span>
</code></pre></div></div>

<p>My guest VLAN is 15, and, the L3 for it is <code class="language-plaintext highlighter-rouge">192.168.15.0/24</code> (this shows up as <code class="language-plaintext highlighter-rouge">eth1.15</code> in <code class="language-plaintext highlighter-rouge">show interfaces</code>).
As you can see, anything originating from this network will be shipped over WAN2 (AT&amp;T).
This configuration works just fine, but, if and when the USG restarts, it’ll be lost.
We need to get this onto the CloudKey, and for that, it’ll take a custom <a href="https://help.ui.com/hc/en-us/articles/215458888-UniFi-USG-Advanced-Configuration-Using-config-gateway-json">config.gateway.json</a>.</p>

<p>This part was relatively straightforward – basically, I dumped and diffed.
In <code class="language-plaintext highlighter-rouge">config.gateway.json</code>, you should only supply the differences (unless you’re okay with not using the UI to manage the USG).
As with anything, it’s always a good idea to take a backup before making changes.
On the USG, before doing anything, I did a <code class="language-plaintext highlighter-rouge">cp /config/config.boot{,.bak}</code>.
You can dump your config like this <code class="language-plaintext highlighter-rouge">mca-ctrl -t dump-cfg &gt;config.txt 2&gt;&amp;1</code>.
Then, I ran the configuration stuff above, and then, diffed them to figure out how to craft <code class="language-plaintext highlighter-rouge">config.gateway.json</code>.</p>

<p>In the end, what I ended up with is this:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"firewall"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ip-src-route"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disable"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ipv6-receive-redirects"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disable"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ipv6-src-route"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disable"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"log-martians"</span><span class="p">:</span><span class="w"> </span><span class="s2">"enable"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"modify"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"LOAD_BALANCE"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"rule"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"2501"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"action"</span><span class="p">:</span><span class="w"> </span><span class="s2">"modify"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"modify"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
              </span><span class="nl">"lb-group"</span><span class="p">:</span><span class="w"> </span><span class="s2">"wan2_failover"</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"source"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
              </span><span class="nl">"address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"192.168.15.0/24"</span><span class="w">
            </span><span class="p">}</span><span class="w">
          </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"source-validation"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disable"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"interfaces"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ethernet"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"eth0"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"dhcp-options"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"default-route"</span><span class="p">:</span><span class="w"> </span><span class="s2">"update"</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="nl">"duplex"</span><span class="p">:</span><span class="w"> </span><span class="s2">"auto"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"speed"</span><span class="p">:</span><span class="w"> </span><span class="s2">"auto"</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"eth1"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"duplex"</span><span class="p">:</span><span class="w"> </span><span class="s2">"auto"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"speed"</span><span class="p">:</span><span class="w"> </span><span class="s2">"auto"</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"eth2"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"dhcp-options"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"default-route"</span><span class="p">:</span><span class="w"> </span><span class="s2">"update"</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="nl">"duplex"</span><span class="p">:</span><span class="w"> </span><span class="s2">"auto"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"speed"</span><span class="p">:</span><span class="w"> </span><span class="s2">"auto"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"load-balance"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"group"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"wan2_failover"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"flush-on-active"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disable"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"interface"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"eth0"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"failover-only"</span><span class="p">:</span><span class="w"> </span><span class="s2">"''"</span><span class="w">
          </span><span class="p">},</span><span class="w">
          </span><span class="nl">"eth2"</span><span class="p">:</span><span class="w"> </span><span class="s2">"''"</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="nl">"lb-local"</span><span class="p">:</span><span class="w"> </span><span class="s2">"enable"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"lb-local-metric-change"</span><span class="p">:</span><span class="w"> </span><span class="s2">"enable"</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"wan_failover"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"flush-on-active"</span><span class="p">:</span><span class="w"> </span><span class="s2">"disable"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"lb-local"</span><span class="p">:</span><span class="w"> </span><span class="s2">"enable"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"lb-local-metric-change"</span><span class="p">:</span><span class="w"> </span><span class="s2">"enable"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>After constructing this file, I dropped it onto my CloudKey.
I only have a single site, and a Gen1 CloudKey, so, it lives at <code class="language-plaintext highlighter-rouge">/srv/unifi/data/sites/default/config.gateway.json</code>.
A little while after, the USG rebooted to (re)provision.
It only took me a few tries to get it right <code class="language-plaintext highlighter-rouge">:smile:</code>.
I’ll do more than primarily routing my guest network through it in time.
Adding more VLANs should be as simple as adding <code class="language-plaintext highlighter-rouge">sources</code> to the <code class="language-plaintext highlighter-rouge">wan2_failover</code> configuration.
Hope this helps the next person!</p>]]></content><author><name></name></author><category term="unifi" /><category term="pbr" /><category term="policy-based" /><category term="routing" /><category term="homelab" /><category term="ubiquiti" /><summary type="html"><![CDATA[My home is powered by Ubiquiti’s UniFi product line. I’ve been using the kit for, oh, probably 3 or 4 years now, and it’s been fine (there’s much to be said for things that just work). I don’t jump on the latest-and-greatest firmware; typically, I’m probably months behind what’s current. At the time of writing, I’m at 6.0.45 and the blurb says that 6.4.54 is out.]]></summary></entry><entry><title type="html">Local dev environment for Concourse CI</title><link href="https://mamercad.github.io/docker/concourse/concourse-di/2021/04/25/concourse-local-dev.html" rel="alternate" type="text/html" title="Local dev environment for Concourse CI" /><published>2021-04-25T13:09:52+00:00</published><updated>2021-04-25T13:09:52+00:00</updated><id>https://mamercad.github.io/docker/concourse/concourse-di/2021/04/25/concourse-local-dev</id><content type="html" xml:base="https://mamercad.github.io/docker/concourse/concourse-di/2021/04/25/concourse-local-dev.html"><![CDATA[<p>Over the last couple of days, I’ve been writing a <a href="https://github.com/mamercad/concourse-awx-resource">custom Concourse resource</a>, and I thought I’d take a few to walk through how I set up my local development environment. If you’re not familiar with <a href="https://concourse-ci.org/">Concourse CI</a>, and you’re doing some sort of CI/CD, give it a look. For me, it’s been quite refreshing. At <code class="language-plaintext highlighter-rouge">$lastjob</code>, we were a <a href="https://www.jenkins.io/">Jenkins</a> shop, and, I’m not sure I miss it all that much <code class="language-plaintext highlighter-rouge">:smile:</code>. When it comes down to usage, I’ll take declarative YAML over imperative Groovy every day of the week, including Sunday.</p>

<p>Alright, let’s roll. Concourse is built with containers in mind, so, we’ll need something like <a href="https://www.docker.com/products/docker-desktop">Docker Desktop</a>. The container runtime shouldn’t matter all that much, I just happened to be using my Mac at the time. When I first started writing the Concourse resource, I was pushing to <a href="quay.io">quay.io</a>, which is fine, but, when you’re trying to iterate quickly, becomes a bottleneck in a hurry. I’d never written a Concourse resource in the past, so, it took me lots of tries to get things working. There’s nothing fast about pushing and pulling ~ gigabyte between a 5-year-old laptop and Quay over and over again. It didn’t take me long to throw that out and set everything up locally.</p>

<p>Setting up Concourse locally is relatively painless, <a href="https://github.com/concourse/concourse/blob/master/docker-compose.yml">they provide a compose file</a>. The only change (addition) I’ve made is adding a local Docker registry to keep my images pushing and pulling local.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># existing Concourse services here</span>
  <span class="na">registry</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">registry:2</span>
    <span class="na">ports</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">5000:5000"</span><span class="pi">]</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">${PWD}/server.crt:/server.crt</span>
      <span class="pi">-</span> <span class="s">${PWD}/server.key:/server.key</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">REGISTRY_HTTP_ADDR</span><span class="pi">:</span> <span class="s">0.0.0.0:5000</span>
      <span class="na">REGISTRY_HTTP_TLS_CERTIFICATE</span><span class="pi">:</span> <span class="s">server.crt</span>
      <span class="na">REGISTRY_HTTP_TLS_KEY</span><span class="pi">:</span> <span class="s">server.key</span>
</code></pre></div></div>

<p>Here’s a one-liner for generating “snake oil” crypto:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>❯ openssl req <span class="nt">-new</span> <span class="nt">-newkey</span> rsa:4096 <span class="nt">-days</span> 3650 <span class="nt">-nodes</span> <span class="nt">-x509</span> <span class="se">\</span>
    <span class="nt">-subj</span> <span class="s2">"/C=</span><span class="k">${</span><span class="nv">COUNTRY</span><span class="k">}</span><span class="s2">/ST=</span><span class="k">${</span><span class="nv">STATE</span><span class="k">}</span><span class="s2">/L=</span><span class="k">${</span><span class="nv">CITY</span><span class="k">}</span><span class="s2">/O=</span><span class="k">${</span><span class="nv">ORG</span><span class="k">}</span><span class="s2">)/CN=</span><span class="k">${</span><span class="nv">FQDN</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">-keyout</span> server.key <span class="nt">-out</span> server.crt
</code></pre></div></div>

<p>So, when it comes to tagging your images, they’d look something like this <code class="language-plaintext highlighter-rouge">registry:5000/my-image-name:0.0.1</code>. Note that <code class="language-plaintext highlighter-rouge">registry</code> lines up with the service name (the existing Concourse services in the “pod” can resolve this name), the <code class="language-plaintext highlighter-rouge">5000</code> is obviously the chosen port, <code class="language-plaintext highlighter-rouge">my-image-name</code> is an arbitrary image name, and <code class="language-plaintext highlighter-rouge">0.0.1</code> is an arbitrary tag. Now, when you <code class="language-plaintext highlighter-rouge">docker push ...</code> you’ll be pushing to the Docker registry running locally.</p>

<p>Closing the loop, we’ll need to also pull from our local Docker registry, here’s an example of how our <code class="language-plaintext highlighter-rouge">resource_type</code> would look:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">resource_types</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">our-example-image</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">docker-image</span>
    <span class="na">source</span><span class="pi">:</span>
      <span class="na">repository</span><span class="pi">:</span> <span class="s">registry:5000/my-image-name</span>
      <span class="na">version</span><span class="pi">:</span> <span class="s">0.0.1</span>
</code></pre></div></div>

<p>Hopefully this’ll help speed things up for you as they did for me!</p>]]></content><author><name></name></author><category term="docker" /><category term="concourse" /><category term="concourse-di" /><summary type="html"><![CDATA[Over the last couple of days, I’ve been writing a custom Concourse resource, and I thought I’d take a few to walk through how I set up my local development environment. If you’re not familiar with Concourse CI, and you’re doing some sort of CI/CD, give it a look. For me, it’s been quite refreshing. At $lastjob, we were a Jenkins shop, and, I’m not sure I miss it all that much :smile:. When it comes down to usage, I’ll take declarative YAML over imperative Groovy every day of the week, including Sunday.]]></summary></entry></feed>