<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Bits and Boats]]></title><description><![CDATA[Random musings of a Geek on the web... Topics include Boating, Camping, Coding, Computing, Embedded Controls type projects, Parenting, Scouting, Security, and a]]></description><link>https://blog.murawsky.net</link><generator>RSS for Node</generator><lastBuildDate>Wed, 20 May 2026 09:25:22 GMT</lastBuildDate><atom:link href="https://blog.murawsky.net/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[What is the Easiest Way to Manage Your AWS Root Credentials?]]></title><description><![CDATA[There are many ways of handling your AWS Root credentials, but after many years of going back and forth with various vaults and password management systems, I came across a surprisingly simple pattern: Don’t bother remembering those passwords to begi...]]></description><link>https://blog.murawsky.net/what-is-the-easiest-way-to-manage-your-aws-root-credentials</link><guid isPermaLink="true">https://blog.murawsky.net/what-is-the-easiest-way-to-manage-your-aws-root-credentials</guid><category><![CDATA[AWS]]></category><category><![CDATA[Security]]></category><category><![CDATA[Credentials]]></category><category><![CDATA[root account]]></category><category><![CDATA[Bitwarden]]></category><category><![CDATA[Cloud Computing]]></category><category><![CDATA[cloud security]]></category><dc:creator><![CDATA[Derek Murawsky]]></dc:creator><pubDate>Fri, 04 Oct 2024 21:35:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/hW11fwjzVfA/upload/8de376ad618023ebc168a581c416ccd7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There are many ways of handling your AWS Root credentials, but after many years of going back and forth with various vaults and password management systems, I came across a surprisingly simple pattern: Don’t bother remembering those passwords to begin with! Set up a 2fa token, and just use the password reset workflow.</p>
<p>We’ve all been there… We set up a new AWS account and new we have an MFA token and password to deal with. Do we use a physical security token (single point of failure)? If so where do we store it so that it’s accessible? If you have a 24×7 staffed NOC with a vault, this is a simple solve, but most places don’t have that. Especially in the SMB and startup spaces. So we go to a virtual MFA device. And now only one person has that on their phone (single point of failure again) or we need to share the key out (hacky) or store in in a vault of some kind (at least these are digital).</p>
<p>Well, here is one approach that I’ve actually used several times with surprising success. It has also withstood many audits. Does that mean it’s perfect? No. But it is an effective method, even it it’s a bit out there.</p>
<h1 id="heading-prerequisites">Prerequisites</h1>
<ol>
<li><p>An email provider that supports distribution lists. Ideally one that lets you manage those memberships dynamically based on security groups. I use <a target="_blank" href="https://www.microsoft.com/en-us/security/business/microsoft-entra">Azure Entre (Active Directory)</a> and Exchange Online (all via Office365) for this.</p>
</li>
<li><p>A place to securely store OTP keys, ideally tied to the same security group management tool as the email distribution lists. I like to use <a target="_blank" href="https://bitwarden.com/">Bitwarden</a> for this, but <a target="_blank" href="https://www.vaultproject.io/">Hashicorp Vault</a> and other tools support this as well.</p>
</li>
</ol>
<h1 id="heading-initial-setup">Initial Setup</h1>
<p>For every AWS account you create, you will use the DL as the email address for the account. Your root account might be <code>DL-AWS-Root@example.com</code> while your shared dev account might be <code>DL-AWS-Dev@example.com</code>. <a target="_blank" href="https://martinfowler.com/bliki/TwoHardThings.html">Naming things is hard</a>, but defining this namespace early and trying to stick with it is important to your long-term sanity as accounts sprawl. What do you use for the password? The longest string of random characters that your password generator of choice will spit out, and that AWS will accept. I believe that that is <a target="_blank" href="https://docs.aws.amazon.com/singlesignon/latest/userguide/password-requirements.html#:~:text=Passwords%20are%20case%2Dsensitive.,and%2064%20characters%20in%20length.">currently 64 characters</a>. Copy it to your clipboard, use it to log in, then overwrite your clipboard. You never want to remember it. Just get rid of it. It never existed. Poof! It’s gone forever into the ether…</p>
<p>Now bootstrap your account as normal, add your support plan if you want one, setting up any core roles that you need to assume that aren’t part of your account factory, and finally, set up your MFA token for the root account. Store that token in your vault, and control access to it. Now that the bootstrapping is complete, log out. And make sure that root password is gone, never to be a thorn in your side again. Beautiful isn’t it?</p>
<p><img src="https://images.unsplash.com/photo-1500964757637-c85e8a162699?fm=jpg&amp;q=60&amp;w=3000&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="A beautiful, serene landscape photography of mountains - https://unsplash.com/photos/landscape-photography-of-mountains-twukN12EN7c" /></p>
<h1 id="heading-but-how-do-i-log-back-in">But how do I log back in?</h1>
<p>Only certain people should ever use the root account. Generally, it’s a break glass kind of thing, and you <em>want</em> everyone to know it’s happening. Thankfully, you have an email distribution list set up for your password reset process. So any time someone tries to reset the password to log in, everyone on the list will know about the attempt. You also still have your second factor in a separate system. Then the individual logs in, and resets the password to another completely random string of 64 characters and purposely doesn’t remember it.</p>
<h1 id="heading-problems-with-this-approach">Problems with this approach</h1>
<p>Though this approach is a bit out there, it does work, and it stands up to many security policies and controls frameworks. It’s still not perfect though.</p>
<ul>
<li><p>It relies on the people in the process to not save the new password. This can be mitigated by creating alerts for whenever the root account is accessed, which is already a best practice.</p>
</li>
<li><p>It creates reliance on other systems, email and a vault, which have other administrators and attack surfaces. Granted, we have reliance on those systems anyway, but it’s still an extra hop. In a break glass scenario, we want fast access. We don’t want to wait for an email to go through.</p>
</li>
<li><p>Though the OTP key is stored in a separate system, access to it is typically controlled by the same authentication and authorization framework. This doesn’t have to be the case, but it does dramatically reduce operational burden.</p>
</li>
<li><p>It’s very counter-intuitive. We want to store important credentials, not forget them! As a result, this can take some time to work through the approvals process.</p>
</li>
<li><p>Now that <a target="_blank" href="https://aws.amazon.com/blogs/aws/aws-adds-passkey-multi-factor-authentication-mfa-for-root-and-iam-users/">AWS supports Passkeys</a>, you could also store those in a vault instead, though there may be issues with that approach that I am not aware of. This approach might be worth diving into, though. Especially since <a target="_blank" href="https://bitwarden.com/help/storing-passkeys/">Bitwarden</a> and the like supports passkey storage as well.</p>
</li>
</ul>
<h1 id="heading-wrapping-up">Wrapping up</h1>
<p>Well, there it is. A surprisingly scaleable pattern for handling root credentials and tokens in a simple or complex AWS environment. What do you think? Is this approach out there? What other issues do you see with it? How do you manage your root credentials, and what issues do you have with your approach?</p>
]]></content:encoded></item><item><title><![CDATA[EventBridge Pipes in Practice]]></title><description><![CDATA[Amazon released EventBridge Pipes in December of 2022. It is a fantastic tool that attempts to simplify gluing pieces of a cloud infrastructure together in a serverless way. Where you previously needed a glue lambda and queues, you may be able to use...]]></description><link>https://blog.murawsky.net/eventbridge-pipes-in-practice</link><guid isPermaLink="true">https://blog.murawsky.net/eventbridge-pipes-in-practice</guid><category><![CDATA[AWS]]></category><category><![CDATA[eventbridge]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[pipes]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[CDK]]></category><dc:creator><![CDATA[Derek Murawsky]]></dc:creator><pubDate>Wed, 14 Aug 2024 21:30:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/_EFvjSgbw1c/upload/ee662626751cddfe926a8a599ce103ef.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Amazon released <a target="_blank" href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-pipes.html">EventBridge Pipes</a> in <a target="_blank" href="https://aws.amazon.com/about-aws/whats-new/2022/12/amazon-eventbridge-pipes-generally-available/">December of 2022</a>. It is a fantastic tool that attempts to simplify gluing pieces of a cloud infrastructure together in a serverless way. Where you previously needed a glue lambda and queues, you may be able to use Pipes instead.</p>
<h1 id="heading-components-of-a-pipe">Components of a Pipe</h1>
<p><img src="https://docs.aws.amazon.com/images/eventbridge/latest/userguide/images/pipes-overview-detailed_eventbridge_architectural.svg" alt="High-level image of an AWS Pipe, with an event source leading to a filter, enricher, and target." class="image--center mx-auto" /></p>
<p>Pipes, fundamentally, are very simple: they are a service that connects a source to a destination, allowing you to optionally filter and enrich the data as it flows through. It's built in a way that lets you compose items together easily, and add custom code where you need to. The real nice bit here is how many services it supports, from Kinesis Sources to destinations like API Gateway, EventBridge, and many things in between, Pipes can really simplify interconnections. New service are being added, so for an update list or <a target="_blank" href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-pipes-event-source.html">sources</a> and <a target="_blank" href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-pipes-event-target.html">targets</a>, see the AWS Documentation site.</p>
<h1 id="heading-example-scenario">Example Scenario</h1>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723646790773/2bf0d9d2-c3e8-48f4-aa98-6246ad0e1a20.png" alt class="image--center mx-auto" /></p>
<p>Let's take the following example: We have an existing Events API that collects event data from many external sources (web apps, mobile apps, etc). The API pushes to a Kinesis Stream to be handled by a series of backend processes. We want to tap that stream and push the events in it to an internal Event Bridge <em>without disrupting the existing architecture</em> so that we can generate metrics and further react to events dynamically.</p>
<h1 id="heading-other-options">Other Options</h1>
<p>If we were able to change everything or build a new system from scratch, we could just leverage <a target="_blank" href="https://aws.amazon.com/blogs/compute/capturing-client-events-using-amazon-api-gateway-and-amazon-eventbridge/">API Gateway's native capability to push directly to EventBridge</a> (this is a great pattern, btw). However that is a lot of change, and in this scenario, we aren't allowed to disrupt things to that degree.</p>
<p>Before Pipes, you would likely attach Lambda to the existing Kinesis Stream and have it push messages to the target EventBridge Bus. This is a completely valid approach, but does have some complexities involved.</p>
<h1 id="heading-enter-aws-pipes">Enter AWS Pipes</h1>
<p>Instead, let's use pipes to tap that stream and get our events to EventBridge completely serverlessly. And since we like IAC (and a challenge), let's do it in <a target="_blank" href="https://aws.amazon.com/cdk/">CDK</a> with <a target="_blank" href="https://www.typescriptlang.org/">Typescript</a>... Why? Well, that's what I use at work. Let me preface all of this by saying I'm relatively new to Typescript, so please let me know where I can improve the code.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723647550763/4a126a4d-0f08-431a-b2ef-2bdcc00ff85c.png" alt class="image--center mx-auto" /></p>
<p>CDK makes building infrastructure in AWS much simpler. There are many built in constructs that make common patterns available with a simple instantiation, and it enforces best practices like least-privilege out of the box. Check out the <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html">API Docs</a> for more if you're not familiar. Unfortunately, Pipes is so new, that the support in CDK is limited... As of this writing, there are <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-pipes-alpha-readme.html">Alpha Packages</a> (<a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-pipes-enrichments-alpha-readme.html">enrichments</a>, <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-pipes-sources-alpha-readme.html">sources</a>, <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-pipes-targets-alpha-readme.html">targets</a>) that have the basics in place, and we can build the missing pieces on top of it.</p>
<h2 id="heading-custom-cdk-pipe-source">Custom CDK Pipe Source</h2>
<p>Lets start where all things begin; the source. There is an example SQS Source, and <a target="_blank" href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-pipes-alpha-readme.html#example-source-implementation">the documentation</a> mentions how to create a source implementation, so let's dive in...</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cdk <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib"</span>;

<span class="hljs-comment">/**
 * A Kinesis source for EventBridge Pipes
 *
 * @param stream The Kinesis stream to use as the source
 * @param kinesisStreamParameters The parameters for the Kinesis stream. Starting Position defaults to LATEST
 */</span>
<span class="hljs-keyword">class</span> KinesisSource <span class="hljs-keyword">implements</span> pipes.ISource {
  sourceArn: <span class="hljs-built_in">string</span>;
  sourceParameters: pipes.SourceParameters = {
    kinesisStreamParameters: {
      startingPosition: <span class="hljs-string">"LATEST"</span>, <span class="hljs-comment">// I like sane defaults</span>
    },
  };

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> stream: kinesis.IStream,
    kinesisStreamParameters?: cdk.aws_pipes.CfnPipe.PipeSourceKinesisStreamParametersProperty
  </span>) {
    <span class="hljs-built_in">this</span>.stream = stream;
    <span class="hljs-built_in">this</span>.sourceArn = stream.streamArn;
    <span class="hljs-keyword">if</span> (kinesisStreamParameters) {
      <span class="hljs-built_in">this</span>.sourceParameters = { kinesisStreamParameters };
    }
  }
  <span class="hljs-comment">// eslint-disable-next-line</span>
  bind(_pipe: pipes.IPipe): pipes.SourceConfig {
    <span class="hljs-keyword">return</span> {
      sourceParameters: <span class="hljs-built_in">this</span>.sourceParameters,
    };
  }

  grantRead(pipeRole: cdk.aws_iam.IRole): <span class="hljs-built_in">void</span> {
    <span class="hljs-built_in">this</span>.stream.grantRead(pipeRole);
  }
}
</code></pre>
<p>This source uses the existing CfnPipe primitives to handle the properties, which makes the implementation relatively easy. There is only one required parameter, and that is the Starting Position. I assumed <code>LATEST</code> as a sane default, but this object allows us to override as needed.</p>
<h2 id="heading-custom-cdk-pipe-target">Custom CDK Pipe Target</h2>
<p>Just like the Source, we need a target.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cdk <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib"</span>;

<span class="hljs-comment">/**
 * An EventBridge target for EventBridge Pipes
 *
 * @param targetEventBridge The EventBridge event bus to use as the target
 * @param inputTransformation The input transformation to apply to the event
 */</span>
<span class="hljs-keyword">class</span> EventBridgeTarget <span class="hljs-keyword">implements</span> pipes.ITarget {
  targetArn: <span class="hljs-built_in">string</span>;
  <span class="hljs-keyword">private</span> inputTransformation: pipes.InputTransformation | <span class="hljs-literal">undefined</span>;
  <span class="hljs-keyword">private</span> targetParameters: cdk.aws_pipes.CfnPipe.PipeTargetEventBridgeEventBusParametersProperty | <span class="hljs-literal">undefined</span>;

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> targetEventBridge: events.IEventBus,
    props: {
      inputTransformation?: pipes.InputTransformation;
      targetConfig?: CfnPipe.PipeTargetEventBridgeEventBusParametersProperty;
    } = {}
  </span>) {
    <span class="hljs-built_in">this</span>.targetEventBridge = targetEventBridge;
    <span class="hljs-built_in">this</span>.targetArn = targetEventBridge.eventBusArn;
    <span class="hljs-built_in">this</span>.inputTransformation = props?.inputTransformation;
    <span class="hljs-built_in">this</span>.targetParameters = props?.targetConfig;
  }

  bind(_pipe: pipes.Pipe): pipes.TargetConfig {
    <span class="hljs-keyword">return</span> {
      targetParameters: {
        inputTemplate: <span class="hljs-built_in">this</span>.inputTransformation?.bind(_pipe).inputTemplate,
        eventBridgeEventBusParameters: <span class="hljs-built_in">this</span>.targetParameters,
      },
    };
  }

  grantPush(pipeRole: cdk.aws_iam.IRole): <span class="hljs-built_in">void</span> {
    <span class="hljs-built_in">this</span>.targetEventBridge.grantPutEventsTo(pipeRole);
  }
}
</code></pre>
<p>Again, we're leveraging the primitives from the underlying CDK alpha to generate our target. Targets support more options, so I handled them a little differently here. Note the bind function, where I return a targetParameters object. That is what corresponds to the <a target="_blank" href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pipes-pipe-pipetargetparameters.html">CloudFormation target parameters object</a> which is complex and is used by all pipe targets. I'm sure there's a nicer abstraction that could be used here, but I was more focused on getting it up and running that writing the next alpha target.</p>
<h2 id="heading-using-our-new-constructs">Using Our New Constructs</h2>
<p>Using the above constructs, we are able to use the existing pipe from the alpha library to get a working pipe!</p>
<pre><code class="lang-typescript">    <span class="hljs-comment">// EventsMetricsBus is ppublic on the construct so that other stacks</span>
    <span class="hljs-comment">// can reference it for adding rules easily. </span>
    <span class="hljs-built_in">this</span>.EventsMetricsBus = <span class="hljs-keyword">new</span> events.EventBus(<span class="hljs-built_in">this</span>, <span class="hljs-string">`<span class="hljs-subst">${prefix}</span>-eventBus`</span>, {
      eventBusName: <span class="hljs-string">`<span class="hljs-subst">${prefix}</span>-eventBus`</span>,
    });

    <span class="hljs-keyword">const</span> sourceEventStream = kinesis.Stream.fromStreamArn(<span class="hljs-built_in">this</span>, <span class="hljs-string">`<span class="hljs-subst">${prefix}</span>-sourceEventStream`</span>, kinesisStreamArn);

    <span class="hljs-keyword">new</span> eventsSchemas.CfnDiscoverer(<span class="hljs-built_in">this</span>, <span class="hljs-string">`<span class="hljs-subst">${prefix}</span>-discoverer`</span>, {
      sourceArn: <span class="hljs-built_in">this</span>.EventsMetricsBus.eventBusArn,
      description: <span class="hljs-string">"Discoverer for event metrics bus"</span>,
    });

    <span class="hljs-keyword">const</span> base64InputTransformer = pipes.InputTransformation.fromText(
      <span class="hljs-string">'{ "kinesisWrappedEvent": &lt;$.data&gt;, "originalEvent" : &lt;aws.pipes.event&gt; }'</span>
    );

    <span class="hljs-keyword">const</span> kinesisToEventBridgePipe = <span class="hljs-keyword">new</span> pipes.Pipe(<span class="hljs-built_in">this</span>, <span class="hljs-string">`<span class="hljs-subst">${prefix}</span>-kinesisToEventBridgePipe`</span>, {
      pipeName: <span class="hljs-string">`<span class="hljs-subst">${prefix}</span>-kinesisToEventBridgePipe`</span>,
      source: <span class="hljs-keyword">new</span> KinesisSource(sourceEventStream),
      target: <span class="hljs-keyword">new</span> EventBridgeTarget(<span class="hljs-built_in">this</span>.EventsMetricsBus, {
        inputTransformation: base64InputTransformer,
        targetConfig: {
          source: sourceName,
          detailType: <span class="hljs-string">"$.partitionKey"</span>, <span class="hljs-comment">// This will always be here as it is the partition key of the Kinesis record itself</span>
        },
      }),
      logIncludeExecutionData: [pipes.IncludeExecutionData.ALL],
      logLevel: pipes.LogLevel.ERROR,
    });
</code></pre>
<p>Some notes on the above code...</p>
<ul>
<li><p><strong>Killer Feature:</strong> The Input Transformer uses a feature of Pipes called <a target="_blank" href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-pipes-input-transformation.html#input-transform-implicit">Implicit Body Data Parsing</a>. The Kinesis event data is base64 encoded. Pipes will automatically decode it and make it available. This means you can set your input for the EventBridge Bus to the decoded data, allowing for simple filtering. No enrichment required!</p>
<ul>
<li>This would let us set the input to pipes to just be <code>&lt;$.data&gt;</code> and it would essentially keep our data the same as it was when it came into the API, completely hiding the plumbing of kinesis. I really like this for downstream developer clarity.</li>
</ul>
</li>
<li><p>I like to use <code>${prefix}</code> in my CDK stacks. It allows for standard naming of resources throughout the stack.</p>
</li>
<li><p>Using the source is easy. I pass in a kinesis stream from another stack and use it as the source of our KinesisSource.</p>
</li>
<li><p><strong>Killer Feature:</strong> The target config supports the same JSON path syntax as the Input Transformer! So we are able to take anything from the object and use it as the detail-type! See <a target="_blank" href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-pipes-event-target.html#pipes-targets-dynamic-parms">Dynamic Path Parameters</a> in the docs. In this instance, our API Gateway sets the partition key to the event type, so our downstream events are now the same as the event name coming in to the API.</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>With these pieces in place, we have reached our goal! We can now react to our API Gateway events dynamically in our new EventBridge Bus. In our instance, we wanted to generate metrics on all the events in our system. This approach lets us do so cleanly, and without touching our existing process. As a bonus, we also got schema discovery of our events, which helped our team stay in sync to changes over time to our inbound events.</p>
<h1 id="heading-thanks">Thanks</h1>
<p>Many thanks to my seniors who encoruaged me and provided guidance through this project (Alex, Brandon, and Matt, you guys are the best)!</p>
]]></content:encoded></item><item><title><![CDATA[The CrowdStrike Outage is an Opportunity]]></title><description><![CDATA[CrowdStrike is in the news today as they released a faulty driver as part of their Falcon Sensor which caused millions of Windows systems around the world to go into a boot loop. For details on the issue, you can read their official statement on the ...]]></description><link>https://blog.murawsky.net/the-crowdstrike-outage-is-an-opportunity</link><guid isPermaLink="true">https://blog.murawsky.net/the-crowdstrike-outage-is-an-opportunity</guid><category><![CDATA[CrowdStrike Outage]]></category><category><![CDATA[Security]]></category><category><![CDATA[Software Update]]></category><category><![CDATA[Crowdstrike]]></category><category><![CDATA[#operations]]></category><dc:creator><![CDATA[Derek Murawsky]]></dc:creator><pubDate>Fri, 19 Jul 2024 18:19:42 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://www.crowdstrike.com/en-us/">CrowdStrike</a> is in the news today as they released a faulty driver as part of their Falcon Sensor which caused millions of Windows systems around the world to go into a boot loop. For details on the issue, <a target="_blank" href="https://www.crowdstrike.com/blog/statement-on-windows-sensor-update/">you can read their official statement on the topic</a>. My condolences to the various teams that are dealing with that all right now. Stay Strong!</p>
<p>I'm not going to jump on the bandwagon and bash CrowdStrike in this article, though. There is plenty of that out there already. Instead, I thought I would reflect a bit on what I see as a big miss with Endpoint Security Software in general: unfettered access to production.</p>
<p><a target="_blank" href="https://www.crowdstrike.com/global-threat-report/"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721403633563/d78e061b-1297-44db-85fa-cfdbd6f55b5e.jpeg" alt="Ironic CrowdStrike advertisement indicating how &quot;62 minutes could bring down your business&quot;. With friends like these, who needs adversaries?" class="image--center mx-auto" /></a></p>
<h1 id="heading-let-me-pose-a-leading-question">Let me pose a leading question</h1>
<p><em>Would you ever allow a third party to be able to install software on your production systems without testing it first in some kind of non-production environment?</em> How about software that explicitly has full system or kernel level access across your entire infrastructure? The answer in 99% of instances would be "Absolutely not", yet that 1% is allowed in the case of AV software. In fact, it has become a thing that many people assume should be allowed and most don't question unless they are in highly sensitive areas. <em>Why is that?</em></p>
<p>I believe the main reason is simple: We want timely updates in our security software. It just makes sense, right? Zero-day vulnerabilities are a real concern and timely updates as soon as they are available is just about the best thing we can do to address the problem. With that approach, though, comes the requirement to push those changes to prod as quickly as possible. That risk is just assumed to be worth it, but is it really? Or, rather, <em>should we always assume that it is always worth the risk?</em></p>
<p>According to <a target="_blank" href="https://www.ibm.com/reports/threat-intelligence">IBM’s X-Force threat intelligence team</a>, zero-day vulnerabilities account for about <a target="_blank" href="https://www.ibm.com/topics/zero-day">3% of all vulnerabilities</a>. That's not insignificant though it is small. Considering that <a target="_blank" href="https://storage.googleapis.com/gweb-uniblog-publish-prod/documents/Year_in_Review_of_ZeroDays.pdf">nation-states like China</a> are some of the top abusers of these vulnerabilities, though, the stakes can be surprisingly high. However, when you look at some key findings from the <a target="_blank" href="https://www.verizon.com/business/resources/Tb74/reports/2024-dbir-data-breach-investigations-report.pdf">Verizon Data Breach Investigations Report (DBIR)</a>, exploits and vulnerabilities are not the leading way-in to a system - that's still credential leakage and phishing. Further, a full 68% of breaches involved the human element... not directly tied to exploits. So my next leading question is simple: <em>Is the risk of system breach due to out of date security software and virus definitions always so severe that we need to make an exception to our production controls?</em></p>
<h1 id="heading-going-back-to-basics">Going back to basics</h1>
<p>Servers should be locked down from general user access, and even admin access should require quite a bit of proof of identity and need. These are the most basic of basics. In fact, there should be many other controls in place for reaching servers, including network isolation and segmentation. If we do these things well, we limit vulnerability exploitation to direct physical or network access only. This buys us time to do things like test our Security Software updates before rolling them out.</p>
<p>Commonly accepted practice with our own application deployments is that we roll out changes incrementally, sometimes by region, availability zone, or some other logical grouping. Using some kind of <a target="_blank" href="https://semaphoreci.com/blog/what-is-canary-deployment">Canary</a> or similar pattern allows us to watch for impact of the current change set that is going live. Yet we do not have that requirement with Security Software either. In fact, such software usually has a separate update channel entirely that is minimally configurable. At the very least, we should be rolling out updates to security software incrementally to ensure it doesn't break our systems or the services running on them.</p>
<p>Kiosk systems and end-user devices are certainly different as they are exposed more directly to the human element. As such, the do likely require more timely updates. However, I believe there are other compensating controls that can be implemented here as well - chief among them being zero-trust type systems where access to other services is controlled more by user identity than system level security. Proper segmentation goes a long way to buying us time that we can use to test a roll out of a security update.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>I am not advocating that we stop updating our security software quickly, nor am I suggesting that we keep AV software off of our servers (though I think there is a case for that in some instances, heresy, I know). What I am suggesting, though, is that we have a lot of assumptions in the security update space that we should reevaluate, chief among them is that we need to allow endpoint security software to update itself immediately without looking at the risks involved.</p>
<p>I believe that we have gotten lucky with updates in the past on the endpoint security side of things. It's clear that vendors in this space spend a lot of time making sure their tools don't break things, but that isn't something we can rely on. Let's look at the opportunity in the CrowdStrike outage. We can guide our teams into taking a step back and examining our assumptions around what we are currently doing for endpoint security updates. Then we can think on ways to improve on our operational practices to reduce our risk and prevent things like this outage from happening again.</p>
]]></content:encoded></item><item><title><![CDATA[DIY Damp Rid Alternative, Super Deluxe Size!]]></title><description><![CDATA[DampRid is great stuff for dealing with excess humidity. Lots of boaters (and hunters, and countless other folks) use it to keep less frequently used spaces fresh and free of mold and mildew. The only real downside is that it's expensive. A 3 pack of...]]></description><link>https://blog.murawsky.net/diy-damp-rid-alternative-super-deluxe-size</link><guid isPermaLink="true">https://blog.murawsky.net/diy-damp-rid-alternative-super-deluxe-size</guid><category><![CDATA[moisture]]></category><category><![CDATA[boat]]></category><category><![CDATA[DIY]]></category><dc:creator><![CDATA[Derek Murawsky]]></dc:creator><pubDate>Tue, 16 Jul 2024 14:41:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/1qeTU99G07Q/upload/f7cef0a8f19d101b81f898028894a4d9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>DampRid is great stuff for dealing with excess humidity. Lots of boaters (and hunters, and countless other folks) use it to keep less frequently used spaces fresh and free of mold and mildew. The only real downside is that it's expensive. A 3 pack of bags is $20, and they're not that big either. If you want to go large, the 4lb bucket is around $32 on Amazon as of this writing... So it got me wondering, what is this stuff?</p>
<p>The <a target="_blank" href="https://damprid.com/wp-content/uploads/2018/02/DampRid-Moisture-Absorbers-12-03-2019-SDS.pdf">Material Safety Data Sheet for DampRid</a> has the answer! The miracle crystals are primarily... Calcium Chloride... Basically, de-icing salt. The same de-icing salt that you can buy at big box stores in the winter for $1/lb or less.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721137684693/630db3e5-6e8e-4eb0-8ca7-40ab91f2943c.png" alt="Material Safety Data Sheet Screenshot showing that Damp Rid is 80-100% calcium chloride with the other portions being sodium chloride and potassium chloride." class="image--center mx-auto" /></p>
<p>I'm aware that I don't <em>need</em> to go big or go home. After all, the 4 lb bucket is good for 1,000 square feet, according to the manufacturer. Though I argue that they don't work quite that well due to air flow and all that, a couple of the smaller ones would probably work fine for a simple sailboat. So why look at rolling your own? Well, I like the idea of a larger reservoir. When I used the commercial ones, they would fill up quickly and needed to be checked every week or two. Although I like to think I will get to the boat frequently, I know that reality is often different. Plus, I like to tinker. So here's what I came up with...</p>
<h1 id="heading-super-ultra-mega-damprid-alternative">Super Ultra Mega DampRid Alternative</h1>
<p>Or SUMDRA, for short. Really rolls of the tongue, right? 😅 It's simply a 5 gallon bucket with a mesh sieve on top to hold the ice melt. This lets the user put in as much ice melt as they want, and have a large reservoir to catch all the moisture.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721138795227/06511887-fb46-4011-9821-743bc278b88c.jpeg" alt class="image--center mx-auto" /></p>
<p>For this build, I used a <a target="_blank" href="https://www.homedepot.com/p/The-Home-Depot-5-Gallon-Orange-Homer-Bucket-05GLHD2/100087613">Home Depot bucket</a> (about $5) and a <a target="_blank" href="https://www.amazon.com/dp/B003EDZOTM">600 micron plastic mesh</a> ($27!!!) designed for 5 gallon bucket. Yes, the mesh was expensive. I couldn't find anything plastic that had fine enough holes for the small salt crystals. Next time, I might try cheese cloth or similar in a regular plastic colander, but for now, this is it. Just remember, these crystals are highly corrosive, so make sure whatever you get is plastic. I wouldn't even trust stainless in this situation.</p>
<p>As for the salt itself, the key is to look for one that is mostly Calcium Chloride. Normally, this wouldn't be a problem. However, right now we're in the middle of a heat wave and most stores don't stock ice melt in the summer. You should have seen the look on the guy's face when I asked...</p>
<p>Thankfully, in many cases, you can order it and have it shipped to the store for free. Use their websites to check availability and ingredients, but call ahead to confirm availability if they say there are some in stock. I know it may come as a surprise, but the internet lies, and availability is often mis-reported. I settled on <a target="_blank" href="https://www.homedepot.com/p/Snow-Joe-20-lb-94-Pure-Calcium-Chloride-Ice-Melt-Pellets-MELT20CPP/311687848">Snow Joe 20 lb. 94% Pure Calcium Chloride Ice Melt Pellets</a>. Be sure to read the details! One of the larger bags from the same manufacturer had a very similar label, but was mostly Sodium Chloride.</p>
<h1 id="heading-results">Results</h1>
<p>So, how does it work? I have no idea! It's the middle of summer, and I'm waiting on my ice melt to get delivered. 😆 It should be in next week. I plan on throwing it in the bucket and moving everything to the boat after that. So stay tuned for updates!</p>
]]></content:encoded></item><item><title><![CDATA[Boat Plan: Detailed Next Steps]]></title><description><![CDATA[Overall, the boat is in pretty good shape, or so it seems to me. Reminder: I'm very new to boat ownership - This is my first! After giving it a thorough once over, I've worked out the following...
Airflow
Unfortunately, the docks at the lake do not h...]]></description><link>https://blog.murawsky.net/boat-plan-detailed-next-steps</link><guid isPermaLink="true">https://blog.murawsky.net/boat-plan-detailed-next-steps</guid><category><![CDATA[Sailing]]></category><category><![CDATA[repair]]></category><dc:creator><![CDATA[Derek Murawsky]]></dc:creator><pubDate>Mon, 01 Jul 2024 20:00:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/P0ou_dHBb0I/upload/1f4423cc31d018ad6af78b490690ffa9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Overall, the boat is in pretty good shape, or so it seems to me. Reminder: I'm very new to boat ownership - This is my first! After giving it a thorough once over, I've worked out the following...</p>
<h2 id="heading-airflow">Airflow</h2>
<p>Unfortunately, the docks at the lake do not have power, so I will have to rely on passive ventilation for now. My plan is to get a <a target="_blank" href="https://www.amazon.com/dp/B074MGD8XL/">Marinco Solar Powered Vent Fan</a> to install in the front hatch. In order to ensure good flow, I'm considering adding a 4" <a target="_blank" href="https://www.amazon.com/dp/B09Z35BQK7/?coliid=I1J4C6VOCV0F4C&amp;colid=2YSQXNPH12T8B&amp;ref_=list_c_wl_lv_ov_lig_dp_it&amp;th=1">stainless vent</a> to the companionway door, but I want to confirm with someone more knowledgeable that that isn't a bad idea.</p>
<p>Additionally, for the winter, I plan on adding a dehumidifier to run 24/7, with a condensate pump to get all the water out of the boat. I just need to figure out a good way to get the hose out of the main compartment without letting more water <em>in</em>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719859354361/7dc2de06-d8ad-4b1e-9f57-38c4aa0fc775.png" alt="A collage of various airflow treatments for the boat. Two vents, a solar powered vent fan, and a dehumidifier." class="image--center mx-auto" /></p>
<h2 id="heading-cleaning">Cleaning</h2>
<p>There will be a lot of cleaning involved. This boat seems to have sat closed for at least a season. I pulled all the soft goods out and it looks like the vinyl cabin cushions are salvageable. The others can likely be saved for a season or two, but ultimately I want to replace them.</p>
<p>For the interior cushions (corduroy &amp; vinyl), I used a combination of vinegar, detergent, and OxiClean to de-mold and freshen them. I tried this on some throw pillows that were in the cabin and it worked perfectly, so I think it will work well on the larger covers as well. For the foam, I plan to use diluted vinegar and lots of sunshine to try and freshen them.</p>
<p>The exterior cushions are covered in vinyl. I did try a vinyl cleaner on them, but it wouldn't get out the stains from the mold/mildew, so I decided to use some bleach spray on them. I know this is generally not a thing you want to do to vinyl, but I think it is worth it once to establish a baseline. I did try this on one cushion and it had no negative effects and got most of the staining out. After this, I will stick to vinyl cleaner.</p>
<h2 id="heading-replace-the-mast-step">Replace the Mast Step</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719858266174/9e476a38-6203-4899-8851-392ef56b9698.jpeg" alt="Image of a mast step from a Capri 22 in good condition, courtesy of Catalina Direct." class="image--center mx-auto" /></p>
<p>Thankfully, the <a target="_blank" href="https://www.catalinadirect.com/shop-by-boat/capri-18/rigging/standing-rigging/mast-step-c-22-cp-22cp-18-wwelded-vang-loop/">mast step</a> is a fairly inexpensive part. I ordered it from Catalina Direct as well as <a target="_blank" href="https://www.catalinadirect.com/shop-by-boat/catalina-22/rigging/standing-rigging/mast-step-fastener-kit-86-gt/">new bolts</a> and <a target="_blank" href="https://www.catalinadirect.com/shop-by-boat/catalina-38/hull-deck/midship/polyurethane-sealant-high-strength-adhesive-5200/">sealant</a>. They should get here in a week or two. I also got some flexible marine butyl tape and may end up using either or both. Still trying to decide. All in, it's 'bout another hundred...</p>
<h2 id="heading-what-else">What else?</h2>
<p>Again, I'm far from an expert here. I joined and reached out to <a target="_blank" href="https://nockamixonsailclub.org/">The Nockamixon Sail Club</a> to see if anyone with experience would be willing to come by and do a once-over on the boat with me to see what I missed. Hopefully someone will volunteer. I've offered bribes in the form of Pastured Poultry and unskilled/skilled labor.</p>
]]></content:encoded></item><item><title><![CDATA[A New Boat]]></title><description><![CDATA[I have long had a dream of sailing around the world and exploring its various cultures, cuisines, and vistas. However, such grand dreams usually fail to launch unless you actually start. I've been thinking about buying a boat for nearly 20 years - wa...]]></description><link>https://blog.murawsky.net/a-new-boat</link><guid isPermaLink="true">https://blog.murawsky.net/a-new-boat</guid><category><![CDATA[Sailing]]></category><category><![CDATA[new project]]></category><dc:creator><![CDATA[Derek Murawsky]]></dc:creator><pubDate>Fri, 28 Jun 2024 20:50:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1719607747205/25986711-8f44-42a6-bc95-185c608703af.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have long had a dream of sailing around the world and exploring its various cultures, cuisines, and vistas. However, such grand dreams usually fail to launch unless you actually start. I've been <em>thinking</em> about buying a boat for nearly 20 years - watching YouTube videos, reading articles and magazines, all while longing over what I saw. Over the past year and a half I looked at dozens of boats in various price ranges and conditions, but kept finding reasons that they wouldn't work for me. Then I realized that I will likely never find "the perfect one", and I really need to just get something and start moving on my dreams, or they would never happen. So that's what I did.</p>
<h2 id="heading-the-boat">The Boat</h2>
<p>I came across an ad on Facebook Marketplace for a 1988 <a target="_blank" href="https://sailboatdata.com/sailboat/capri-22-catalina/">Catalina Capri 22</a> sailboat with a wing keel, roller-furler, and gps/fish finder, and some other extras. Catalina makes solid boats, and the capri is one of them. This particular boat was about a half hour away right next to <a target="_blank" href="https://www.dcnr.pa.gov/StateParks/FindAPark/NockamixonStatePark/Pages/default.aspx">Lake Nockamixon</a>, which was where I have a dry slip waiting, and needed a little bit of love. Her mast step got bent in an unfortunate accident, but everything else seemed to be in good condition... aside from that "classic" boat smell of course. So after a brief inspection, we agreed on a price and I bought her and brought her home. Her name is Cykada (for now), and she's now dry-docked on my RV pad.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719605922404/a60451ac-2cde-40f0-ab82-a7748564fcbb.jpeg" alt="Sailboat on a trailer in front of a red barn." class="image--center mx-auto" /></p>
<h2 id="heading-the-immediate-plan">The Immediate Plan</h2>
<p>All great dreams start with a plan, so here is mine.</p>
<ol>
<li><p>Get everything out of the boat that isn't bolted down ✅</p>
</li>
<li><p>Clean the ever-loving daylights out of it</p>
</li>
<li><p>Address airflow &amp; moisture to prevent new mold issues</p>
</li>
<li><p>Replace the mast step</p>
</li>
<li><p>Figure out what other things she needs to be ready for actual sailing</p>
</li>
</ol>
<p>That's it! At least, for now. I spent the evening yesterday getting all the generic stuff inside the boat out and sorted. She's spending today open with a fan trying to dry her out as much as possible. All the soft goods are out in the sun to see if there is any hope of saving some of them. This evening I'll be bringing the wet/dry shop vac up to get rid of any remaining standing water.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719605268574/311096ce-24b8-4f2e-af02-f3b61074a4cb.jpeg" alt="Fan blowing into open hatch on a boat." class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719605685524/6d0de1ec-909f-44f4-a9d8-2e9815b81093.jpeg" alt="Bent metal mast plate and mounting bolts" class="image--center mx-auto" /></p>
<h2 id="heading-mid-range-plans">Mid-range Plans</h2>
<p>The immediate goal is to get her into good enough condition to get on the lake this season. Once I actually start sailing, I'm sure I'll find countless things I want to add and tweak. On my radar, in no particular order, are the following:</p>
<ul>
<li><p><strong>Solar Fan</strong> - This is probably part of the short term plan, but it's important enough to list twice. Airflow prevents mold. I want airflow. Interestingly, in order for air to flow, it needs an inlet... and the boat doesn't currently have one that I can tell. So part of this will be adding some kind of inlet.</p>
</li>
<li><p><strong>Remote Monitoring</strong> - I love to tinker, and this is a great excuse to mix my Home Automation and Embedded Controls hobbies with water. I think remote temp/humidity and location tracking are immediate goals for this little side project, but with Open Source projects like <a target="_blank" href="https://openmarine.net/openplotter">OpenPlotter</a>, the sky is the limit.</p>
</li>
<li><p><strong>Electrical Upgrade</strong> - The boat has a new battery, but the panel and most of the wiring is original. I think I want to add a quick disconnect on the battery (Anderson style connector), as well as add a solar charger / battery maintainer. After that, replacing the panel (modern chargers instead of cigarette style ones), lights (LEDs please), and maybe even adding more charge ports around.</p>
</li>
<li><p><strong>New Cushions</strong> - I know next to nothing about sewing, so this would likely involve a lot of bribes to my lovely wife. The existing cushions are quite literally falling apart, and are full of mold. We could try to salvage them, but replacement is probably the way.</p>
</li>
<li><p><strong>Bottom Paint</strong> - This is more like general maintenance, but the bottom could definitely use a new coat of paint. When I do this, I'll likely increase the scope to include any repairs needed for the gelcoat and fiberglass.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1719605736146/18c9ce18-2acc-4c77-a778-bd04640c171e.jpeg" alt="Various cushions and cushion covers spread out on the ground to air out" class="image--center mx-auto" /></p>
<h2 id="heading-long-range-plans">Long-range Plans</h2>
<ul>
<li><p><strong>Cruising Vacations</strong> - If this lifestyle suits me and the family, there will be many trips to tropical locals to do more sailing on larger boats. My dream is to sail the world, but it takes baby steps to get there when you've got a family that you love.</p>
</li>
<li><p><strong>Get a Bigger Boat</strong> - This is a great boat for Lake Nockamixon, but it's not a great boat for multi-day cruises or vacations. Getting a bigger boat would unlock more aspects of sailing, and might even add a "beach house" to the vacation options. Only time will tell</p>
</li>
</ul>
<h2 id="heading-closing-thoughts">Closing Thoughts</h2>
<p>Though I sailed a bit as a kid, I'm very much new to the boating lifestyle. If you have any thoughts, suggestions, or comments, please reach out and let me know. Going forward, I'm going to document the work on the boat here in the hopes of helping others who are interested see that this is a somewhat accessible hobby as long as you are willing to work at it.</p>
]]></content:encoded></item><item><title><![CDATA[IslandBridge Dock(er)s and (z)Waves]]></title><description><![CDATA[In the previous article in the series, I briefly went over what I wanted to set up: a Z-Wave and Zigbee to Home Assistant bridge. That was two years ago. As often happens, I just dove in and didn't take many notes along the way. However, I can say wi...]]></description><link>https://blog.murawsky.net/islandbridge-dockers-and-zwaves</link><guid isPermaLink="true">https://blog.murawsky.net/islandbridge-dockers-and-zwaves</guid><category><![CDATA[zwave]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Raspberry Pi]]></category><category><![CDATA[Home Assistant]]></category><category><![CDATA[zigbee]]></category><category><![CDATA[Traefik]]></category><dc:creator><![CDATA[Derek Murawsky]]></dc:creator><pubDate>Sat, 06 Jan 2024 05:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/nKtTubtxf6g/upload/v1658862032015/pYl3qrFNm.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://derekmurawsky.hashnode.dev/islandbridge">previous article</a> in the series, I briefly went over what I wanted to set up: a Z-Wave and Zigbee to Home Assistant bridge. That was two years ago. As often happens, I just dove in and didn't take many notes along the way. However, I can say with certainty that this has worked very well for the last two years! Below are the configs that I have been using.</p>
<h2 id="heading-zigbee2mqtt-docker-composeyaml">Zigbee2mqtt docker-compose.yaml</h2>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3.8'</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">zigbee2mqtt:</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">koenkk/zigbee2mqtt</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./data:/app/data</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/run/udev:/run/udev:ro</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">TZ=America/New_York</span>
    <span class="hljs-attr">devices:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'/dev/serial/by-id/usb-dresden_elektronik_ingenieurtechnik_GmbH_ConBee_II_DE2400118-if00:/dev/ttyACM0'</span>
    <span class="hljs-attr">labels:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.zigbee2mqtt.rule=Host(`zigbee2mqtt.internal.murawsky.net`)"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.services.zigbee2mqtt.loadbalancer.server.port=8080"</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">web</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">web:</span>
    <span class="hljs-attr">external:</span> <span class="hljs-literal">true</span>
</code></pre>
<h2 id="heading-zwavejs2mqtt-docker-composeyaml">zwavejs2mqtt docker-compose.yaml</h2>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">"3.7"</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">zwavejs2mqtt:</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">zwavejsui</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">zwavejs/zwave-js-ui:latest</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">tty:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">stop_signal:</span> <span class="hljs-string">SIGINT</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ZWAVEJS_EXTERNAL_CONFIG=/usr/src/app/store/.config-db</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">TZ=America/New_York</span>
    <span class="hljs-attr">env_file:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./secrets.env</span>
    <span class="hljs-attr">devices:</span>
      <span class="hljs-comment"># Do not use /dev/ttyUSBX serial devices, as those mappings can change over time.</span>
      <span class="hljs-comment"># Instead, use the /dev/serial/by-id/X serial device for your Z-Wave stick.</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'/dev/serial/by-id/usb-Silicon_Labs_Zooz_ZST10_700_Z-Wave_Stick_0001-if00-port0:/dev/zwave'</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">zwave-config:/usr/src/app/store</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"3000:3000"</span> <span class="hljs-comment"># port for Z-Wave JS websocket server</span>
    <span class="hljs-attr">labels:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.zwavejs2mqtt.rule=Host(`zwavejs2mqtt.internal.murawsky.net`)"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.services.zwavejs2mqtt.loadbalancer.server.port=8091"</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">web</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">web:</span>
    <span class="hljs-attr">external:</span> <span class="hljs-literal">true</span>

<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">zwave-config:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">zwave-config</span>
</code></pre>
<h2 id="heading-traefik-docker-composeyaml">Traefik docker-compose.yaml</h2>
<p>Both services exist behind the traefik reverse proxy, also running via docker.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3'</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">reverse-proxy:</span>
    <span class="hljs-comment"># The official v2 Traefik docker image</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">traefik:v2.8</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-comment"># Enables the web UI and tells Traefik to listen to docker</span>
    <span class="hljs-comment"># --serverstransport.insecureskipverify=true</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">--api.insecure=true</span> <span class="hljs-string">--providers.docker</span> <span class="hljs-string">--metrics.prometheus=true</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-comment"># The HTTP port</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"80:80"</span>
      <span class="hljs-comment"># The Web UI (enabled by --api.insecure=true)</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"8080:8080"</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-comment"># So that Traefik can listen to the Docker events</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/var/run/docker.sock:/var/run/docker.sock</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">web</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">web:</span>
    <span class="hljs-attr">external:</span> <span class="hljs-literal">true</span>
</code></pre>
<h2 id="heading-closing-thoughts">Closing Thoughts</h2>
<p>Better late than never! 😆 This system has worked very well with almost no hiccups at all. It integrated easily with Home Assistant as well.</p>
]]></content:encoded></item><item><title><![CDATA[IslandBridge]]></title><description><![CDATA[Sometimes it seems like home automation protocols are isolated islands, preventing devices from being truly useful. Sure, you can find integrations for the big services, but what about the obscure devices? I'm a fan of interoperability and local cont...]]></description><link>https://blog.murawsky.net/islandbridge</link><guid isPermaLink="true">https://blog.murawsky.net/islandbridge</guid><category><![CDATA[Docker]]></category><category><![CDATA[zwave]]></category><category><![CDATA[zigbee]]></category><category><![CDATA[Home Assistant]]></category><category><![CDATA[Traefik]]></category><dc:creator><![CDATA[Derek Murawsky]]></dc:creator><pubDate>Mon, 25 Jul 2022 18:43:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/AOFlegU1eTs/upload/v1658858436201/0iW58O0ex.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sometimes it seems like home automation protocols are isolated islands, preventing devices from being truly useful. Sure, you can find integrations for the big services, but what about the obscure devices? I'm a fan of interoperability and local control wherever reasonable. It's not that I'm against Alexa's or other cloud controlled systems, but I try to keep them where they can't do that much damage if they stopped working. As such, I embrace <a target="_blank" href="https://www.home-assistant.io/">Home Assistant</a> and the more open protocols where-ever possible. So when the opportunity came up to redo my automation setup and introduce a lot more controllable tech into my life, I jumped on it. This is the first post in a series on how I implemented my Home Assistant &lt;-&gt; Z-Wave/Zigbee bridge.</p>
<h1 id="heading-the-opportunity">The Opportunity</h1>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658858705641/X-Y8ERgZE.jpeg" alt="Kitchen Demolition" /></p>
<p>Here's the opportunity: my kitchen as it stands now... It's a bare canvas! With everything open like this, and new fixtures being selected, it's the perfect time to add a serious dose of control-ability into my home.</p>
<h1 id="heading-goals">Goals</h1>
<p>My goals for this project are fairly simple: I want a device that can bridge my Zigbee and Z-Wave devices to Home Assistant via MQTT. I want it to be stable, relatively secure, and easy to maintain. Now, just because the goals are simple, doesn't mean the implementation will be... Because, to me, stability and ease of maintenance means putting it in Docker containers... And that brings some  complexity with it. </p>
<p>Further, I want this to be a stable platform that I can build on over time. My kids love RGB LEDs and other cool tech, so having a reliable home control network is important to nurture that love going forward. </p>
<h1 id="heading-devices">Devices</h1>
<p>What devices do I want to support, exactly? Here's a list, with handy Amazon links. They're not affiliate links yet, though. I'm not sure if I want to get into that whole world or not, yet.  </p>
<ul>
<li><a target="_blank" href="https://www.amazon.com/dp/B0151Z8ZQY">Aeotec Multisensor 6</a> - A Z-Wave multisensor for motion, temperature, humidity, light, UV, and vibration sensing. For outdoor entry ways (protected areas).</li>
<li><a target="_blank" href="https://www.amazon.com/dp/B07D1CRRVF">Aqara motion sensor</a> - A fantastic, <em>tiny</em> little motion sensor. For discrete interior motion detection.</li>
<li><a target="_blank" href="https://www.amazon.com/dresden-elektronik-ConBee-Universal-Gateway/dp/B07PZ7ZHG5/">Conbee 2</a> - A Zigbee controller USB stick to interface with the Zigbee network from a Raspberry Pi.</li>
<li><a target="_blank" href="https://www.amazon.com/dp/B07GNZ56BK">S2 Stick 700</a> - A Z-Wave controller USB stick to interface with the Z-Wave network from a Raspberry Pi.</li>
<li><a target="_blank" href="https://www.amazon.com/dp/B09482M85Y">Zooz ZEN72</a> - A Z-Wave dimmer switch to control all new and existing light fixtures.</li>
<li><a target="_blank" href="https://www.amazon.com/dp/B09B6S4TSL">Zooz ZEN32</a> - A Z-Wave switch with one switch button and four scene buttons to control outdoor lights and provide convenient scene selection capabilities for the entire first floor. </li>
<li><a target="_blank" href="https://www.amazon.com/dp/B01AKSO80O">Zooz ZSE40</a> - A Z-Wave motion/light/temp/humidity sensor for interior entry ways to detect a person coming into the home.</li>
</ul>
<h1 id="heading-more-to-come">More to come</h1>
<p>In the next post in this series, I'll dive in and configure docker and zwavejs2mqtt on an old Raspberry Pi 3 that I had lying around. I don't have controllable devices installed, yet, but I can at least start getting some signal data.</p>
]]></content:encoded></item><item><title><![CDATA[Setting up zsh with Oh My Zsh, NerdFonts, and StarShip]]></title><description><![CDATA[I thought I would share a quick write up on how I set up the shell on my MacBook Pro. Here's what I'm going to review in this post:

Set up Oh my Zsh
Install a NerdFont
Set up Starship

In terms of terminal emulators, I like the good old reliable iTe...]]></description><link>https://blog.murawsky.net/setting-up-zsh-with-oh-my-zsh-nerdfonts-and-starship</link><guid isPermaLink="true">https://blog.murawsky.net/setting-up-zsh-with-oh-my-zsh-nerdfonts-and-starship</guid><category><![CDATA[shell]]></category><category><![CDATA[terminal]]></category><category><![CDATA[zsh]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[Developer]]></category><dc:creator><![CDATA[Derek Murawsky]]></dc:creator><pubDate>Thu, 07 Jul 2022 17:46:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1657215722853/-x3PDVZFy.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I thought I would share a quick write up on how I set up the shell on my MacBook Pro. Here's what I'm going to review in this post:</p>
<ul>
<li>Set up <a target="_blank" href="https://ohmyz.sh/">Oh my Zsh</a></li>
<li>Install a <a target="_blank" href="https://www.nerdfonts.com/">NerdFont</a></li>
<li>Set up <a target="_blank" href="https://starship.rs/">Starship</a></li>
</ul>
<p>In terms of terminal emulators, I like the good old reliable <a target="_blank" href="https://iterm2.com/">iTerm2</a>. If you don't already have it, you can install it easily with <a target="_blank" href="https://brew.sh/">homebrew</a> <code>brew install --cask iterm2</code>.</p>
<h1 id="heading-set-up-oh-my-zsh">Set up Oh My Zsh</h1>
<p><a target="_blank" href="https://ohmyz.sh/">Oh my Zsh</a> has a scripted installer available, so it's really simple to get it installed. Open up iTerm2 and run <code>sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"</code> 
<strong>Note:</strong> You should definitely check the install script before running it!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657202586183/sn866akmF.png" alt="iTerm2 install OhMyZSH" /></p>
<p>Great we're installed! But, what's this? Some of my old installed apps aren't working (like NVM)? Oh My Zsh overwrites your <code>~/.zshrc</code> file, so you will have to edit it to add your missing configuration back in. Your original config was backed up by the installer to <code>~/.zshrc.pre-oh-my-zsh</code>. While you're editing your <code>~/.zshrc</code>, it's also a good idea to read through all the new options that were added. Once you're done with your edits, restart your terminal and you're off!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657203520984/L4FC0K2pj.png" alt="image.png" /></p>
<p>Finally, if you ever want to uninstall Oh My Zsh, just run <code>uninstall_oh_my_zsh</code> from your terminal. It will reset your old <code>~/.zshrc</code> file, too. </p>
<h2 id="heading-plugins">Plugins</h2>
<p>One of the big reasons I like Oh My Zsh is their <a target="_blank" href="https://github.com/ohmyzsh/ohmyzsh/wiki/Plugins-Overview">absolutely massive list of plugins</a>. It's easy to enable too many and forget what you even have loaded (and slow down your shell as well!). The ones I use are <a target="_blank" href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/aliases">aliases</a>, <a target="_blank" href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/aws">aws</a>, <a target="_blank" href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/git">git</a>, <a target="_blank" href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/macos">macos</a>, <a target="_blank" href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/npm">npm</a> &amp; <a target="_blank" href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/nvm">nvm</a>. If I dive into specific areas, like docker/k8s, I'll add additional plugins as needed and then remove them when I'm done. </p>
<h1 id="heading-install-a-nerdfont">Install a NerdFont</h1>
<p>Next up is installing a <a target="_blank" href="https://www.nerdfonts.com/">NerdFont</a>. What is a NerdFont? From the website:</p>
<blockquote>
<p>Nerd Fonts patches developer targeted fonts with a high number of glyphs (icons). Specifically to add a high number of extra glyphs from popular ‘iconic fonts’ such as Font Awesome, Devicons, Octicons, and others.</p>
</blockquote>
<p>Essentially, this means you get a font library with a ton of extras that you can use in the console, or anywhere else. So head over to <a target="_blank" href="https://www.nerdfonts.com/font-downloads">the download area</a> and grab a font that you like. I like <a target="_blank" href="https://www.programmingfonts.org/#firacode">FiraCode</a>. And since I have brew, I install it with <code>brew tap homebrew/cask-fonts &amp;&amp; brew install --cask font-&lt;FONT NAME&gt;-nerd-font</code>. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657212184359/ln8EGU1kC.png" alt="NerdFont install via brew" /></p>
<p>And, yes, you may need to figure out the font name... Next time, remember, you can search with <code>brew search --casks &lt;PART OF FONT NAME&gt;</code> to take the guesswork out. </p>
<p>To use the font as your primary in your terminal, open the profiles editor, edit your default profile, and select the NerdFont you downloaded. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657212536674/6k7LRcT1Z.png" alt="Open iTerm2 profiles" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657212654124/D4XRKoKo4.png" alt="Change font to FiraCode" /></p>
<h1 id="heading-install-starship">Install StarShip</h1>
<p>The last thing to do is set up <a target="_blank" href="https://starship.rs/">StarShip</a>, a fast cross-shell prompt written in <a target="_blank" href="https://www.rust-lang.org/">rust</a>. Using homebrew, the install couldn't be simpler. Just <code>brew install starship</code>. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657213082074/873Dbl1jW.png" alt="Brew installing StarShip" /></p>
<p>Once installed, you can <code>echo 'eval "$(starship init zsh)"' &gt;&gt; ~/.zshrc</code> to get it to load in your shell every time. Now we have a great prompt, with useful feedback and near infinite customizability. Here is an example of the default when you're in a node project (CDK app in this case). And if you're curious about anything in your prompt, a quick <code>starship explain</code> will give you some really useful information. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657213482286/asJS3YcW-.png" alt="Starship Example" />
<a target="_blank" href="https://cdn.hashnode.com/res/hashnode/image/upload/v1657213376040/jfWX1mng4.png">image.png</a></p>
<h1 id="heading-other-customizations">Other Customizations</h1>
<p>There are a lot of other little extras that I add in, but the basics are above. I'm a fan of the setup used in the <a target="_blank" href="https://starship.rs/faq/#what-is-the-configuration-used-in-the-demo-gif">examples of StarShip</a>. </p>
<ul>
<li><a target="_blank" href="https://github.com/zsh-users/zsh-autosuggestions">zsh-autosuggestions</a> - For some nice autosuggestion display functionality.</li>
<li><a target="_blank" href="https://github.com/zsh-users/zsh-syntax-highlighting">zsh-syntax-highlighting</a> - for those long, complicated command lines with lots of escapes and special characters.</li>
<li><a target="_blank" href="https://github.com/sindresorhus/iterm2-snazzy">iterm2-snazzy</a> - For a nice color scheme in iTerm2. </li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Be Kind to Your Future Self with Conventional Commits and ADRs]]></title><description><![CDATA[Over the years I have adopted a few practices in my coding life that have really paid dividends. When I introduce the ideas to new teams, I always put it under the guise of being kind to your future self and "engineering excellence", though the latte...]]></description><link>https://blog.murawsky.net/be-kind-to-your-future-self-with-conventional-commits-and-adrs</link><guid isPermaLink="true">https://blog.murawsky.net/be-kind-to-your-future-self-with-conventional-commits-and-adrs</guid><category><![CDATA[Developer]]></category><category><![CDATA[Devops]]></category><category><![CDATA[best practices]]></category><dc:creator><![CDATA[Derek Murawsky]]></dc:creator><pubDate>Tue, 17 May 2022 17:28:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/842ofHC6MaI/upload/v1652708047324/jHKoT6VpZ.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Over the years I have adopted a few practices in my coding life that have really paid dividends. When I introduce the ideas to new teams, I always put it under the guise of being kind to your future self and "engineering excellence", though the latter doesn't always get as much traction as the former. In this article, I will go into two practices that I've adopted that came up in a recent conversation. </p>
<h2 id="heading-conventional-commits">Conventional Commits</h2>
<p>When it comes to commit messages, there are <a target="_blank" href="https://www.freecodecamp.org/news/how-to-write-better-git-commit-messages/">many</a> <a target="_blank" href="https://robertcooper.me/post/git-commit-messages">opinions</a> <a target="_blank" href="https://itnext.io/ten-commandments-of-git-commit-messages-94bd6dcf6e0e">on</a> what makes a good one and why. Most folks will agree, though, that a simple one line commit is far from ideal. I've adopted the <a target="_blank" href="https://www.conventionalcommits.org/en/v1.0.0/">Conventional Commits</a> standard with a <em>required body</em> that describes why I made a change, rather than just describing the change I made. Thank you <a target="_blank" href="https://cbea.ms/git-commit/">Chris Beams</a> for that idea. Here's the standard message format in a nutshell, pulled from the <a target="_blank" href="https://www.conventionalcommits.org/en/v1.0.0/#summary">Conventional Commits summary</a>: </p>
<pre><code><span class="hljs-operator">&lt;</span><span class="hljs-keyword">type</span><span class="hljs-operator">&gt;</span>[optional scope]: <span class="hljs-operator">&lt;</span>description<span class="hljs-operator">&gt;</span>

[optional body]

[optional footer(s)]
</code></pre><blockquote>
<p>The commit contains the following structural elements, to communicate intent to the consumers of your library:</p>
<ol>
<li>fix: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in Semantic Versioning).</li>
<li>feat: a commit of the type feat introduces a new feature to the codebase (this ?&gt;correlates with MINOR in Semantic Versioning).</li>
<li>BREAKING CHANGE: a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any type.</li>
<li>types other than fix: and feat: are allowed, for example @commitlint/config-conventional (based on the the Angular convention) recommends build:, chore:, ci:, docs:, style:, refactor:, perf:, test:, and others.</li>
<li>footers other than BREAKING CHANGE:  may be provided and follow a convention similar to git trailer format.</li>
</ol>
<p>Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic Versioning (unless they include a BREAKING CHANGE). A scope may be provided to a commit’s type, to provide additional contextual information and is contained within parenthesis, e.g., feat(parser): add ability to parse arrays.</p>
</blockquote>
<h3 id="heading-benefits">Benefits</h3>
<p>This approach has many advantages when working with a team or on a complex project. </p>
<p>Firstly, when something breaks it is easy to not only find our who made a change but also <strong><em>why</em></strong> they made the change. That context can be invaluable, and you no longer have to break your flow to go look up an old ticket reference that's hopfully still valid. Couple that message with a tool like <a target="_blank" href="https://gitlens.amod.io/">GitLens</a>, which lets you quickly and easily see git commit messages in your IDE, and your flow is even cleaner.</p>
<p>Next up, this aligns beautifully with <a target="_blank" href="https://semver.org/">Semantic Versioning</a> and allows for more automation around releases and version numbering. Fixes map to patches. Features map to minor increments. Breaking changes map to major. There is even some good automation already built to support this not only in your release flow, but also in your pre-commit stage. <a target="_blank" href="https://medium.com/@jsilvax/automate-semantic-versioning-with-conventional-commits-d76a9f45f2fa">This post</a> from jsilvax over on Medium goes into this in more detail.</p>
<p>Finally, ticketting systems change. While you may have kept your issues in GitHub Issues, your organization may want to move to Jira. I would hope the teams in charge of that would migrate your issues over, but do you want to rely on that? It is my opinion that the <em>why</em> of your changes should live with your code, in you git commit messages. As long as you keep git as your VCS, these messages will always remain. </p>
<h2 id="heading-architectural-decision-records-adrs">Architectural Decision Records (ADRs)</h2>
<p>But can the context of all changes be clearly conveyed in a commit message? What if the change is part of a larger decision made by the team? Enter <a target="_blank" href="https://adr.github.io/">Architectural Decision Records</a> (ADRs). ADRs are a way to capture the <a target="_blank" href="https://en.wikipedia.org/wiki/Architecturally_significant_requirements">Architecturally significant</a> decisions made as part of a projects evolution. There are many formats, but the key is to capture what decision was made, why it was made, and who agreed to it. In addition, I like to capture the other options that were considered and why the final choice was made. I'm a fan of <a target="_blank" href="https://adr.github.io/madr/">Markdown Any Decision Records</a> for their flexibility and relative completeness.</p>
<p>By putting your ADRs in a subdirectory of your documentation, it becomes easy to reference as things move forward. Keeping this close to your code, instead of a separate system like Confluence, has many of the same advantages as those listed above for conventional commits, like ease of reference from comments/commits and the fact that it will always be along side your code even if external systems change.</p>
<p>A bonus feature of this approach is that if you use a static site generator, your ADRs can be published along with your normal documentation. I typically use MKDocs to generate a static site from the <code>/docs/</code> directory which includes ADRs. For an example, you can check out this <a target="_blank" href="https://github.com/derekmurawsky/ansible-derekmurawsky-general">reference repo</a> and the <a target="_blank" href="http://general.ansible.murawsky.net/ADRs/">static site that it generates</a>.</p>
]]></content:encoded></item></channel></rss>