<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://obtusenet.com/blog/" xml:lang="en">
  <title>Obtuse Networking Blog</title>
  <subtitle>Practical IT advice for small businesses from Obtuse Networking.</subtitle>
  <id>https://obtusenet.com/blog/</id>
  <link href="https://obtusenet.com/blog/" rel="alternate" type="text/html"/>
  <link href="https://obtusenet.com/blog/feed.xml" rel="self" type="application/atom+xml"/>
  <icon>https://obtusenet.com/img/favicon.svg</icon>
  <updated>2026-11-01T20:54:37Z</updated>
  <author><name>Michael Graves</name></author>
  <entry>
    <title>Running AI..</title>
    <link href="https://obtusenet.com/blog/running-ai.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/running-ai.html</id>
    <published>2026-10-21T00:00:00Z</published>
    <updated>2026-11-01T20:54:37Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>AI is finally becoming useful...</summary>
    <category term="ai"/>
    <content type="html">&lt;p&gt;AI is finally becoming useful... to me. When it was just a &amp;quot;chat&amp;quot; bot, I couldn&#39;t have cared less. And when it could spit out a python script that I had to copy and paste into an editor and test and debug myself... I still wasn&#39;t impressed. But now that AI can &amp;quot;use tools&amp;quot;, it is now becoming useful to me.&lt;/p&gt;
&lt;p&gt;I am not an AI pundit or an expert by any stretch of the imagination. I have not drunk the cool-aide and think AI will change everything. I&#39;m just some guy on the Internet and I do think things will change. So take these tips for what their worth.&lt;/p&gt;
&lt;p&gt;You do not need to follow or even understand everything here before starting with AI. You most likely will not. As you gain more experience revisit this page and what didn&#39;t make sense at first, may start to make more sense.&lt;/p&gt;
&lt;p&gt;Here are some of my &amp;quot;discoveries&amp;quot; when running AI.&lt;/p&gt;
&lt;h2 id=&#34;first-things-to-always-remember&#34;&gt;First things to always remember&lt;/h2&gt;
&lt;p&gt;Keep these items in mind when using AI.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;YOU ARE RESPONSIBLE!!!&lt;/strong&gt; And you will be held responsible. If you ask an AI to do a thing and it does it. Or you ask AI to do a thing and it does something totally different. You are responsible for any damage or outcome that happens. Never forget that.&lt;/li&gt;
&lt;li&gt;AI is like an &lt;em&gt;&lt;strong&gt;idiot savant&lt;/strong&gt;&lt;/em&gt; or &lt;em&gt;&lt;strong&gt;a stochastic parrot.&lt;/strong&gt;&lt;/em&gt; What AI can do is amazing, but AI is just a tool. It doesn&#39;t &lt;em&gt;&amp;quot;know&amp;quot;&lt;/em&gt; anything. It doesn&#39;t &lt;em&gt;&amp;quot;understand&amp;quot;&lt;/em&gt; anything.&lt;/li&gt;
&lt;li&gt;AI uses human language, so it can seem human. It is &lt;strong&gt;&lt;!-- raw HTML omitted --&gt;NOT&lt;!-- raw HTML omitted --&gt;&lt;/strong&gt;. AI is, at its core, a large (really large) multidimensional array. The fact that you can put words in and get words out something that make sense and can do things is like magic... but its just technology. Remember what &lt;a href=&#34;https://en.wikipedia.org/wiki/Arthur_C._Clarke&#34;&gt;Arthur C Clark&lt;/a&gt; said &amp;quot;any sufficiently advanced technology is indistinguishable from magic&amp;quot;. Right now, AI is like that.&lt;/li&gt;
&lt;li&gt;Hardware prices are crazy for running models yourself (Oct &#39;26). Start with a monthly subscriptions to one of the &amp;quot;frontier&amp;quot; companies if you have never used AI. Once you get some of the basics down, you&#39;ll probably want try a different harness than the &amp;quot;frontier&amp;quot; company harness. Then you will start with a site like &lt;a href=&#34;https://ollama.com/&#34;&gt;ollama.com&lt;/a&gt;, &lt;a href=&#34;https://huggingface.co/&#34;&gt;huggingface.co&lt;/a&gt;, &lt;a href=&#34;https://nousresearch.com/&#34;&gt;nousresearch.com&lt;/a&gt;, &lt;a href=&#34;https://openrouter.ai/&#34;&gt;openrouter.a&lt;/a&gt;i, etc and be able to select from a bunch of different models.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;general-usage-guides&#34;&gt;General usage guides&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Harness matters a lot more than the model. A good harness with a marginal model can give you good results. A bad harness with a great model can also give you good results. The difference is the great model probably costs 3x more than the marginal model. Result matter.&lt;/li&gt;
&lt;li&gt;Don&#39;t get hung up on the &#39;latest and greatest&#39; model released. Find a model that can do what you want for the price you want to pay.&lt;/li&gt;
&lt;li&gt;You need to have an idea / goal first. Take as much time as you need to refine that goal. Ask AI to help refine that goal (which is, in itself, a goal). But get the goal for AI clearly defined first.&lt;/li&gt;
&lt;li&gt;Remember &amp;quot;if you don&#39;t know where you are going, any road will get you there... lost&amp;quot;. So it is with AI. AI will give you an answer. It will most likely not be 100% correct, but it &lt;em&gt;may&lt;/em&gt; be close enough, but how can you tell? Always double check the answer.&lt;/li&gt;
&lt;li&gt;Running multiple sub-agents &lt;strong&gt;may&lt;/strong&gt; get things done faster, but &lt;strong&gt;will&lt;/strong&gt; cost a many more tokens because every time sub-agent is spun up you will path the whole ingest tax. For long projects this can really add up. For most jobs, stick with one model.&lt;/li&gt;
&lt;li&gt;For repeated activities (or ones you think will be repeated) have the model create scripts that both it and you can run. This will allow you to do the activity without the model.&lt;/li&gt;
&lt;li&gt;Getting clever with prompts is usually not worth the effort. Todays models (as of Oct &#39;26) are really good at interpreting your intent from your questions. With that said, you still need to be clear on what the goal of the task is and any parameters that the model should align to. Giving a &#39;one shot&#39; prompt is a gimmick.&lt;/li&gt;
&lt;li&gt;When prompting do:&lt;br&gt;
a) Set a &lt;!-- raw HTML omitted --&gt;clear goal&lt;!-- raw HTML omitted --&gt; for the model. Define when the task is complete and the model is done.&lt;br&gt;
b) Provide any parameters you want it to follow or adhere to.&lt;br&gt;
c) Explicitly tell AI what you don&#39;t want it to do. Know that it may still ignore you and do it anyway. So make sure you have a backup way to prevent it.&lt;br&gt;
d) Tell the model how you expect it to save its progress. This will also help with the model being able to &amp;quot;remember&amp;quot; where it left off.&lt;br&gt;
e) Ask the model to repeat back to you or summarized its understanding of the ask before doing any work.&lt;br&gt;
f) Have AI create a plan before any work is done so you can review the plan. And &lt;strong&gt;do&lt;/strong&gt; the review. This will keep both you and the model aligned.&lt;br&gt;
g) Explicitly tell the model to interview you or ask you questions for clarifications.&lt;br&gt;
h) Tell the model to record the assumptions that it makes in its history. This way you can review it.&lt;/li&gt;
&lt;li&gt;Don&#39;t chase parameters, quantization, test suites, etc. Find a model that works for you and what you want to do. Build your own series of &amp;quot;tests&amp;quot; and what &amp;quot;good&amp;quot; looks like. Compare models to that.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;application-tips&#34;&gt;Application Tips&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Basically all application development habits and process you use today are what we want the model to use.&lt;/li&gt;
&lt;li&gt;Make sure the model creates a Makefile or other standard build file for the application. This way you can make changes and/or swap out the models and still receive consistent results.&lt;/li&gt;
&lt;li&gt;Always tell the model to initialize a new git project in the local working directory. Even if you don&#39;t intend to have the project be replicated anywhere else. This provide history of the work and the models generally provide really good summaries of what each commit provides.&lt;/li&gt;
&lt;li&gt;For each feature or step in the plan, have the model create a new branch to do the work and when complete and tested, merge that branch back into main.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;final-thoughts&#34;&gt;Final thoughts...&lt;/h2&gt;
&lt;p&gt;None. I will continue to update this list for myself as time goes on. Hopefully this will be helpful for you.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>All Aboard! The Upgrade Train</title>
    <link href="https://obtusenet.com/blog/all-aboard-the-upgrade-train.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/all-aboard-the-upgrade-train.html</id>
    <published>2026-10-14T13:57:44Z</published>
    <updated>2026-10-14T13:57:44Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>(brought to you by AI...</summary>
    <content type="html">&lt;p&gt;&lt;em&gt;(brought to you by AI... I mean the constant upgrades... not the article... that was all me... and my spell checker)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As IT professional(s), I (we) have been on the constant upgrade cycle. So much so, Microsoft created a monthly &#39;holiday&#39; where we get &#39;gifts&#39; from them.&lt;/p&gt;
&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Different IT environments get different levels of tolerance for upgrades/patching. For Microsoft servers, there is usually an agreed upon window of time where &#39;upgrades&#39; are done and the business knows to &#39;take a break&#39;. However, additional IT environments are trying to join those Microsoft &#39;take a break&#39; windows and a complete mess is ensuing. Storage patches need to go into place impacting the actual server storage the Microsoft servers rely on. And network upgrades are impacting both of them, leading to situation of:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;No network,&lt;br&gt;
No storage,&lt;br&gt;
No servers...&lt;br&gt;
No business&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;AI hasn&#39;t made that easier. Besides the flood of &#39;slop&#39; patches, AI is actually finding real bugs and real vulnerabilities. But AI is doing it at a rate that neither the software developers nor the business users can handle.&lt;/p&gt;
&lt;h2 id=&#34;is-there-an-answer&#34;&gt;Is there an Answer?&lt;/h2&gt;
&lt;p&gt;Honestly, not without a lot of work.&lt;/p&gt;
&lt;p&gt;I&#39;ve heard some people floated the idea that &#39;&lt;em&gt;&lt;strong&gt;we just need to get thru this period and eventually all the bugs will be found and we will have perfect software&lt;/strong&gt;&lt;/em&gt;&#39;. In a static environment, that &lt;em&gt;may&lt;/em&gt; be true. But who lives/works in a static environment? I think that there are several things that are working against this idea:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Developer fatigue: Researching and understanding a bug and then developing a patch and patching a software is not an instant process. It takes time. Time no one has. With the amount of reports coming in, we have a choice... drowned or fight AI with AI. Call in the life line of AI to evaluate, propose, and patch the bugs. How soon will it be before we just press the &#39;I believe&#39; button and tell AI... just go do it? How soon will it be before we can no longer reason about the software?&lt;/li&gt;
&lt;li&gt;The AI models keep getting better and finding more kinds of bugs. More reports, etc. etc. The key here is AI&#39;s ability isn&#39;t fixed.&lt;/li&gt;
&lt;li&gt;Model availability: Or the lack there of. Not all models are available to everyone. Many models have been restricted to just some &#39;elite&#39; companies for special purposes. Everyone else... you just get the scraps. Good Luck!&lt;/li&gt;
&lt;li&gt;Model affordability: At some point in time the full cost of running models will come due. And those companies and/or individuals that cannot afford to pay, will not be allowed to play. At the risk of speaking something into existence... how soon we will start seeing &#39;Go Fund Me&#39; campaigns for tokens?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&#39;s assume that none of those items manifests itself. Where does that leave us, the IT pro, who is trying to do the best we can, not having to upgrade the literal entire IT environment every... month, week, day? Here are my suggestions...&lt;/p&gt;
&lt;h3 id=&#34;short-term-the-business-as-it-is&#34;&gt;Short term: the business, as it is...&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;The business will need to have more &#39;downtime&#39; in a month. If server patches are done on Sunday, then Storage patches are done on Saturday and Network (inclusive of firewall, WAFs, proxies, etc) patches are done Friday.&lt;/li&gt;
&lt;li&gt;We will be introducing a lot of change. We will want to develop some good &#39;canary testing&#39; methods to be reasonably sure when an upgrade is complete, everything is &#39;as we expect it&#39;.&lt;/li&gt;
&lt;li&gt;If you are in a business whose size makes a single patching day per area impractical, then that business will need to have more patching days per month. Maybe geographically related. (East coast on first week, West cost third week)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There will be pushback. But remind those that push back that patching systems doesn&#39;t prevent the business from making money... it protects the business so that it can continue to make money. Think of it like an insurance. Bad stuff &lt;em&gt;will&lt;/em&gt; happen. This is you paying your premiums so that &lt;em&gt;when&lt;/em&gt; it does happen, the impact can be recovered. If that is too nuanced, just think of it like this... It&#39;s just the cost of doing business.&lt;/p&gt;
&lt;h3 id=&#34;long-term-the-business-as-it-will-need-to-become&#34;&gt;Long term: the business, as it will need to become...&lt;/h3&gt;
&lt;p&gt;The business (and IT), will need to transform its thinking. Instead of relying on servers, and storage, and network we need to start thinking in terms of &lt;em&gt;services&lt;/em&gt;. No business buys a server or storage or network gear just for that gears sake. (IT geeks excluded) That gear is purchased to provide a service. We need to begin thinking about those services. Service is important. Servers are not.&lt;/p&gt;
&lt;h3 id=&#34;how-do-we-do-this&#34;&gt;How do we do this?&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Assume failure:&lt;/strong&gt; Any server, storage, network device will fail. We have developed processes to try and weed out those failures early and to minimize them... but they do and will continue happen. Let us not &#39;hope they don&#39;t happen&#39;. Let us assume they will and have a plan.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stop worrying about the shortest path:&lt;/strong&gt; What is the shortest path between your machine and Google? Microsoft? You don&#39;t know. And you don&#39;t need to know. So stop trying to optimize it. The only thing our users care about is they &lt;em&gt;can&lt;/em&gt; get to the service they desire, when they desire them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Re-architect applications:&lt;/strong&gt; You &lt;!-- raw HTML omitted --&gt;will&lt;!-- raw HTML omitted --&gt; need to re-architect the application the business relies on. Just shoving the servers into a &amp;quot;cloud&amp;quot; and calling it &amp;quot;cloud enabled&amp;quot; is a lie. We can no longer lie to ourselves or allow ourselves to be lied to. What does re-architect mean? In a nutshell it means:&lt;br&gt;
a) separating your data from its presentation layer (where your customers, employees, etc. interact with the data).&lt;br&gt;
b) re-structuring how the data is organized so that it can be easily chunked up and replicated across physical locations.&lt;br&gt;
c) and while we are at it, we need to plan in how we backup &lt;!-- raw HTML omitted --&gt;and&lt;!-- raw HTML omitted --&gt; restore the data when it is corrupted or lost.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;Does this mean I &lt;!-- raw HTML omitted --&gt;must&lt;!-- raw HTML omitted --&gt; move to the cloud and pay the cloud tax?&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;No. You will need &lt;del&gt;some&lt;/del&gt; multiple places to put servers, storage, network hardware in our environment. It also means you will need start thinking of your server rooms and data centers like the cloud providers do. Modular-ize. Container-ize.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;Now that I&#39;ve done all this work what does my business get out of it. I still have to patch, upgrade, etc.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Correct. But now your application is setup to be &amp;quot;always on&amp;quot;. Now your users can work from the office or their home or anywhere.&lt;/p&gt;
&lt;h2 id=&#34;and-now-were-back-to&#34;&gt;And now we&#39;re back to...&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;How do I re-architect my application(s)?&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Well... AI. Can AI build a perfect application? No. AI can however, with good prompting, planning, validation and oversight, take an &lt;del&gt;ancient&lt;/del&gt; historic application, and rebuild it in a modern, safe fashion using today&#39;s tools and methods. Slop is still possible. But maybe, a slop application is all that is needed for a specific service. Today&#39;s AI can build more than just slop. It can really customize an application for your specific business. Instead of what we all have done for the last 30 years which is change our business around the applications we could afford to rent.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion?!?&lt;/h2&gt;
&lt;p&gt;Instead of being stuck on the track and being run over by all the fallout of AI... can we use this time of change to help ourselves, our businesses, our families, achieve a better outcome?&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Automatic Certificate Issuance with ACME</title>
    <link href="https://obtusenet.com/blog/automatic-certificate-issuance-with-acme.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/automatic-certificate-issuance-with-acme.html</id>
    <published>2026-07-13T11:47:22Z</published>
    <updated>2026-07-13T11:47:22Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>In a previous article here, I described how to setup an ACME server using Step-CA software.</summary>
    <content type="html">&lt;p&gt;In a previous article &lt;a href=&#34;https://obtusenet.com/blog/self-hosted-episode-4-certificates.html&#34;&gt;here&lt;/a&gt;, I described how to setup an ACME server using Step-CA software. While writing that article I got tired fighting with some of the Step-CA nuances and asked myself if there was a better way.&lt;/p&gt;
&lt;p&gt;Not finding a better way to issue certificates using the ACME protocol, I created &lt;a href=&#34;https://github.com/mgraves00/ACME.cgi&#34;&gt;ACME.cgi&lt;/a&gt;. This is a CGI script that will utilize an external CA tool to issue/revoke certificates using the ACME protocol (and yes... it&#39;s a shell script!).&lt;/p&gt;
&lt;h2 id=&#34;overview&#34;&gt;Overview&lt;/h2&gt;
&lt;p&gt;The ACME.cgi script will utilize a standard web sever and CGI environment to allow ACME clients to register and issue certificates for domains. The ACME.cgi script does not actually do the work of creating, signing, revoking certificates. Those operations are farmed out to external tools.&lt;/p&gt;
&lt;p&gt;This article, and ACME.cgi in general, assume that the external tools is &lt;a href=&#34;https://github.com/mgraves00/pca&#34;&gt;Personal Certificate Authority (PCA)&lt;/a&gt;. However, this is not required.&lt;/p&gt;
&lt;h1 id=&#34;setup&#34;&gt;Setup&lt;/h1&gt;
&lt;p&gt;Since servers and requirements vary greatly, the options below give the steps to deploy ACME.cgi into a Docker, non-chroot and chroot web environment. Please note, that I cannot know your specific server setup and you may need to make additional adjustments.&lt;/p&gt;
&lt;p&gt;These setups all assume that the ACME.cgi service will utilize the Personal Certificate Authority (PCA) script to manage certificate functions. (If you want more information on what PCA is checkout &lt;a href=&#34;https://obtusenet.com/blog/be-your-own-ca.html&#34;&gt;this article&lt;/a&gt;.) If you choose to utilize another tool, update/replace the ACME_helper.* files with whatever makes sense for your setup.&lt;/p&gt;
&lt;h3 id=&#34;for-all-options&#34;&gt;For All Options&lt;/h3&gt;
&lt;p&gt;The following will need to be done for all deployment options.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download the latest version of ACME.cgi from &lt;a href=&#34;https://github.com/mgraves00/ACME.cgi&#34;&gt;github&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Download the latest version of PCA from &lt;a href=&#34;https://github.com/mgraves00/pca&#34;&gt;github&lt;/a&gt;.
&lt;ol&gt;
&lt;li&gt;Copy the &lt;em&gt;pca&lt;/em&gt; script to a location in the PATH. i.e. /usr/local/bin/. If you will setup ACME.cgi in a CHROOT environment, then this path must be callable from that environment.&lt;/li&gt;
&lt;li&gt;Since these instruction assume that PCA will be called from the ACME_helper script, set the PCA_ROOT environment variable to the location where the ACME.cgi service is expected to store the certificate environment. i.e. /var/www/etc/pca.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Install a FastCGI server. e.g. &lt;a href=&#34;https://github.com/gnosek/fcgiwrap&#34;&gt;fcgiwrap&lt;/a&gt;, &lt;a href=&#34;https://man.openbsd.org/slowcgi.8&#34;&gt;slowcgi&lt;/a&gt;. This program is probably already in the package manager of the server.&lt;/li&gt;
&lt;li&gt;Setup web server to send traffic to CGI file. Below is a quick config for nginx. This configlet assumes the following (adjust for your setup):
&lt;ol&gt;
&lt;li&gt;The path ACME.cgi will be called from is https://ISSUER_DOMAIN/acme/ (where ISSUER_DOMAIN is the FQDN of the server hosting the application)&lt;/li&gt;
&lt;li&gt;The web root is /var/www/html&lt;/li&gt;
&lt;li&gt;Web server certificates are kept in /etc/certs&lt;/li&gt;
&lt;li&gt;FastCGI program is listening on localhost port 9000&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;server {
  listen 443 ssl;
  listen [::]:443 ssl;

  ssl_certificate /etc/certs/server.fullchain.pem;
  ssl_certificate_key /etc/certs/server.key;
# use if the server key is encrypted
#  ssl_password_file /etc/certs/server.pass;
  ssl_protocols   TLSv1.2 TLSv1.3;
  ssl_ciphers     HIGH:!aNULL:!MD5;

  index index.html;
  root /var/www/html;

  location /acme/ACME.cgi {
    include /etc/nginx/fastcgi_params;
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param SCRIPT_NAME nfquery.cgi;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;nginx server configlet&lt;/em&gt;&lt;/p&gt;
&lt;h1 id=&#34;deployment-options&#34;&gt;Deployment Options&lt;/h1&gt;
&lt;h3 id=&#34;on-docker-system&#34;&gt;On Docker system&lt;/h3&gt;
&lt;p&gt;This is the simplest deployment method. There is a Makefile created to assist in creation of the docker container. Three volumes are used, &lt;strong&gt;/config&lt;/strong&gt;, &lt;strong&gt;/data&lt;/strong&gt; and &lt;strong&gt;/logs&lt;/strong&gt;. Config is used to store the configuration files for ACME.cgi and the web server. Data is where PCA certificates are stored as well as the ACME working files. Logs as the name implies, is where the logs are stored.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Change to the package/Docker path.&lt;/li&gt;
&lt;li&gt;Build the docker image by running &lt;em&gt;make build&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;make build
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&#34;3&#34;&gt;
&lt;li&gt;Copy the &lt;strong&gt;env.example&lt;/strong&gt; file to &lt;strong&gt;.env&lt;/strong&gt;. Configure the &lt;strong&gt;.env&lt;/strong&gt; file as necessary. Note: Set SERVER_NAME, CONTACT_EMAIL and SERVER_SECRET for the site.&lt;/li&gt;
&lt;li&gt;Start the docker container. Use option A or B
&lt;ol&gt;
&lt;li&gt;Start docker via &lt;em&gt;make run&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Start docker-container via &lt;em&gt;docker-container up -d&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The docker container will create a new CA, create and export a CA directory, create certificate and keys for the container and enable all cron scripts.&lt;/p&gt;
&lt;h3 id=&#34;on-non-chroot-systems&#34;&gt;On non-chroot systems&lt;/h3&gt;
&lt;p&gt;This is only slightly more complicated that the docker image. In this option, all configuration steps will need to be performed manually.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copy the &lt;strong&gt;ACME.cgi&lt;/strong&gt; &lt;strong&gt;ACME_helper.sh&lt;/strong&gt; and &lt;strong&gt;ACME_cleaner.sh&lt;/strong&gt; files to the CGI-BIN directory.&lt;/li&gt;
&lt;li&gt;Copy the ACME.conf example to the /etc directory.
&lt;ol&gt;
&lt;li&gt;Update the &lt;strong&gt;ACME.conf&lt;/strong&gt; file as necessary. Note that the ISSUER_DOMAIN and ISSUER_EMAIL do not have default values and are required to be filled out.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;ACME_helper.conf&lt;/strong&gt; file to /etc directory.
&lt;ol&gt;
&lt;li&gt;Set the CA_NAME to be the name to initialize PCA with (or the name that PCA was initialized with, if done in another step.)&lt;/li&gt;
&lt;li&gt;Optional: If initializing PCA here run the PCA init command. These commands will:
&lt;ol&gt;
&lt;li&gt;Initialize the PCA environment using the CA_NAME that was described above.&lt;/li&gt;
&lt;li&gt;Create a certificate chain&lt;/li&gt;
&lt;li&gt;Create a CRL file (nothing will be in it yet)&lt;/li&gt;
&lt;li&gt;Export an html directory with those files in them.&lt;/li&gt;
&lt;li&gt;Extract that html export into a web root under a /ca directory.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;PCA_ROOT=/etc/pca pca &amp;lt;ca_name&amp;gt; init
PCA_ROOT=/etc/pca pca &amp;lt;ca_name&amp;gt; create chain
PCA_ROOT=/etc/pca pca &amp;lt;ca_name&amp;gt; create crl
PCA_Root=/etc/pca pca &amp;lt;ca_name&amp;gt; export html -file /tmp/ca_html.tgz -overwrite
tar -C /var/www/html -zxf /tmp/ca_hml.tgz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to create the CA HTML files on the CA server&lt;/em&gt;&lt;/p&gt;
&lt;ol start=&#34;4&#34;&gt;
&lt;li&gt;Verify that everything is working by connecting to &lt;strong&gt;https://ISSUER_DOMAIN/acme/directory&lt;/strong&gt;. This will return a directory listing of ACME functions and confirm the script is installed.&lt;/li&gt;
&lt;li&gt;Setup the &lt;strong&gt;ACME_cleaner.sh&lt;/strong&gt; script to run daily.&lt;/li&gt;
&lt;li&gt;Create a script and set it to run daily that will create a new CRL, HTML export and deploy that export.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh
export PCA_ROOT=/etc/pca
export CANAME=&amp;lt;ca_name&amp;gt;
pca ${CANAME} create crl
pca ${CANAME} export html -file /tmp/ca_html.tgz -overwrite
tar -C /var/www/html -zxf /tmp/ca_html.tgz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;script to maintain the CRL on the CA server&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;on-chroot-systems&#34;&gt;On CHROOT systems&lt;/h3&gt;
&lt;p&gt;This is perhaps the most complicated setup. If the system is running on OpenBSD, check out the packages/OpenBSD directory. There is a port that can be run to create an OpenBSD port to deploy. In addition read packages/OpenBSD/pkg/README. That file has all command necessary to copy the helper programs into the CHROOT environment.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copy the ACME.cgi, ACME_helper.sh and ACME_cleaner.sh files to the CGI-BIN directory.&lt;/li&gt;
&lt;li&gt;Copy the required helper programs into the CHROOT directory (Assume the directory &lt;strong&gt;/var/www/etc&lt;/strong&gt;)
&lt;ol&gt;
&lt;li&gt;The following directories are used and will need to be created in the CHROOT environment: &lt;strong&gt;/tmp&lt;/strong&gt;, &lt;strong&gt;/var/run&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The following programs are required to be accessible by the CHROOT environment, as well as any libraries that the programs call: &lt;strong&gt;cat&lt;/strong&gt;, &lt;strong&gt;sleep&lt;/strong&gt;, &lt;strong&gt;rm,&lt;/strong&gt; &lt;strong&gt;cut&lt;/strong&gt;, &lt;strong&gt;mktemp&lt;/strong&gt;, &lt;strong&gt;host&lt;/strong&gt;, &lt;strong&gt;curl&lt;/strong&gt;, &lt;strong&gt;fold&lt;/strong&gt;, &lt;strong&gt;od&lt;/strong&gt;, &lt;strong&gt;openssl&lt;/strong&gt;, &lt;strong&gt;printf&lt;/strong&gt;, &lt;strong&gt;wc&lt;/strong&gt;, &lt;strong&gt;jq&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The following device is required to be available in the CHROOT environment: &lt;strong&gt;/dev/null&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;ACME.conf&lt;/strong&gt; into a config directory located in the CHROOT path. i.e &lt;strong&gt;/var/www/etc&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;Update the &lt;strong&gt;ACME.conf&lt;/strong&gt; file as necessary. Note that the ISSUER_DOMAIN and ISSUER_EMAIL do not have default values and are required to be filled out.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;ACME.conf&lt;/strong&gt; into a config directory located in the CHROOT path. i.e &lt;strong&gt;/var/www/etc&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;Update the &lt;strong&gt;ACME.conf&lt;/strong&gt; file as necessary. Note that the ISSUER_DOMAIN and ISSUER_EMAIL do not have default values and are required to be filled out.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;AMCE_helper.conf&lt;/strong&gt; into a config directory located in the CHROOT path. i.e. &lt;strong&gt;/var/www/etc&lt;/strong&gt;
&lt;ol&gt;
&lt;li&gt;Set the CA_NAME to be the name to initialize PCA with (or the name that PCA was initialized with, if done in another step.)&lt;/li&gt;
&lt;li&gt;Optional: If initialized PCA here run the PCA init command. Make sure to initialize from the CHROOT path.
&lt;ol&gt;
&lt;li&gt;Initialize the PCA environment using the CA_NAME that was described above.&lt;/li&gt;
&lt;li&gt;Create a certificate chain&lt;/li&gt;
&lt;li&gt;Create a CRL file (nothing will be in it yet)&lt;/li&gt;
&lt;li&gt;Export an html directory with those files in them.&lt;/li&gt;
&lt;li&gt;Extract that html export into a web root under a /ca directory. (see example below)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Verify that everything is working by connecting to &lt;strong&gt;https://ISSUER_DOMAIN/acme/directory&lt;/strong&gt;. This will return a directory listing of ACME functions and confirm the script is installed.&lt;/li&gt;
&lt;li&gt;Setup the ACME_cleaner.sh script to run daily.&lt;/li&gt;
&lt;li&gt;Create a script and set it to run daily that will create a new CRL, HTML export and deploy that export.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;PCA_ROOT=/etc/pca chroot /var/www pca &amp;lt;ca_name&amp;gt; init
PCA_ROOT=/etc/pca chroot /var/www pca &amp;lt;ca_name&amp;gt; create chain
PCA_ROOT=/etc/pca chroot /var/www pca &amp;lt;ca_name&amp;gt; create crl
PCA_Root=/etc/pca chroot /var/www pca &amp;lt;ca_name&amp;gt; export html -file /tmp/ca_html.tgz -overwrite
tar -C /var/www/html -zxf /var/www/tmp/ca_hml.tgz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;example initializing PCA in CHROOT path&lt;/em&gt;&lt;/p&gt;
&lt;h1 id=&#34;clients&#34;&gt;Clients&lt;/h1&gt;
&lt;p&gt;Before you begin selecting your favorite ACME client to pull a certificate, you will need to download and install PCA root certificate.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Browse to the https://ISSUER_DOMAIN/ca/ site. Accept any certificate errors for &amp;quot;unknown authority&amp;quot;.&lt;/li&gt;
&lt;li&gt;Download the Certificate Chain. It should be in a PEM format.&lt;/li&gt;
&lt;li&gt;Install that chain into the trusted certificate store on the client. (The process will vary depending on client)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now use your preferred ACME client to request and download a certificate.&lt;/p&gt;
&lt;h1 id=&#34;conclusions&#34;&gt;Conclusions&lt;/h1&gt;
&lt;p&gt;This is the first step in gaining independence from public certificate authorities.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Notes on Roundcube Mail</title>
    <link href="https://obtusenet.com/blog/notes-on-roundcube-mail.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/notes-on-roundcube-mail.html</id>
    <published>2026-07-08T15:15:01Z</published>
    <updated>2026-07-08T15:15:01Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>These are some notes for my future self on how to get data out of the roundcube mail database.</summary>
    <category term="notes"/>
    <category term="roundcube"/>
    <category term="howto"/>
    <content type="html">&lt;p&gt;These are some notes for my future self on how to get data out of the roundcube mail database. This assume sqlite3 database but the SQL should also work with MySQL or PostgreSQL databases as well.&lt;/p&gt;
&lt;h3 id=&#34;view-schema&#34;&gt;View schema&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sqlite3 rc_sqlite.db &amp;quot;.schema&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;cli for showing roundcube database schema&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;list-users&#34;&gt;List users&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sqlite3 rc_sqlite.db &amp;quot;select user_id,username from users;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;cli for listing roundcube users from database&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;export-contacts-in-vcard-format&#34;&gt;Export contacts in vcard format&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sqlite3 rc_sqlite.db &amp;quot;select vcard from contacts where user_id = NN;&amp;quot; &amp;gt; username_contacts.vcf

# where the NN is the user_id number for the list users output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;cli for extracting vcard contact information&lt;/em&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Be your own CA...</title>
    <link href="https://obtusenet.com/blog/be-your-own-ca.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/be-your-own-ca.html</id>
    <published>2026-04-01T13:42:03Z</published>
    <updated>2026-04-01T13:42:03Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>I have been running my own Certificate Authority (CA) for my systems for some time now.</summary>
    <content type="html">&lt;p&gt;I have been running my own Certificate Authority (CA) for my systems for some time now. While running my CA is nothing like running a large commercial CA, running a personal CA is surprisingly easy.&lt;/p&gt;
&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;When I first got started with becoming my own CA, it was in the early 2000&#39;s and the tools out there were nonexistent. There was basically just &lt;a href=&#34;http://www.openssl.org/&#34;&gt;openssl&lt;/a&gt; and the command line.&lt;/p&gt;
&lt;p&gt;This was well before things like &lt;a href=&#34;https://letsencrypt.org/&#34;&gt;Lets Encrypt&lt;/a&gt; existed and the only place to get certificates was to pay for them. I recognize the landscape is different now (in 2026) but some of my reasons are even more important now than they were then.&lt;/p&gt;
&lt;p&gt;My main reason for running my own CA was I didn&#39;t want to be forced to trust an external authority for my systems. Have you looked at the list of Certificate Authorities you browser and/or OSes trust by default? Fully trust. By default. Have a look, read through the names. I&#39;m sure that 95% of these CAs you have never heard of, yet they are fully trusted. Any certificate issue by them for &lt;em&gt;&lt;strong&gt;&lt;!-- raw HTML omitted --&gt;ANY&lt;!-- raw HTML omitted --&gt;&lt;/strong&gt;&lt;/em&gt; domain will be fully trusted. I had a problem with this.&lt;/p&gt;
&lt;p&gt;My original goal was to be able to create a certificate authority and be able to sign certificate requests (CSR) using a minimum of applications. Fortunately, openssl was extremely versatile, if poorly documented.&lt;/p&gt;
&lt;p&gt;In order to preform all the CA related functions I started out with a set of Makefiles. These files would do almost everything, but were getting difficult to maintain as I added more features. Eventually I created a script to do the work. The script is a simple shell script and should run anywhere. This script can be &lt;a href=&#34;https://github.com/mgraves00/pca&#34;&gt;found here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;setup&#34;&gt;Setup&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Download the script from my &lt;a href=&#34;https://github.com/mgraves00/pca&#34;&gt;github&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Copy to any directory in the path.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That&#39;s it! The script is self contained. By default it will create a &lt;strong&gt;$HOME/.pca&lt;/strong&gt; directory. This can be overridden by specifying &lt;strong&gt;PCA_ROOT=/some/path&lt;/strong&gt; in your environment.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: A quick note about security. This program predates the availability and use of hardware security modules. It assumes that the filesystem that the CA files are stored in is secure. Maybe a future improvement will be to add the ability to use hardware security module capabilities.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;initialize-ca&#34;&gt;Initialize CA&lt;/h2&gt;
&lt;p&gt;To start, initialize the CA. This will setup structures and configs openssl will need to become a CA.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pca CANAME init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The setup will only need to be done once per CA. If you would like to have a second CA... just change &lt;em&gt;CANAME&lt;/em&gt; to anything else... maybe &lt;em&gt;2ndCA&lt;/em&gt;... and you have a second CA.&lt;/p&gt;
&lt;p&gt;To view all the commands and options, just run the &lt;em&gt;pca&lt;/em&gt; command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pca
pca 1.6 - Personal Certificate Authroity
pca &amp;lt;ca&amp;gt; &amp;lt;command&amp;gt; [options]
Commands:
  &amp;lt;ca&amp;gt; init
  &amp;lt;ca&amp;gt; config list &amp;lt;mac[ros] | pol[icy] | ext[ension] | prog[ram]&amp;gt;
  &amp;lt;ca&amp;gt; config get macro &amp;lt;-key key&amp;gt; &amp;lt;-value&amp;gt;
  &amp;lt;ca&amp;gt; config set macro [-a|-d] &amp;lt;-key key&amp;gt; &amp;lt;-value val&amp;gt;
  &amp;lt;ca&amp;gt; config get pol|ext|prog &amp;lt;-name name&amp;gt; &amp;lt;-key key&amp;gt; &amp;lt;-value&amp;gt;
  &amp;lt;ca&amp;gt; config set pol|ext|prog [-a|-d] &amp;lt;-name name&amp;gt; &amp;lt;-key key&amp;gt; &amp;lt;-value val&amp;gt;
  &amp;lt;ca&amp;gt; config sign &amp;lt;-name name&amp;gt;
  &amp;lt;ca&amp;gt; create root [-days #] [-bits #] [-cn name] [-ct x] [-sp x] [-co x] [-or x] [-ou x] [-em x] [ -san val ... ]
  &amp;lt;ca&amp;gt; create crl
  &amp;lt;ca&amp;gt; create key &amp;lt;-name name&amp;gt; [-bits #]
  &amp;lt;ca&amp;gt; create chain
  &amp;lt;ca&amp;gt; create req[uest] &amp;lt;-name name&amp;gt; &amp;lt;-key file | -newkey [#bits]&amp;gt; [-days #] [-cn name] [-co x] [-sp x] [-ct x] [-or x] [-ou x] [-em x] [ -san val ... ]
  &amp;lt;ca&amp;gt; show crl [-issuer] [-hash] [-fingerprint] [-num] [-last] [-next]
  &amp;lt;ca&amp;gt; show req [-name name]
  &amp;lt;ca&amp;gt; show root [-serial] [-expire] [-subject] [-subjecthash] [-issuer] [-issuerhash] [-fingerprint]
  &amp;lt;ca&amp;gt; show cert [-name name] [-serial] [-expire] [-subject] [-subjecthash] [-issuer] [-issuerhash] [-fingerprint] [-client] [-ca]
  &amp;lt;ca&amp;gt; sign &amp;lt;-name name&amp;gt; [-sign] [-days] [-san val ...] [-ext alt_extension]
  &amp;lt;ca&amp;gt; resign &amp;lt;-name name&amp;gt; [-sign] [-days]
  &amp;lt;ca&amp;gt; revoke &amp;lt;-name name | -serial #&amp;gt; [-crl]
  &amp;lt;ca&amp;gt; delete req|key|cert &amp;lt;-name name&amp;gt;
  &amp;lt;ca&amp;gt; export pkcs12 &amp;lt;-name name&amp;gt; &amp;lt;-file name&amp;gt; &amp;lt;-pass -|file:filename|password&amp;gt; [-chain] [-overwrite]
  &amp;lt;ca&amp;gt; export html &amp;lt;-file name&amp;gt; [-overwrite]
  &amp;lt;ca&amp;gt; export cert &amp;lt;-name name&amp;gt; &amp;lt;-file name&amp;gt; [-overwrite] [-chain]
  &amp;lt;ca&amp;gt; import pkcs12 &amp;lt;-name name&amp;gt; &amp;lt;-file name&amp;gt; &amp;lt;-pass -|file:filename|password&amp;gt; [-overwrite]
  &amp;lt;ca&amp;gt; import request &amp;lt;-name name&amp;gt; &amp;lt;-file name&amp;gt; [-overwrite]
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&#34;basics-uses&#34;&gt;Basics Uses&lt;/h1&gt;
&lt;p&gt;Below are the basics of creating and maintaining a CA.&lt;/p&gt;
&lt;h2 id=&#34;create-a-csr&#34;&gt;Create a CSR&lt;/h2&gt;
&lt;p&gt;To create a CSR and associated key, run the &lt;em&gt;create req&lt;/em&gt; command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pca CANAME create req -name CERT1 -newkey -cn CERT1 -san DNS=CERT1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will create a CSR and KEY setting the subject to CN=CERT1 and the Subject Alternative Name (SAN) to CERT1.&lt;/p&gt;
&lt;h2 id=&#34;show-unsigned-csrs&#34;&gt;Show unsigned CSRs&lt;/h2&gt;
&lt;p&gt;To show all unsigned CSRs run the &lt;em&gt;show req&lt;/em&gt; command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pca CANAME show req
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;sign-a-csr&#34;&gt;Sign a CSR&lt;/h2&gt;
&lt;p&gt;To sign a CSR run the &lt;em&gt;sign&lt;/em&gt; command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pca CANAME sign -name CERT1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will sign the CSR and create the certificate.&lt;/p&gt;
&lt;h2 id=&#34;show-signed-certificates&#34;&gt;Show signed Certificates&lt;/h2&gt;
&lt;p&gt;To show signed certificates run the &lt;em&gt;show cert&lt;/em&gt; command. It&#39;s important to note that unless a certificate is deleted, it will continue to show in this list regardless of its state (valid, expired or revoked)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pca CANAME show cert
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;revoke-a-certificate&#34;&gt;Revoke a certificate&lt;/h2&gt;
&lt;p&gt;If a certificate needs to be revoked because it expired or for any other reason, use the &lt;em&gt;revoke&lt;/em&gt; command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pca CANAME revoke -name CERT1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;resign-a-certificate&#34;&gt;Resign a certificate&lt;/h2&gt;
&lt;p&gt;It may not be desirable to just revoke a certificate and create a new request and then sign that request. That is a lot of steps! Fortunately for everyone I got tired of that dance and created a &lt;em&gt;resign&lt;/em&gt; command to resign an existing certificate and give it a new expiration date.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pca CANAME resign -name CERT1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;import-a-csr&#34;&gt;Import a CSR&lt;/h2&gt;
&lt;p&gt;There may come times when a CSR is created outside of the PCA program. In those cases, the CSR can be imported and then signed. Use the &lt;em&gt;import request&lt;/em&gt; command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pca CANAME import request -name EXTCERT2 -file EXTCERT2.csr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the certificate can be signed, resigned, revoked, etc like any other certificate/request.&lt;/p&gt;
&lt;h2 id=&#34;export-certificate-and-key&#34;&gt;Export certificate and key&lt;/h2&gt;
&lt;p&gt;Now that we can create a certificate (and key) we need a way to export them from the CA so they can be used on a device, server, etc. To do this the standard was of exporting a certificates and keys securely is to use a PKCS12 file. To do this use the &lt;em&gt;export pkcs12&lt;/em&gt; command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pca CANAME export pkcs12 -name CERT1 -file CERT1.p12 -pass a_better_password_should_be_choosen -chain
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will create a &lt;strong&gt;CERT1.p12&lt;/strong&gt; file with the certificate, key and certificate chain. These can be transferred to any system and exported using the &lt;em&gt;openssl pkcs12&lt;/em&gt; commands.&lt;/p&gt;
&lt;h3 id=&#34;export-the-certificate&#34;&gt;Export the certificate&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;openssl pkcs12 -in CERT1.p12 -nokeys -clcert -out CERT1.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;export-the-key&#34;&gt;Export the key&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;openssl pkcs12 -in CERT1.p12 -nocerts -nodes -out CERT1.key
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;export-the-chain&#34;&gt;Export the chain&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;openssl pkcs12 -in CERT1.p12 -nokeys -cacert -out CANAME_chain.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Depending on what system the PKCS12 files was created and what system the cert/key/chain is being extracted to the &lt;em&gt;-legacy&lt;/em&gt; flag may need to be specified.&lt;/p&gt;
&lt;h2 id=&#34;export-certificate-only&#34;&gt;Export Certificate (only)&lt;/h2&gt;
&lt;p&gt;In cases where the CSR was imported or where only the certificate needs to be exported, the PKCS12 format may not be necessary. To export only the certificate use the &lt;em&gt;export cert&lt;/em&gt; command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pca CANAME export cert -name EXTCERT2 -file EXTCERT2.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&#34;advanced-uses&#34;&gt;Advanced Uses&lt;/h1&gt;
&lt;p&gt;Openssl uses various files and configurations to run the CA. The &lt;em&gt;init&lt;/em&gt; command above created the defaults of these files. But for reasons of your own, you may need to update/change these default files. There are 4 types of files that get used: Macros, Policy, Extension, and Program files. To view the defaults for these files run the &lt;em&gt;config list &lt;!-- raw HTML omitted --&gt;&lt;/em&gt; command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pca CANAME config list prog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The files that you will probable change the most often are the policy and extension files as these control how openssl behaves based on different types of inputs.&lt;/p&gt;
&lt;p&gt;To make a change to one of the files use the &lt;em&gt;config set&lt;/em&gt; command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pca CANAME config set pol -name POLICY_NAME -key KEY_NAME -value VALUE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If a key needs to be added or deleted use the  &lt;em&gt;-a&lt;/em&gt; or &lt;em&gt;-d&lt;/em&gt; flags when calling the &lt;em&gt;set&lt;/em&gt; command.&lt;/p&gt;
&lt;h1 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Hopefully this article helps you consider running your own certificate authority and shows you how easy it can be.&lt;/p&gt;
&lt;p&gt;Feel free to provide me any comments, questions, or feedback.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Notes on Doveadm</title>
    <link href="https://obtusenet.com/blog/notes-on-doveadm.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/notes-on-doveadm.html</id>
    <published>2026-01-09T14:18:53Z</published>
    <updated>2026-03-26T16:53:24Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>Some quick notes for myself about using doveadm and managing mailboxes.</summary>
    <category term="notes"/>
    <category term="dovecot"/>
    <content type="html">&lt;p&gt;Some quick notes for myself about using doveadm and managing mailboxes. It should be no surprise, that if you are an admin with a server, you can read any email/files on that server.&lt;/p&gt;
&lt;p&gt;List users&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;doveadm user &#39;*&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;List mailboxes for user&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;doveadmm mailbox list -u &amp;lt;user&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Show messages in trash for user older than 30 days&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;doveadm search -u &amp;lt;user&amp;gt; mailbox Trash NOT SINCE 30days
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Delete messages in trash for all users older than 30 days&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;doveadm expunge -A mailbox Trash NOT SINCE 30days
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Delete messages from users mailbox older than 6 months and not flagged&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;doveadm expunge -u &amp;lt;user&amp;gt; mailbox &amp;lt;mailbox&amp;gt; NOT SINCE 26weeks NOT FLAGGED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output and users email&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;doveadm fetch -u &amp;lt;user&amp;gt; &amp;quot;text&amp;quot; MAILBOX &amp;lt;mailbox&amp;gt; UID &amp;lt;email_uid&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fix index on a mailbox&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;doveadm index -u &amp;lt;user&amp;gt; &amp;lt;mailbox&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sync users mailboxes with backup server&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;doveadm -v sync -u &amp;lt;user&amp;gt; -d
&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Self Hosted: Episode 8 - External Services</title>
    <link href="https://obtusenet.com/blog/self-hosted-episode-9-external-services.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/self-hosted-episode-9-external-services.html</id>
    <published>2025-10-25T05:00:30Z</published>
    <updated>2025-10-25T05:00:30Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>Software Used: * OpenBSD 7.7 Overview Now that we have some services in our self hosted environment, we may want to allow access from outside our environment.</summary>
    <category term="self-hosted"/>
    <category term="opensmtpd"/>
    <category term="smtp"/>
    <category term="relayd"/>
    <content type="html">&lt;p&gt;&lt;strong&gt;Software Used&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenBSD 7.7&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&#34;overview&#34;&gt;Overview&lt;/h1&gt;
&lt;p&gt;Now that we have some services in our self hosted environment, we may want to allow access from outside our environment. The question is how do we allow this connectivity for ourselves (and anyone else we choose) but not allow the world at large?&lt;/p&gt;
&lt;p&gt;To accomplish this we will setup a proxy server. But this will not be a standard proxy server. Our server will be setup to mutually authenticate incoming requests to the services we extend. More on the mutual authentication later.&lt;/p&gt;
&lt;p&gt;First we will need to setup a proxy host that will accept requests from the Internet and tunnel them to our self hosted firewall. Here are some of the services we will want to have access to from the Internet.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Email access (both sending and receiving)&lt;/li&gt;
&lt;li&gt;Web access to web based email client (proxied)&lt;/li&gt;
&lt;li&gt;IMAPS and SMTPS to the email server (proxied)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&#34;setup-vm-with-external-provider&#34;&gt;Setup VM with external provider&lt;/h1&gt;
&lt;p&gt;There are many providers on the Internet that will provide virtual machines. For this article I will use &lt;a href=&#34;https://www.vultr.com/?ref=8281514&#34;&gt;Vultr (referral link attached)&lt;/a&gt;. Your are free to use whatever hosting provider you deem best.&lt;/p&gt;
&lt;p&gt;Since this VM will be hosted directly on the Internet it will be exposed to all kinds of attacks. My preference for an operating system for used in hostel environments is OpenBSD. Fortunately, Vultr has an option to deploy an OpenBSD VM directly from their console.&lt;/p&gt;
&lt;p&gt;I cannot possibly get into tall the different nuances of hosting providers. The basic things to look for is:&lt;/p&gt;
&lt;p&gt;a) A variety of build options. This setup will not require huge amounts of CPU or memory. Recommendation is to get at least 1G (preferable 2G of memory). Any CPU speed is fine.&lt;/p&gt;
&lt;p&gt;b) Bandwidth. Are there caps. Do you pay for inbound and outbound usage? While email and general apps do not require lots of bandwidth, if you someday extend this self-hosted environment to host music or movies for yourself while you&#39;re on the road, bandwidth will spike.&lt;/p&gt;
&lt;p&gt;c) ability to set the PTR record. This is possibly the number one requirement when hosting email.&lt;/p&gt;
&lt;p&gt;After creating the VM and logging in for the first time there are several things we will want to change.&lt;/p&gt;
&lt;h3 id=&#34;create-the-local-user&#34;&gt;Create the local user&lt;/h3&gt;
&lt;p&gt;As with all the setups, I assume that there is a non-root user called &#39;&lt;strong&gt;alfred&lt;/strong&gt;&#39;. Good chance there is not a default user in the pre-canned OS install from the provider. If your build falls into this bucket create the &lt;strong&gt;alfred&lt;/strong&gt; user.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;useradd -g =uid -G wheel -k /etc/skel -s /bin/ksh -m alfred
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;create the &#39;alfred&#39; user&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;set-root-and-local-password&#34;&gt;Set Root and Local password&lt;/h3&gt;
&lt;p&gt;If you have logged in as a non-root user, change the users local password. Make it something that cannot be guessed. (We will disable password login via SSH, but the local password still needs to be a strong password.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ passwd
Changin the password for alfred
New Password: &amp;lt;This should be something really hard to guess!&amp;gt;
Retype new password:
passwd: password updated successfully
$ su -
Password:&amp;lt;enter root password here&amp;gt;
# passwd root
Changin the password for root
New Password: This should be something really hard to guess!
Retype new password:
passwd: password updated successfully
#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;password change example&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;banner&#34;&gt;Banner&lt;/h3&gt;
&lt;p&gt;Change the default banner. Blank it out, place a warning, whatever makes sense.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;lt;EOF&amp;gt;/etc/banner
This is a private system.
Go away!
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;script to update banner file.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;ssh-daemon-update&#34;&gt;SSH Daemon update&lt;/h3&gt;
&lt;p&gt;The following commands will set the banner sshd uses, change the port it listens on, deny root login, and disable password authentication(optional).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sed -i &#39;s/^#?Banner.*/Banner /etc/banner/&#39; /etc/ssh/sshd_config
sed -i &#39;s/^#?Port.*/Port 3721/&#39; /etc/ssh/sshd_config
sed -i &#39;s/^#?PermitRootLogin.*/PermitRootLogin no/&#39; /etc/ssh/sshd_config
sed -i &#39;s/^#?PasswordAuthentication.*/PasswordAuthentication no/&#39; /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the changes are made restart sshd. Note, do not exit your current SSH connection until you&#39;ve tested it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rcctl restart sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From another shell test the connection on a new port.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -p 3721 alfred@newserver.example.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the SSH connection doesn&#39;t work, check the config, fix and restart sshd again.&lt;/p&gt;
&lt;h3 id=&#34;setup-the-firewall&#34;&gt;Setup the firewall&lt;/h3&gt;
&lt;p&gt;Event though this device will be locked down and only run services we want, setting up the firewall to make sure that only the ports we want running are accessible is good practice.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ext=vio0
tun_port=5921

set skip on lo
set block-policy drop
set loginterface $ext
set syncookies adaptive ( start 25%, end 12% )
set limit states 400000
set limit table-entries 2000000

table &amp;lt;whitelist&amp;gt; persist file &amp;quot;/var/db/pf.whitelist&amp;quot;
table &amp;lt;banned&amp;gt; persist file &amp;quot;/var/db/pf.banned&amp;quot;
table &amp;lt;aggressive&amp;gt; persist file &amp;quot;/var/db/pf.aggressive&amp;quot;
table &amp;lt;sshguard&amp;gt; persist

block return
pass in quick on egress inet6 proto icmp6 all icmp6-type { routeradv neighbrsol neighbradv }
pass in quick on egress inet6 proto udp \
        from fe80::/10 port dhcpv6-server \
        to fe80::/10 port dhcpv6-client no state
pass in  quick on egress inet6 proto icmp6 from fe80::/10 to fe80::/10
pass in  quick on egress inet6 proto icmp6 from ff02::/16 to fe80::/10
pass out quick on egress inet6 proto icmp6 from fe80::/10 to ff02::/16
  
# allow traffic to/from wg
pass in on wg
pass out on wg nat-to (wg)

# allow ping
pass in  quick on egress inet6 proto icmp6 all icmp6-type echoreq
pass in  quick on egress inet  proto icmp all icmp-type echoreq

# pass in tunnel port
pass in proto udp to egress port $tun_port

# pass in ssh (remember port was changed to 3721)
pass in proto tcp to egress port 3721

# pass in http and https
pass in proto tcp to egress port { 80 443 }

# pass in email ports
pass in proto tcp to egress port { 25 465 }

# pass in imap ports
pass in proto tcp to egress port { 143 993 }

# allow us to send traffic out
pass out from self
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/pf.conf&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;update-hosts-file&#34;&gt;Update Hosts file&lt;/h3&gt;
&lt;p&gt;We will need to connect to the real IP&#39;s of the internal hosts. To do this we will override the hostnames by placing the real IPs into the hosts file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1 newserver.example.org
::1       newserver.example.org
192.168.30.14 sso sso.example.org
192.168.30.15 mail mail.example.org
192.168.30.11 certauth certauth.example.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/hosts&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We will also need to tell the OS resolver to look at the hosts file for resolution. To do this add this like to the &lt;strong&gt;/etc/resolv.conf&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lookup file bind
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;partial /etc/resolv.conf&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;update-dns-with-server-name&#34;&gt;Update DNS with server name&lt;/h3&gt;
&lt;p&gt;Update the external DNS provider with the name and IP of this server. We will also need to add the following records.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;example.org            A   add.server.ip.address
newserver.example.org  A   add.server.ip.address
mail.example.org   CNAME   newserver.example.org
smtp.example.org   CNAME   newserver.example.org
imap.example.org   CNAME   newserver.example.org
sso.example.org    CNAME   newserver.example.org
certauth.example.org CNAME newserver.example.org
autoconfig.example.org  CNAME newserver.example.org
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;get-certificate&#34;&gt;Get certificate&lt;/h3&gt;
&lt;p&gt;This server will have many aliases. Since it will be proxying traffic for all internal systems we choose to expose. Since this will be public facing server we may want a &amp;quot;real&amp;quot; public certificate. We will also need an internal certificate for communicating with the rest of the self-hosted environment. acme-client(1) can be used to get both certificates.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# basic HTTP config for certificate generation
server &amp;quot;*&amp;quot; {
        listen on * port 80
        location &amp;quot;/.well-known/acme-challenge/*&amp;quot; {
                root &amp;quot;/acme&amp;quot;
                request strip 2
        }
        location * {
                block return 302 &amp;quot;https://$HTTP_HOST$REQUEST_URI&amp;quot;
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next we will configure the acme-client.conf file to get the certificate from Lets Encrypt.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
authority letsencrypt {
  api url &amp;quot;https://acme-v02.api.letsencrypt.org/directory&amp;quot;
  account key &amp;quot;/etc/acme/letsencrypt-privkey.pem&amp;quot;
}

domain newserver.example.org {
  alternative names {
    mail.example.org
    smtp.example.org
    imap.example.org
    sso.example.org
    certauth.example.org
    autoconfig.example.org
    example.org
  }
  domain key &amp;quot;/etc/ssl/private/newserver.key&amp;quot;
  domain full chain certificate &amp;quot;/etc/ssl/newserver.fullchain.pem&amp;quot;
  sign with letsencrypt
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/acme-client.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We will also need an internal certificate to mutually authenticate clients. We will do they by creating a second acme config file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# example.org acme-internal.conf
#
authority example.org {
        api url &amp;quot;https://certauth.example.org/acme/acme/directory&amp;quot;
        account key &amp;quot;/etc/acme/example.org-privkey.pem&amp;quot;
}

domain newserver.example.org {
        domain key &amp;quot;/etc/ssl/private/newserver_internal.key&amp;quot;
        domain full chain certificate &amp;quot;/etc/ssl/newserver_internal.pem&amp;quot;
        sign with example.org
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/acme-internal.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now start httpd and retrieve the certificates.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rcctl enable httpd
rcctl start httpd
acme-client -v newserver.example.org
acme-client -v -f /etc/acme-internal.conf newserver.example.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to start httpd and issue new certificates&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We will need to periodically update the certificates. Update crontab with the following lines. Use the &#39;&lt;em&gt;crontab -e&lt;/em&gt;&#39; command to edit the crontab file. This will run the check/update every Sunday at a random time after midnight.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 0 * * 0  sleep $((RANDOM \% 2048)) &amp;amp;&amp;amp; acme-client newserver.example.org
0 0 * * 0  sleep $((RANDOM \% 2048)) &amp;amp;&amp;amp; acme-client -f /etc/acme-internal.conf newserver.example.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;crontab to update the certificates as needed&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;create-tunnel-from-firewall-to-newserver&#34;&gt;Create Tunnel from firewall to newserver&lt;/h3&gt;
&lt;p&gt;To create a tunnel we will need to update the configs on both the &lt;strong&gt;newserver&lt;/strong&gt; as well as the &lt;strong&gt;firewall&lt;/strong&gt; server. We will assume that the &lt;strong&gt;firewall&lt;/strong&gt; server does not have direct access to the Internet (because if it does, the article to this point is unnecessary). Due to the firewall having to pass through one or more NAT&#39;s we will setup the &lt;strong&gt;firewall&lt;/strong&gt; to initiate the connection and the &lt;strong&gt;newserver&lt;/strong&gt; to just listen for connections.&lt;/p&gt;
&lt;p&gt;We will be configuring these systems in parallel as we will need information for each server to complete the configuration on the other.&lt;/p&gt;
&lt;p&gt;First on both systems we will create a private key.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openssl rand -base64 32 &amp;gt; /etc/wg.pkey
chmod 640 /etc/wg.pkey
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;create and save wireguard private key.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&#34;new-interfaces-config&#34;&gt;New interfaces config&lt;/h4&gt;
&lt;p&gt;Now create a new &lt;strong&gt;/etc/hostname.wg0&lt;/strong&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wgkey `cat /etc/wg.pkey`
wgport 5921
#wgpeer ### some public key ### wgdescr firewall wgaip 0.0.0.0/0
inet 192.0.2.1 255.255.255.0
!route add -net 192.168.30.0/24 192.0.2.10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;newserver /etc/hostname.wg0&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Do the same thing on the &lt;strong&gt;firewall&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wgkey `cat /etc/wg.pkey`
# wgpeer ### some public key### wgendpoint newser.real.ip.address 5921 wgdescr wall wgaip 0.0.0.0/0
inet 192.0.2.10 255.255.255.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;firewall /etc/hostname.wg0&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now load the private keys. This will show us public keys.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/bin/sh /etc/netstart wg0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;load the new wireguard interface config&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once the interfaces are loaded get the public key.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ifconfig wg0 | wgpubkey
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to get the public key&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now replace the &lt;strong&gt;###some public key###&lt;/strong&gt; part of the configs with the opposite public key (also remove the leading # sign). i.e. &lt;strong&gt;newserver&lt;/strong&gt; public key goes into the &lt;strong&gt;firewall&lt;/strong&gt; &lt;strong&gt;/etc/hostname.wg0&lt;/strong&gt; config file and visa versa. Once the configs are updated reload the configs. Test pinging from &lt;strong&gt;firewall&lt;/strong&gt; server to the &lt;strong&gt;newserver&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;firewall# ping 192.0.2.1
PING 192.0.2.1 (192.0.2.1): 56 data bytes
64 bytes from 192.0.2.1: icmp_seq=0 ttl=255 time=28.060 ms
64 bytes from 192.0.2.1: icmp_seq=1 ttl=255 time=29.061 ms
64 bytes from 192.0.2.1: icmp_seq=2 ttl=255 time=27.577 ms
^C
--- 192.0.2.1 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 27.577/28.232/29.061/0.618 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;example of ping&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once the ping is successful from firewall to &lt;strong&gt;newserver&lt;/strong&gt;, the reverse should also work. Since the tunnel will need to be established from the &lt;strong&gt;firewall&lt;/strong&gt; to &lt;strong&gt;newserver&lt;/strong&gt; we will use the &lt;em&gt;ifstated(8)&lt;/em&gt; process to keep the tunnel active. This config will ping the new server over the tunnel every 60 seconds. Adjust as necessary if 60 seconds isn&#39;t enough to keep your connection alive.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# Keep the tunnel alive
#
chk_peer = &#39;(&amp;quot;ping -q -c 5 -w 1 192.0.2.1 &amp;gt;/dev/null 2&amp;gt;&amp;amp;1&amp;quot; every 60)&#39;

init-state tunnel_down

state tunnel_up {
        init {
                run &amp;quot;logger -st ifstated &#39;state: UP&#39;&amp;quot;
        }
        if ! $chk_peer {
                run &amp;quot;logger -st ifstated &#39;tunnel DOWN&#39;&amp;quot;
                set-state tunnel_down
        }
}
state tunnel_down {
        init {
                run &amp;quot;logger -st ifstated &#39;state: DOWN&#39;&amp;quot;
        }
        if $chk_peer {
                run &amp;quot;logger -st ifstated &#39;tunnel UP&#39;&amp;quot;
                set-state tunnel_up
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/ifstated.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Enable and start ifstated&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rcctl enable ifstated
rcctl start ifstated
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;setup-relay-service&#34;&gt;Setup relay service&lt;/h3&gt;
&lt;p&gt;Now that the tunnel is up we can setup the relay service. This will accept connection (on specified ports), perform TLS authentication of the client, and then forward to the internal servers. To start this, create the &lt;em&gt;&lt;strong&gt;/etc/relayd.conf&lt;/strong&gt;&lt;/em&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ext_ip=&amp;quot;newserver.ip.addr.ess&amp;quot;
ext_ip6=&amp;quot;newserver:ipv6:ip::address&amp;quot;

sso_int=&amp;quot;192.168.30.14&amp;quot;
mail_int=&amp;quot;192.168.30.15&amp;quot;
certauth_int=&amp;quot;192.168.30.11&amp;quot;

table &amp;lt;sso_table&amp;gt; { $sso_int }
table &amp;lt;mail_table&amp;gt; { $mail_int }
table &amp;lt;certauth_table&amp;gt; { $certauth_int }

# TLS PROXY for Internal services
tcp protocol tls_proxy {
        tcp nodelay
        tcp sack
        # validate the clients against internal CA
        tls client ca &amp;quot;/etc/ssl/example.org.pem&amp;quot;
        tls keypair &amp;quot;newserver_internal&amp;quot;
}
# HTTPS PROXY for Internal services
http protocol http_proxy {
        return error
        match header set &amp;quot;X-Forwarded-For&amp;quot; value &amp;quot;$REMOTE_ADDR&amp;quot;
        match header set &amp;quot;X-Forwarded-By&amp;quot; value &amp;quot;$SERVER_ADDR:$SERVER_PORT&amp;quot;
        match header set &amp;quot;Keep-Alive&amp;quot; value &amp;quot;$TIMEOUT&amp;quot;
        # match based on the Host header and set the table
        pass request header &amp;quot;Host&amp;quot; value &amp;quot;sso.example.org&amp;quot; forward to &amp;lt;sso_table&amp;gt;
        pass request header &amp;quot;Host&amp;quot; value &amp;quot;mail.example.org&amp;quot; forward to &amp;lt;mail_table&amp;gt;
        pass request header &amp;quot;Host&amp;quot; value &amp;quot;certauth.oexample.org&amp;quot; forward to &amp;lt;certauth_table&amp;gt;
        # only trust devices that have our cert
        tls client ca &amp;quot;/etc/ssl/example.org.pem&amp;quot;
        tls keypair wall
}
# setup the relays
relay proxy {
        listen on $ext_ip port 443 tls
        protocol http_proxy
        forward with tls to &amp;lt;sso_table&amp;gt; port https check icmp
        forward with tls to &amp;lt;mail_table&amp;gt; port https check icmp
        forward with tls to &amp;lt;certauth_table&amp;gt; port https check icmp
}
# remove if IPv6 is not used
relay proxy2 {
        listen on $ext_ip6 port 443 tls
        protocol http_proxy
        forward with tls to &amp;lt;sso_table&amp;gt; port https check icmp
        forward with tls to &amp;lt;mail_table&amp;gt; port https check icmp
        forward with tls to &amp;lt;certauth_table&amp;gt; port https check icmp
}
# setup mail and imap relays
relay mail_proxy {
        listen on $ext_ip port 465 tls
        protocol tls_proxy
        forward with tls to &amp;lt;mail_table&amp;gt; port 465 check icmp
}
relay imap_proxy {
        listen on $ext_ip port 993 tls
        protocol tls_proxy
        forward with tls to &amp;lt;mail_table&amp;gt; port 993 check icmp
}
# remove if IPv6 is not used
relay mail_proxy2 {
        listen on $ext_ip6 port 465 tls
        protocol tls_proxy
        forward with tls to &amp;lt;mail_table&amp;gt; port 465 check icmp
}
relay imap_proxy2 {
        listen on $ext_ip6 port 993 tls
        protocol tls_proxy
        forward with tls to &amp;lt;mail_table&amp;gt; port 993 check icmp
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/relayd.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Enable and start relayd(8)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rcctl enable relayd
rcctl start relayd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;enable and start relayd&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now create a user/email certificate from our &lt;strong&gt;certauth&lt;/strong&gt; server as described in &lt;a href=&#34;https://obtusenet.com/blog/self-hosted-episode-4-certificates.html&#34;&gt;Episode 4 - Host/Email certificates&lt;/a&gt;. Connect to the web email interface from a device that is not on the internal network. The web browser should prompt use to for a user defined certificate. If you ignore the browser request and continue the server should abort the connection. Try again, but this time provide the browser the certificate created and confirm that the web interface is now presented.&lt;/p&gt;
&lt;h2 id=&#34;setup-mail-server&#34;&gt;Setup MAIL server&lt;/h2&gt;
&lt;p&gt;Finally we stage where we will configure our ability to send and receive external email. As was discussed in Episode 7 we will need to setup SPF, DKIM and DMARC records. This will tell the world about our valid email service and provide a level of trust. While it&#39;s not guaranteed that our email will always be accepted, it goes a long way to indicate that our email server is run in a responsible fashion.&lt;/p&gt;
&lt;h3 id=&#34;add-dns-records&#34;&gt;Add DNS records&lt;/h3&gt;
&lt;p&gt;As we did when we setup the &lt;strong&gt;mail&lt;/strong&gt; server we will need to setup some external DNS records. These records will be used by external mail providers to make sure that our mail server is legitimate.&lt;/p&gt;
&lt;p&gt;The first record is the MX record. This will tell the world where to send email to. The @ sign indicated the our root domain. Depending on the DNS provider that part may be able to be left blank.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@    IN   MX    10  newserver.example.org.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;MX DNS record&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;SPF is the next record we need. Here we indicate what servers mail will come from for our domain. Since we will be receiving and sending mail from a single server we can just setup the SPF record to indicate the use of the MX values and to deny all else.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@   IN   TXT   &amp;quot;v=spf1 mx -all&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;SPF DNS record&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The DMARC record tells the world what we suggest be done when receiving email that does not comport to our DKIM and SPF settings&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_dmarc  IN  TXT    &amp;quot;v=DMARC1;p=quarantine;pct=100;rua=mailto:dmarcreports@example.org;ruf=mailto:dmarcreports@example.org;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;DMARC DNS record&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally we will create the DKIM record. This will be based on the private key we created on the &lt;strong&gt;mail&lt;/strong&gt; server. On the &lt;strong&gt;mail&lt;/strong&gt; server we can use the below command to generate the DNS record.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openssl rsa -in /etc/mail/dkim/example.org_priv.rsa.key -pubout 2&amp;gt;/dev/null | sed &#39;1s/.*/v=DKIM1;p=/;:nl;${s/-----.*//;q;};N;s/\n//g;b nl;&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to generate DKIM TXT record&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The output should look something like this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;v=DKIM1;p=MIIBIjANBgkqhki_LOTS_OF_MORE_BASE64_E1wIDAQAB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;example of DKIM public key&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now add that to the DNS record.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;selector01._domainkey    IN   TXT   &amp;quot;v=DKIM1;REST_OF_THE_LINE&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;DKIM DNS record&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;create-smtp-config-files&#34;&gt;Create SMTP config files&lt;/h3&gt;
&lt;p&gt;This &lt;strong&gt;smtpd.conf&lt;/strong&gt; file will be a little different that the previous one we built for &lt;strong&gt;mail&lt;/strong&gt;. This file will accept mail from any location, filter that mail based on some well known spamming techniques and then forward it onto the &lt;strong&gt;mail.example.org&lt;/strong&gt; server in our self hosted environment.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# mail relay config
#
# 1) server will receive any mail destin for &amp;lt;domains&amp;gt; table and
#   forward to internal relay
# 2) server will receive mail from internal server for any and
#   relay
#
# DKIM signing will occur on the internal server
smtp max-message-size &amp;quot;100M&amp;quot;
ca  local_ca cert &amp;quot;/etc/ssl/example.org_root_ca.pem&amp;quot;
pki mail_cert cert &amp;quot;/etc/ssl/wall.fullchain.pem&amp;quot;
pki mail_cert key  &amp;quot;/etc/ssl/private/wall.key&amp;quot;
pki int_cert cert &amp;quot;/etc/ssl/wall_internal.pem&amp;quot;
pki int_cert key  &amp;quot;/etc/ssl/private/wall_internal.key&amp;quot;

table   aliases         file:/etc/mail/aliases
table   domains         file:/etc/mail/domains
table   bad-from        file:/etc/mail/blacklist
table   bad-to          file:/etc/mail/spamtrap
table   relayaccts      file:/etc/mail/relayaccts
table   relays          file:/etc/mail/relays
table   creds           file:/etc/mail/credentials

#check for dynamic dns sources
filter check_dyndns phase connect match rdns regex { \
  &#39;.*\.dyn\..*&#39;, \
  &#39;.*\.dsl\..*&#39;, \
  &#39;.*dynamic.*&#39;, \
  &#39;*\.vodafonedsl\.*&#39;, \
  &#39;*\.vodafone\.*&#39; \
  } disconnect &amp;quot;550 no residential connections&amp;quot;

# check for numeric IPv4 sources
filter check_ip_names phase connect match rdns regex { \
  &#39;[0-9]{1,3}[-\.][0-9]{1,3}[-\.][0-9]{1,3}[-\.]]0-9]{1,3}[-\.].*&#39; \
  } disconnect &amp;quot;550 no residential connections&amp;quot;

# confirm rdns is valid
filter check_rdns phase connect match !rdns \
  disconnect &amp;quot;550 no rDNS&amp;quot;

# confirm fcrdns is valid
filter check_fcrdns phase connect match !fcrdns \
  disconnect &amp;quot;550 no FCrDNS&amp;quot;

# check to see if mail is from a known bad sender
filter check_bad_from phase mail-from match mail-from regex &amp;lt;bad-from&amp;gt; \
  disconnect &amp;quot;550 go away spammer&amp;quot;

# check to see if mail is going to a bad receptient
filter check_bad_to phase rcpt-to match rcpt-to regex &amp;lt;bad-to&amp;gt; \
  disconnect &amp;quot;550 go away spammer&amp;quot;

# combine filters into a list
filter initial_checks chain { check_rdns, check_fcrdns, check_ip_names, check_dyndns, check_bad_from, check_bad_to }

# listen for incoming email and apply the filter list
listen on all tls pki mail_cert filter initial_checks
# listen for relayd mail on the tunnel interface
listen on wg port 9465 smtps pki int_cert auth &amp;lt;relayaccts&amp;gt; tag RELAY

action &amp;quot;outbound&amp;quot; relay
action &amp;quot;inbound&amp;quot; relay host smtps://ponyexpress@mail.example.org:9465 tls auth &amp;lt;creds&amp;gt;
# local mail, send inbound
match for local action &amp;quot;inbound&amp;quot;
# anything marked as relay, send outbound
match tag RELAY from src &amp;lt;relays&amp;gt; for any action &amp;quot;outbound&amp;quot;
# anything for one of our domains send inbound.
match from any for domain &amp;lt;domains&amp;gt; action &amp;quot;inbound&amp;quot;

# default deny
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/mail/smtpd.conf&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# domains
#
# list domains to accept mail for
# to update smtpd use &#39;smtpctl update table domains&#39;
#
example.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/mail/domains&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# place sender names of domains (or whole domains here)
# to deny their acceptance.
#
#somename@somedom.ain
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/mail/blacklist&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# List of servers that we will accpet relayed mail from
# use &#39;smtpctl update table relays&#39; to update table
192.168.30.15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/mail/relays&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# spamtrap
#
# place email names of accounts that should never 
# receive email.
#spamtrap@example.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/mail/spamtrap&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# relay credentials
# use &#39;smtpctl update table creds&#39; to update
# relay user
ponyexpress     ponyexpress:MAKE_THIS_A_GOOD_PASSWORD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/mail/credentials&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# user names to be used for relays
#
# use &#39;smtpctl update table relayusers&#39; to reload file
# use &#39;smtpctl encrypt &amp;quot;password&amp;quot;&#39; to encrypt password
ponyexpress     $2b$08$19n8/YAQQxBxOVApmDxmHuQVqtbMZNSADb6LJaGvRW1NdW1oOO0y.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/mail/relayaccts&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;With all those files in place check that &lt;em&gt;smtpd(8)&lt;/em&gt; is happy then restart if successful.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;smtpd -nvf /etc/mail/smtpd.conf
# if successful restart
rcctl restart smtpd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to check validity of smtpd.conf and associated files&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;update-forwarding-on-internal-mail-server&#34;&gt;Update forwarding on internal mail server&lt;/h3&gt;
&lt;p&gt;Now we can receive external email, we need to revisit the &lt;strong&gt;mail.example.org&lt;/strong&gt; server to have it be able to forward email via &lt;strong&gt;newserver&lt;/strong&gt;. On &lt;strong&gt;mail&lt;/strong&gt; server un-comment the lines that were left commented.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;table relayaccts        file:/etc/mail/relayaccts
table creds             file:/etc/mail/credentials

listen on 0.0.0.0 port 9465 smtps pki mail_cert auth &amp;lt;relayaccts&amp;gt; tag RELAY

action &amp;quot;relay&amp;quot; relay host smtps://ponyexpress@newserver.example.org:9465 tls auth &amp;lt;creds&amp;gt; helo &amp;quot;mail.example.org&amp;quot;

match from auth for any action &amp;quot;relay&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;lines in smtpd.conf to uncomment&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Be sure to add the &lt;strong&gt;relayaccts&lt;/strong&gt; and &lt;strong&gt;credentials&lt;/strong&gt; files. These can be mirrors of the ones on &lt;strong&gt;newserver&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# relay credentials
# use &#39;smtpctl update table creds&#39; to update
# relay user
ponyexpress     ponyexpress:MAKE_THIS_A_GOOD_PASSWORD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/mail/credentials&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# user names to be used for relays
#
# use &#39;smtpctl update table relayusers&#39; to reload file
# use &#39;smtpctl encrypt &amp;quot;password&amp;quot;&#39; to encrypt password
ponyexpress     $2b$08$19n8_ENCRYPTED_PASSWORD_W1NdW1oOO0y.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/mail/relayaccts&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now check that the updates are valid and then restart the service.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;smtpd -nvf /etc/mail/smtpd.conf

rcctl restart smtpd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;sending-our-first-emails&#34;&gt;Sending our first emails&lt;/h3&gt;
&lt;p&gt;Now that everything is in place, send your first email. Send it to an alternate account you have. If you receive it, respond to make sure that inbound access is working. If you run into problems, review the &lt;strong&gt;/var/log/maillog&lt;/strong&gt; files on both &lt;strong&gt;newserver&lt;/strong&gt; and &lt;strong&gt;mail&lt;/strong&gt; servers for indications as to what failed.&lt;/p&gt;
&lt;h1 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Now that we have email access to and from our self hosted environment to the rest of the world... you need to decide what is next! Good Luck!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Self Hosted: Episode 7 - Email</title>
    <link href="https://obtusenet.com/blog/self-hosted-episode-8-email.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/self-hosted-episode-8-email.html</id>
    <published>2025-10-18T05:00:48Z</published>
    <updated>2025-10-18T05:00:48Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>Software Used: * OpenBSD 7.7 (as VM) * OpenSMTPD (part of OS) * Dovecot 2.3.21.1 (from ports) * RoundcubeMail 1.6.11 (from ports) * twofactor_webauthn 1.3.0 Overview Email is a surprisingly vast and…</summary>
    <category term="self-hosted"/>
    <category term="email"/>
    <category term="dovecot"/>
    <category term="roundcube"/>
    <category term="opensmtpd"/>
    <content type="html">&lt;h4 id=&#34;software-used&#34;&gt;Software Used:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;OpenBSD 7.7 (as VM)&lt;/li&gt;
&lt;li&gt;OpenSMTPD (part of OS)&lt;/li&gt;
&lt;li&gt;Dovecot 2.3.21.1 (from ports)&lt;/li&gt;
&lt;li&gt;RoundcubeMail 1.6.11 (from ports)&lt;/li&gt;
&lt;li&gt;twofactor_webauthn 1.3.0&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&#34;overview&#34;&gt;Overview&lt;/h1&gt;
&lt;p&gt;Email is a surprisingly vast and charged topic. It is one of the first services that someone/anyone can deploy on the Internet and begin communicating. However, email has gotten this reputation that it is &amp;quot;hard&amp;quot;. I disagree. Just about anything can be messed up if you don&#39;t understand what you are doing. Email is no different.&lt;/p&gt;
&lt;p&gt;I hope to dispel the myth of email being &amp;quot;hard&amp;quot; and provide a guide that will get you started on the right foot. I will touch on some topics only lightly and try to provide some links where you can get more information.&lt;/p&gt;
&lt;h2 id=&#34;a-little-background&#34;&gt;A little background&lt;/h2&gt;
&lt;p&gt;Before we begin creating services and sending email it is always good to understand the protocols and interactions that are involved.&lt;/p&gt;
&lt;p&gt;To begin, email is transmitted over the internet using &lt;a href=&#34;https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol&#34;&gt;SMTP&lt;/a&gt;. Email access from an client can occur over several protocols. The protocol we will use to access email is called &lt;a href=&#34;https://obtusenet.com/blog/password-sensibilities.html&#34;&gt;IMAP&lt;/a&gt;. When sending email to others, and not be mistaken for SPAM, the email needs to be seen as legitimate. In order to prove the legitimacy of email being sent a couple of protocols a are used: &lt;a href=&#34;https://en.wikipedia.org/wiki/Sender_Policy_Framework&#34;&gt;SPF&lt;/a&gt; and &lt;a href=&#34;https://obtusenet.com/blog/password-sensibilities.html&#34;&gt;DKIM&lt;/a&gt;. SPF allows a receiving email server to verify that the sending server is allowed to send email for that domain. DKIM provides a cryptographic way that the email being received actually originated under to control of that domain. DKIM and SPF are wrapped up into an additional protocol called &lt;a href=&#34;https://en.wikipedia.org/wiki/DMARC&#34;&gt;DMARC&lt;/a&gt;. DMARC is a way for a domain owner to publish how receiving servers should treat email that does not pass the SPF and DKIM tests. There is no way to force the receiving servers to act per the DMARC policy but DMARC is a way to signal to the Internet that servers falsely claiming to operate under a domain should be suspect or better yet ignored.&lt;/p&gt;
&lt;p&gt;SPF, DKIM and DMARC are all controlled using DNS records. In addition to the above protocols simply publishing proper A and PTR records for the email servers will also help in proving legitimacy. In fact in the next episode when we deploy an external email forwarder, we will use the fact that improper A and PTR records are indicators of illegitimate mail servers.&lt;/p&gt;
&lt;p&gt;With the background out of the way and to keep the things simple, I have broken down this guide into 4 parts. There is 1) creating the VM, 2) the mail storage, 3) the mail exchanger, and 4) mail access sections.&lt;/p&gt;
&lt;h2 id=&#34;create-the-mail-vm&#34;&gt;Create the mail VM...&lt;/h2&gt;
&lt;p&gt;First this we need to do is create the VM. &lt;a href=&#34;https://obtusenet.com/blog/self-hosting-episode-2-firewall.html&#34;&gt;Similar to building the firewall&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vm create -t openbsd77 -s 50G -m 4G -c 2 mail
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next add the DMZ network to the VM.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vm add -t network -s net-dmz mail
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The image is was already downloaded so we can skip this step. Now install the OS.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vm install mail install77.iso
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and connect to the console&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vm console mail
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finish installing the OS and shut it down. When the installation is complete break out of the console using &lt;strong&gt;~.&lt;/strong&gt; escape. This will take you back to a the hypervisor. From the hypervisor make sure the VM is halted using the &#39;vm list&#39; command. The state should be &amp;quot;Stopped&amp;quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@hyperv01:~ # vm list
NAME      DATASTORE  LOADER  CPU  MEMORY  VNC  AUTO  STATE
mail      default    uefi    2    4G      -    No    Stopped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the VM using the &#39;&lt;em&gt;vm start mail&lt;/em&gt;&#39; command. Next we can install the software.&lt;/p&gt;
&lt;h2 id=&#34;mail-storage&#34;&gt;Mail Storage&lt;/h2&gt;
&lt;p&gt;There needs to be a place to store the mail that we receive. This is the mail servers job. The mail server will need to do several things.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Authenticate against the LDAP directory.&lt;/li&gt;
&lt;li&gt;To be able to &amp;quot;sort&amp;quot; the mail as it comes into the server. This capability is referred to as Sieve.&lt;/li&gt;
&lt;li&gt;It will need to store the mail (obviously). But we have options in now we store the mail. We can use single mailboxes (called the &lt;a href=&#34;https://en.wikipedia.org/wiki/Mbox&#34;&gt;&lt;strong&gt;mbox&lt;/strong&gt;&lt;/a&gt;) format. Which is about as old a email itself. However there is a better options. If the mail is stored in in &lt;a href=&#34;https://en.wikipedia.org/wiki/Maildir&#34;&gt;&lt;strong&gt;Maildir&lt;/strong&gt;&lt;/a&gt; format then each email is kept separate and we have options for folders. This is the options that will be show here.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;install-mail-server&#34;&gt;Install mail server&lt;/h3&gt;
&lt;p&gt;For the mail server we will use an application called &lt;a href=&#34;https://doc.dovecot.org/2.3/&#34;&gt;Dovecot&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;First install the packages&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pkg_add dovecot-- dovecot-ldap-- dovecot-pigeonhole--
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next configure the packages. Dovecot installs a lot of files under the &lt;strong&gt;/etc/dovecot&lt;/strong&gt; directory. We will only need to update some of those files.&lt;/p&gt;
&lt;p&gt;First we will enable LDAP access.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# LDAP connectivity setting
uris = ldaps://ldap.example.org:636
uris = ldap://ldap.example.org
dn = cn=ldapadmin,dc=example,dc=org
dnpass = letmein
tls = no
tls_ca_cert_file = /etc/ssl/example.org_root_ca.pem
tls_cert_file = /etc/ssl/mail.fullchain.pem
tls_key_file = /etc/ssl/private/mail.key

debug_level = 0
auth_bind = no

base = dc=example,dc=org
user_attrs = homeDirectory=home,uidNumber=uid,gidNumber=gid
user_filter = (&amp;amp;(objectClass=posixAccount)(|(mail=%u)(uid=%u)))
pass_attrs = uid=user,userPassword=password
pass_filter = (&amp;amp;(objectClass=posixAccount)(mail=%u))
iterate_attrs = mail=user
iterate_filter = (objectClass=posixAccount)
default_pass_scheme = SSHA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/dovecot/dovecot-ldap.conf.ext&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
#
passdb {
        driver = ldap
        args = /etc/dovecot/dovecot-ldap.conf.ext
}

# override all ldap settings
userdb {
        driver = ldap
        args = /etc/dovecot/dovecot-ldap.conf.ext
        # override ldap values
        override_fields = uid=vmail gid=vmail home=/var/mail/vmail/%n
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/dovecot/conf.d/auth-ldap.conf.ext&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# for mutual auth
#auth_ssl_require_client_cert = yes
#auth_ssl_username_from_cert = yes

#
auth_mechanisms = plain

# use ldap to auth users
!include auth-ldap.conf.ext
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/dovecot/conf.d/10-auth.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Enable the server to support TLS connections.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
ssl = yes
ssl_cert = &amp;lt;/etc/ssl/mail.fullchain.pem
ssl_key  = &amp;lt;/etc/ssl/private/mail.key
ssl_ca = &amp;lt;/etc/ssl/example.org_root_ca.pem
#ssl_key_password =

# only validate certs the we generated
#ssl_client_ca_dir =
ssl_client_ca_file = &amp;lt;/etc/ssl/example.org_root_ca.pem
ssl_client_cert = &amp;lt;/etc/ssl/mail.fullchain.pem
ssl_client_key = &amp;lt;/etc/ssl/private/mail.key

#ssl_cert_username_field = commonName
ssl_client_require_valid_cert = no
ssl_require_crl = no
ssl_verify_client_cert = no
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/dovecot/conf.d/10-ssl.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now set the remaining variables into the &lt;strong&gt;local.conf&lt;/strong&gt; file&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# local changes to dovecot setting
# This file is processed last and overrides the conf.d/*.conf 
# configs.

# only support imap, lmtp and sieve protocols.  disable pop3
protocols = imap lmtp sieve

# set the mail location
mail_location = maildir:~/Maildir

# set the perms on the auth listener (used by smtpd)
service auth {
  unix_listener auth-userdb {
    mode = 0660
    user = vmail
    group = vmail
  }
}

# change the authworker to run as the same user (not root)
service auth-worker {
  user = $default_internal_user
}

# set the metrics
metric auth_success {
  filter = event=auth_request_finished AND success=yes
}
metric auth_failures {
  filter = event=auth_request_finished AND NOT success=yes
}
metric imap_command {
  filter = event=imap_command_finished
  group_by = cmd_name tagged_reply_state
}
metric smtp_command {
  filter = event=smtp_server_command_finished
  group_by = cmd_name status_code duration:exponential:1:5:10
}
metric mail_delivery {
  filter = event=mail_delivery_finished
  group_by = duration:exponential:1:5:10
}

### conf.d/15-lda.conf
# temp fail if user over quote... do not just bounce email
quota_full_tempfail = yes

# enable SIEVE for pre-sorting email
protocol lda {
  mail_plugins = $mail_plugins sieve
}

### conf.d/15-mailboxes.conf
# add other common mailboxes that are used by different clients and point them to consisent destinations
namespace inbox {
  mailbox Spam {
    special_use = \Junk
  }
  mailbox &amp;quot;Deleted Items&amp;quot; {
    special_use = \Trash
  }
}

### conf.d/20-imap.conf
# add sieve to imap
protocol imap {
  mail_plugins = $mail_plugins imap_sieve
}
### conf.d/20-lmtp.conf
# disable verify quota before replying rcpt to
lmtp_rcpt_check_quota = no

# add sieve to lmtp
protocol lmtp {
  mail_plugins = $mail_plugins sieve
}

# mail process statistics tracking
service stats {
  unix_listener stats-writer {
    user = 
    group = $default_internal_group
    mode = 0660
  }
}

### conf.d/90-plugin.conf
# automate the reporting of SPAM and HAM 
plugin {
  sieve_plugins = sieve_imapsieve sieve_extprograms
  sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment

# uncomment if spam/ham processing is configured
#  imapsieve_mailbox1_name = Junk
#  imapsieve_mailbox1_causes = COPY
#  imapsieve_mailbox1_before = file:/etc/dovecot/sieve/report-spam.sieve
#  imapsieve_mailbox2_name = *
#  imapsieve_mailbox2_from = Junk
#  imapsieve_mailbox2_causes = COPY
#  imapsieve_mailbox2_before = file:/etc/dovecot/sieve/report-ham.sieve

  sieve_before = /etc/dovecot/sieve/default.sieve
}

### conf.d/90-sieve-extprograms.conf
plugin {
  sieve_pipe_bin_dir = /etc/dovecot/sieve
}

### conf.d/90-sieve.conf
plugin {
  sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
  sieve_plugins = sieve_imapsieve sieve_extprograms
}

# enable extended logging to troubleshoot
auth_debug = yes
auth_verbose = yes
verbose_ssl = yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/dovecot/local.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We will add the default.sieve file. This file will be run on all incoming emails.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require [&amp;quot;fileinto&amp;quot;];
# rule:[SPAM]
if header :contains &amp;quot;X-Spam&amp;quot; &amp;quot;yes&amp;quot; {
  fileinto &amp;quot;Junk&amp;quot;;
}

if header :contains &amp;quot;X-Spam-Flag&amp;quot; &amp;quot;YES&amp;quot; {
  fileinto &amp;quot;Junk&amp;quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/dovecot/sieve/default.sieve&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We can test the configuration for errors using the doveconf(1) command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;doveconf -n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output will show non-default config options. Use the &#39;-a&#39; to show all the config options. If there are errors in the config they will be indicated. Fix the errors before continuing.&lt;/p&gt;
&lt;p&gt;Finally enable and start the mail storage server&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rcctl enable dovecot
rcctl start dovecot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To verify that the server is running and LDAP connection is functioning we can lookup the users by using the &lt;em&gt;doveadm(1)&lt;/em&gt; command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;doveadm user &#39;*&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command will return a list of all valid users in LDAP.&lt;/p&gt;
&lt;h2 id=&#34;mail-exchanger&#34;&gt;Mail Exchanger&lt;/h2&gt;
&lt;p&gt;The mail exchanger is the server that is responsible for communicating with other mail exchangers either locally or in the Internet. This device uses the SMTP protocol to exchange emails. We will be using &lt;a href=&#34;https://www.opensmtpd.org/&#34;&gt;OpenSMTPd&lt;/a&gt; as our mail exchanger software. OpenSMTPd is built into the OpenBSD operating system. We will install a few extensions to tie the service into the LDAP server for user accounts.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pkg_add opensmtpd-table-ldap opensmtpd-filter-dkimsign
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First configure the LDAP access.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Connection information
url: ldap://ldap.example.org
username: cn=ldapadmin,dc=example,dc=org
password: letmein
basedn: dc=example,dc=org

# filter for aliases
# used in aliases and virtual tables
# attribute is the destination user or another mailaddress
alias_filter: (&amp;amp;(objectclass=posixaccount)(mail=%s))
alias_attributes: uid

# filter for username
# attributes are username and hashed password
# the account need to be allowed to read the hashed password
credentials_filter: (&amp;amp;(objectclass=posixaccount)(mail=%s))
credentials_attributes: uid,description

## domain table
## used for: match for domain &amp;lt;ldap&amp;gt;
#domain_filter (&amp;amp;(objectclass=dnsdomain)(cn=%s))
#domain_attributes: cn

# user info
# extra user info
userinfo_filter: (&amp;amp;(objectclass=posixaccount)(uid=%s))
userinfo_attributes: uidNumber,gidNumber,homeDirectory

## filter for mailaddr info
## list of mailaddresses for match statements
## attribute will be ignored but is needed for ldap
#mailaddr_filter: (&amp;amp;(objectclass=posixaccount)(mail=%s))
#mailaddr_attributes: mail

## filter for mailaddrmap info
## used for: liste on ... sender &amp;lt;senders&amp;gt;
#mailaddrmap_filter: (&amp;amp;(objectclass=posixaccount)(uid=%s))
#mailaddrmap_attributes: mail

## filter for netaddr
## used for: match from src &amp;lt;ldap&amp;gt;
## not real useful, because match is on IP not CIDR
#netaddr_filter: (&amp;amp;(objectclass=host)(ipaddr=%s))
#netaddr_attributes: ipaddr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/mail/ldap.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next configure the SMTPd configuration file. Here we will specify that LDAP should be used for various table lookups as well as a static file for a list of domains the email server supports. We will leave some of the configuration commented out until we have setup the relay server.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#

smtp max-message-size &amp;quot;100M&amp;quot;
ca  local_ca cert &amp;quot;/etc/ssl/example.org_root_ca.pem&amp;quot;
pki mail_cert cert &amp;quot;/etc/ssl/mail.fullchain.pem&amp;quot;
pki mail_cert key  &amp;quot;/etc/ssl/private/mail.key&amp;quot;

table ldap      ldap:/etc/mail/ldap.conf
table domains   file:/etc/mail/domains
#table relayaccts        file:/etc/mail/relayaccts
#table creds             file:/etc/mail/credentials

# create filter for signing emails for supported domains.
filter dkimsign_rsa proc-exec &amp;quot;filter-dkimsign -d example.org -s selector01 -k /etc/mail/dkim/example.org_priv.rsa.key&amp;quot; user _dkimsign group _dkimsign

# create a filter chain to allow more than one filter.
filter dkimsign chain { dkimsign_rsa  }

listen on 0.0.0.0 port submission tls-require pki mail_cert auth &amp;lt;ldap&amp;gt; filter dkimsign
listen on 0.0.0.0 port smtps smtps pki mail_cert auth &amp;lt;ldap&amp;gt; filter dkimsign
listen on 0.0.0.0 tls pki mail_cert
#listen on 0.0.0.0 port 9465 smtps pki mail_cert auth &amp;lt;relayaccts&amp;gt; tag RELAY

# setup the action to handle local email.
# leave external email commented out for now.
action &amp;quot;local&amp;quot; lmtp &amp;quot;/var/dovecot/lmtp&amp;quot; rcpt-to user &amp;quot;vmail&amp;quot; userbase &amp;lt;ldap&amp;gt; virtual &amp;lt;ldap&amp;gt;
#action &amp;quot;relay&amp;quot; relay host smtps://ponyexpress@newserver.example.org:9465 tls auth &amp;lt;creds&amp;gt; helo &amp;quot;mail.example.org&amp;quot;

# Create a match rule for email that has been received.
match for local action &amp;quot;local&amp;quot;
match for domain &amp;lt;domains&amp;gt; action &amp;quot;local&amp;quot;
match from auth for domain &amp;lt;domains&amp;gt; action &amp;quot;local&amp;quot;
#match from auth for any action &amp;quot;relay&amp;quot;

# by default any email that doesnt match a rule is dropped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/mail/smtpd.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Create a file for domains that we will support. This will allow us to add additional email domains as things grow/change.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# domains supported by this mail server
example.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/mail/domains&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;DKIM keys are like other cryptographic keys. There is a private key and a public key. To create the private key use the following command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[ ! -d /etc/mail/dkim ]] &amp;amp;&amp;amp; mkdir -p /etc/mail/dkim
openssl genrsa -out /etc/mail/dkim/example.org_priv.rsa.key 2048
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Generate RSA DKIM key&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Later we will generate the public key record for deploying to DNS.&lt;/p&gt;
&lt;p&gt;Confirm that the configuration are correct. Fix any errors.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;smtpd -nvf /etc/mail/smtpd.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Check validity of smtpd config&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If all configuration changes succeed, restart the smtpd service.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rcctl restart smtpd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Restart smtpd service&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;mail-access&#34;&gt;Mail Access&lt;/h2&gt;
&lt;p&gt;Now that we can receive email and store email. We need to be able to access the emails. This can be done in two ways. First will be over the web and second will be using a native email client that can talk directly to the email server. Both options will be covered. However in the second option only the basics will be touched on as each email client is a little different.&lt;/p&gt;
&lt;h3 id=&#34;web-email-client&#34;&gt;Web email client&lt;/h3&gt;
&lt;p&gt;Perhaps the most widely used web based email client is Roundcube. Do the following to install roundcube and its dependencies.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pkg_add roundcube php-pdo_sqlite8.2 php-curl8.2 php-gd8.2 php-ldap8.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also download and extract the webauthn two factor plugin.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ftp -o /tmp/twofactor_webauthn.tar.gz &amp;quot;https://github.com/bartnv/twofactor_webauthn/archive/refs/tags/v1.3.0.tar.gz&amp;quot;
tar -C /var/www/roundcubemail/plugins -xzf /tmp/twofactor_webauthn.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Initialize the roundcube database.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite3 -init /var/www/roundcubemail/SQL/sqlite-initial.sql /var/www/roundcubemail/db/sqlite.db
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Initialize the main configuration.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$config = [];
// seth the database location
$config[&#39;db_dsnw&#39;] = &#39;sqlite:///db/sqlite.db?mode=0660&#39;;
// set the IMAP host and port used for authentication
$config[&#39;imap_host&#39;] = &#39;mail.example.org:143&#39;;
// set the SMTP host.  used for sending emaik
$config[&#39;smtp_host&#39;] = &#39;ssl://mail.example.org:465&#39;;
$config[&#39;smtp_user&#39;] = &#39;%u&#39;;
$config[&#39;smtp_pass&#39;] = &#39;%p&#39;;
$config[&#39;smtp_conn_options&#39;] = [
        &#39;ssl&#39; =&amp;gt; [
                &#39;verify_peer&#39;   =&amp;gt; false,
                &#39;cafile&#39;        =&amp;gt; &#39;/etc/ssl/example.org_root_ca.pem&#39;,
                &#39;security_level&#39;=&amp;gt; 4,
        ],
];
$config[&#39;support_url&#39;] = &#39;&#39;;
// set our name
$config[&#39;product_name&#39;] = &#39;example.orgt Webmail&#39;;
// change the des_key to something random.
$config[&#39;des_key&#39;] = &#39;CHANGE_ME_PLEASE!!!&#39;;
// enable some useful plugins
$config[&#39;plugins&#39;] = [
    &#39;archive&#39;,
    &#39;zipdownload&#39;,
    &#39;managesieve&#39;,
    &#39;markasjunk&#39;,
    &#39;jqueryui&#39;,
    &#39;twofactor_webauthn&#39;,
];
$config[&#39;skin&#39;] = &#39;elastic&#39;;
// disable the installer
$config[&#39;enable_installer&#39;] = false;
// setup logging of some functions
$config[&#39;log_logins&#39;] = true;
$config[&#39;log_session&#39;] = true;
$config[&#39;smtp_debug&#39;] = false;
$config[&#39;imap_debug&#39;] = false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/var/www/roundcubemail/config/config.inc.php&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Configure and enable the httpd server&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server &amp;quot;*&amp;quot; {
        listen on * port 80
        location &amp;quot;/.well-known/acme-challenge/*&amp;quot; {
                root &amp;quot;/acme&amp;quot;
                request strip 2
        }
        location * {
                block return 302 &amp;quot;https://$HTTP_HOST$REQUEST_URI&amp;quot;
        }
}
server &amp;quot;*&amp;quot; {
        log style combined
        connection max request body 102400000
        listen on * tls port 443
        tls {
                certificate &amp;quot;/etc/ssl/mail.fullchain.pem&amp;quot;
                key &amp;quot;/etc/ssl/private/mail.key&amp;quot;
        }
        # used for autodiscovery
        location &amp;quot;/mail/*&amp;quot; {
                root &amp;quot;/htdocs&amp;quot;
                request strip 1
                directory no index
        }
        root &amp;quot;roundcubemail/public_html&amp;quot;
        directory index index.php
        # block direct access to locations that shouldn&#39;t be accessable from HTTPD
        location &amp;quot;/*.git&amp;quot; { block }
        location &amp;quot;/*.tx&amp;quot; { block }
        location &amp;quot;/*SQL*&amp;quot; { block }
        location &amp;quot;/*bin*&amp;quot; { block }
        location &amp;quot;/*config*&amp;quot; { block }
        location &amp;quot;/*logs*&amp;quot; { block }
        location &amp;quot;/*temp*&amp;quot; { block }
        location &amp;quot;/*tests*&amp;quot; { block }
        location &amp;quot;/*install*&amp;quot; { block }
        # all other php request sent to FPM socket
        location &amp;quot;*.php*&amp;quot; {
                fastcgi socket &amp;quot;/run/php-fpm.sock&amp;quot;
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/httpd.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Link the PHP libraries.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for f in `ls /etc/php-8.2.sample`; do ln -s /etc/php-8.2.sample/${f} /etc/php-8.2/${f}; done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add a custom.ini file for some tweaks&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; Custom PHP settings
; override the defaults
file_uploads = On
upload_max_filesize = 100M
max_file_uploads = 20
post_max_size = 100M
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/php-8.2/custom.ini&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally enable web email&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rcctl enable php82_fpm httpd
rcctl start php82_fpm httpd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To confirm things are working connect to &lt;strong&gt;https://mail.example.org/&lt;/strong&gt; and login with the LDAP user mail address and password.&lt;/p&gt;
&lt;h3 id=&#34;native-email-client&#34;&gt;Native email client&lt;/h3&gt;
&lt;p&gt;Similar to the web email client, dedicate email clients use SMTP and IMAP to send and access email.&lt;/p&gt;
&lt;h4 id=&#34;establishing-trust-in-our-ca&#34;&gt;Establishing trust in our CA&lt;/h4&gt;
&lt;p&gt;Recall we are using our own certificate authority to secure our servers. We will need to load the CA certificate into the clients / systems so that our certificate authority is trusted. The trusted certificate can be download from &lt;strong&gt;https://certauth.example.org/roots.pem&lt;/strong&gt;. Once downloaded, follow instructions of the device / client to trust the certificate.&lt;/p&gt;
&lt;h4 id=&#34;setting-up-autodiscover-for-easy-configuration&#34;&gt;Setting up Autodiscover for easy configuration&lt;/h4&gt;
&lt;p&gt;There are several things we can do to assist the different mail clients to find our services and auto-configure themselves properly.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Add a DNS alias for autoconfig.example.org&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;autoconfig IN CNAME mail.example.org.
autodiscover IN CNAME mail.example.org.
_autodiscover._tcp IN SRV 0 0 443 mail.example.org.
_imap.tcp IN SRV 10 1 143 mail.example.org.
_imaps.tcp IN SRV 0 1 993 mail.example.org.
_pop3.tcp IN SRV 0 0 0 .
_pop3s.tcp IN SRV 0 0 0 .
_submission._tcp IN SRV 0 1 587 mail.example.org.
_submissions.tcp IN SRV 0 1 465 mail.example.org.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create an autodiscovery config file that can be fetched from the email server.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-/var/www/htdocs/config-v1.1.xml&#34;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;clientConfig version=&amp;quot;1.1&amp;quot;&amp;gt;
      &amp;lt;emailProvider id=&amp;quot;example.org&amp;quot;&amp;gt;  
         &amp;lt;domain&amp;gt;example.org&amp;lt;/domain&amp;gt;
         &amp;lt;incomingServer type=&amp;quot;imap&amp;quot;&amp;gt;
            &amp;lt;hostname&amp;gt;mail.example.org&amp;lt;/hostname&amp;gt;
            &amp;lt;port&amp;gt;993&amp;lt;/port&amp;gt;
            &amp;lt;socketType&amp;gt;SSL&amp;lt;/socketType&amp;gt;
            &amp;lt;username&amp;gt;%EMAILADDRESS%&amp;lt;/username&amp;gt;
            &amp;lt;authentication&amp;gt;password-cleartext&amp;lt;/authentication&amp;gt;
         &amp;lt;/incomingServer&amp;gt;
         &amp;lt;outgoingServer type=&amp;quot;smtp&amp;quot;&amp;gt;
            &amp;lt;hostname&amp;gt;mail.example.org&amp;lt;/hostname&amp;gt;
            &amp;lt;port&amp;gt;465&amp;lt;/port&amp;gt;
            &amp;lt;socketType&amp;gt;SSL&amp;lt;/socketType&amp;gt;
            &amp;lt;username&amp;gt;%EMAILADDRESS%&amp;lt;/username&amp;gt;
            &amp;lt;authentication&amp;gt;password-cleartext&amp;lt;/authentication&amp;gt;  
         &amp;lt;/outgoingServer&amp;gt;  
      &amp;lt;/emailProvider&amp;gt;
&amp;lt;/clientConfig&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&#34;thunderbird-client&#34;&gt;Thunderbird client&lt;/h4&gt;
&lt;p&gt;When the Thunderbird client first starts it will ask to create an account. Normally this is exactly what we want to happen. However, since Thunderbird does not use the system certificate store, we need to add our CA certificate to the trusted store on Thunderbird.&lt;/p&gt;
&lt;p&gt;To do this exit the account creation dialogue.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;assets/self-hosted-episode-8-email/capture_2025-09-06_12-43-57.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Open the Setting dialogue&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;assets/self-hosted-episode-8-email/capture_2025-09-06_12-42-24.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Select Privacy &amp;amp; Security and scroll down to find the Manage Certificates button and click it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;assets/self-hosted-episode-8-email/capture_2025-09-06_11-06-53.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;In the Certificate Manager dialog select the Authorities tab then Import button.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;assets/self-hosted-episode-8-email/capture_2025-09-06_11-04-03.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Select the certificate PEM file that contains the our CA certificate. Then click Open.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;assets/self-hosted-episode-8-email/capture_2025-09-06_11-06-16.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Select the check boxes to trust this certificate for web sites and email users. Then click OK button.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;assets/self-hosted-episode-8-email/capture_2025-09-06_11-06-35.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;With that complete, click on the gear icon again and select Add Mail Account to start the account setup process again.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;assets/self-hosted-episode-8-email/capture_2025-09-06_12-04-57.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Enter your username email address and password into the boxes and click Continue. Thunderbird will try to guess your sites settings.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;assets/self-hosted-episode-8-email/capture_2025-09-21_11-06-20.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;The settings should come back with something similar. However the username does not have the domain. To correct this click the Configure Manually link.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;assets/self-hosted-episode-8-email/capture_2025-09-21_11-16-23.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Retype the the email in the username field. Then click the Done button.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;assets/self-hosted-episode-8-email/capture_2025-09-21_11-20-44.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Finally you should receive the Account successfully created window. Click Finish button&lt;/p&gt;
&lt;p&gt;You should now be able to send and receive emails... to yourself. In the next episode we will build the proxy server that will allow us to send and receive emails with the outside world.&lt;/p&gt;
&lt;h1 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;If you made it to here, congratulations! You now have a working email server free from spying and corporate policies that are aggressively against you.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Self Hosted: Episode 6 - Authentication</title>
    <link href="https://obtusenet.com/blog/self-hosted-episode-6-authentication.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/self-hosted-episode-6-authentication.html</id>
    <published>2025-10-11T05:00:43Z</published>
    <updated>2026-01-22T22:42:32Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>Software used: * FreeBSD 14.2 (jail) * OpenLDAP 2.6 (from ports) It has been said: &#34;Three people can keep a secret, if two are dead.&#34; Fortunately, it doesn&#39;t need to come to that.</summary>
    <category term="self-hosted"/>
    <category term="ldap"/>
    <category term="freebsd"/>
    <content type="html">&lt;h3 id=&#34;software-used&#34;&gt;Software used:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;FreeBSD 14.2 (jail)&lt;/li&gt;
&lt;li&gt;OpenLDAP 2.6 (from ports)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;It has been said: &amp;quot;Three people can keep a secret, if two are dead.&amp;quot; Fortunately, it doesn&#39;t need to come to that.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;authentication&#34;&gt;Authentication&lt;/h1&gt;
&lt;p&gt;As with many things in computing there needs to be a source of truth. This is true for user accounts. Where we have one account, we will need more. Good, bad, or indifferent, the defacto way of keeping user and group information is using LDAP.&lt;/p&gt;
&lt;h2 id=&#34;update-dns-with-new-address&#34;&gt;Update DNS with new address&lt;/h2&gt;
&lt;p&gt;To begin with we will need to be able to resolve the new server. To do this update the &lt;strong&gt;example.org&lt;/strong&gt; and &lt;strong&gt;30.168.192.in-addr.arpa&lt;/strong&gt; zone files on the DNS servers respectively.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ldap     IN      A      192.168.30.13
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;record for example.org zone file&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;13      IN      PTR      ldap.example.org.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;record for the 30.168.192.in-addr.arpa zone file&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Don&#39;t forget to update the zone serial number and restart the &lt;strong&gt;named&lt;/strong&gt; service.&lt;/p&gt;
&lt;h2 id=&#34;setup-ldap-server&#34;&gt;Setup LDAP server&lt;/h2&gt;
&lt;p&gt;Just like with the CA server, the LDAP server will be setup using a jail. If you have not already setup the Jail infrastructure review &lt;a href=&#34;https://obtusenet.com/blog/self-hosted-episode-3-dns-dhcp.html&#34;&gt;Episode 3 - Setting up FreeBSD Jail&lt;/a&gt;. For this jail we will be using the following file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# LDAP server jail file

ldap {
	# STARTUP / LOGGING
	exec.start = &amp;quot;/bin/sh /etc/rc&amp;quot;;
	exec.stop = &amp;quot;/bin/sh /etc/rc.shutdown&amp;quot;;
	exec.consolelog = &amp;quot;/var/log/jail_console_${name}.log&amp;quot;;

	# PERMISSIONS
	allow.raw_sockets;
	exec.clean;
	mount.devfs;
	allow.reserved_ports = true;

	# HOSTNAME / PATH
	host.hostname = &amp;quot;${name}&amp;quot;;
	path = &amp;quot;/usr/local/jails/containers/${name}&amp;quot;;

	# NETWORK
	vnet;
	vnet.interface = &amp;quot;${epair}b&amp;quot;;

	# NETWORKS/INTERFACES
	$id = &amp;quot;12&amp;quot;;
	$ip = &amp;quot;192.168.30.${id}&amp;quot;;
	$mask = &amp;quot;255.255.255.0&amp;quot;;
	$gateway = &amp;quot;192.168.30.1&amp;quot;;
	$bridge = &amp;quot;net-dmz&amp;quot;;
	$epair = &amp;quot;epair${id}&amp;quot;;

	exec.prestart += &amp;quot;ifconfig ${epair} create up&amp;quot;;
	exec.prestart += &amp;quot;ifconfig ${epair}a up descr jail:${name}&amp;quot;;
	exec.prestart += &amp;quot;ifconfig ${bridge} addm ${epair}a up&amp;quot;;
	exec.start    += &amp;quot;ifconfig ${epair}b ${ip} netmask ${mask} up&amp;quot;;
	exec.start    += &amp;quot;route add default ${gateway}&amp;quot;;
	exec.poststop += &amp;quot;ifconfig ${bridge} deletem ${epair}a&amp;quot;;
	exec.poststop += &amp;quot;ifconfig ${epair}a destroy&amp;quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;start-the-ldap-jail&#34;&gt;Start the LDAP jail&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;service jail start ldap
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;install-the-ldap-server-packages&#34;&gt;Install the LDAP server packages&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pkg -j ldap update
pkg -j ldap install -y openldap-server openldap-client
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;connect-to-the-new-jail&#34;&gt;Connect to the new jail&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;jexec ldap /bin/sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;edit-ldap-config-file&#34;&gt;Edit ldap config file&lt;/h3&gt;
&lt;p&gt;The LDAP server has a server configuration files called &lt;strong&gt;slapd.conf&lt;/strong&gt; located in the &lt;strong&gt;/usr/local/etc/openldap&lt;/strong&gt; directory. This file consists of several parts defined in the config file. Each part has been commented.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Import Schemas to support
include         /usr/local/etc/openldap/schema/core.schema
include         /usr/local/etc/openldap/schema/cosine.schema
include         /usr/local/etc/openldap/schema/inetorgperson.schema
include         /usr/local/etc/openldap/schema/nis.schema

# specify paths for restarting
pidfile         /var/run/openldap/slapd.pid
argsfile        /var/run/openldap/slapd.args

# load the DB module for storate
modulepath      /usr/local/libexec/openldap
moduleload      back_mdb

# Set access controls on different keys
# NOTE: rootdn can always read and write EVERYTHING!
# Access Control
access to attrs=userPassword
        by group/groupOfNames/member=&amp;quot;ou=services,dc=example,dc=org&amp;quot; read
        by self write
        by * none
access to attrs=description
        by group/groupOfNames/member=&amp;quot;ou=services,dc=example,dc=org&amp;quot; read
        by self write
        by * none

# TLS
TLSCACertificateFile /usr/local/share/certs/example.org_root_ca.pem
TLSCertificateFile /usr/local/etc/ssl/ldap_fullchain.pem
TLSCertificateKeyFile /usr/local/etc/ssl/ldap.key
#TLSVerifyClient never

# enable database config definitions
database config
# configure storage DB settings
database        mdb
maxsize         1073741824
suffix          &amp;quot;dc=example,dc=org&amp;quot;
rootdn          &amp;quot;cn=ldapadmin,dc=example,dc=org&amp;quot;
rootpw          change_me_please!
directory       /var/db/openldap-data

# Indices to maintain
index   objectClass     eq
index   cn,sn,mail      pres,eq,approx,sub
index   uid             eq

# enable DB monitoring
database monitor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/openldap/slapd.conf&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;get-ldap-certificate&#34;&gt;Get LDAP certificate&lt;/h3&gt;
&lt;p&gt;Get a new certificate from the CA for the LDAP server&lt;/p&gt;
&lt;p&gt;Add and trust the root cert&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test -d /usr/local/share/certs || mkdir -p /usr/local/share/certs
fetch --no-verify-peer --no-verify-hostname -o /usr/local/share/certs/root_ca.crt https://certauth.example.org/roots.pem
certctl rehash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to install and trust the CA certificate&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Install the acme package&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pkg install -y acme.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to install the acme.sh package&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Generate the certificate.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export CRTP=/usr/local/etc/ssl
test -f $CRTP || mkdir -p $CRTP
acme.sh --issue --standalone -d $(hostname) --cert-file $CRTP/$(hostname -s).crt --key-file $CRTP/$(hostname -s).key --fullchain-file $CRTP/$(hostname -s)_fullchain.pem --server https://certauth.example.org/acme/acme/directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to generate new certifiate&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update LDAP server args&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To allow the LDAP server to listen on both ldap:/// and ldaps:/// update the service flags&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sysrc slapd_flags=&amp;quot;-h &#39;ldapi://%2fvar%2frun%2fopenldap%2fldapi/ ldap:/// ldaps:///&#39;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;update service flags before start&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;start-the-ldap-server&#34;&gt;Start the LDAP server&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;service slapd start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to start the ldap server&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;forward-logs-to-logging-server&#34;&gt;Forward logs to logging server&lt;/h3&gt;
&lt;p&gt;Create the &lt;strong&gt;/usr/local/etc/syslog.d&lt;/strong&gt; directory if it doesn&#39;t exist.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ -f /usr/local/etc/syslog.d ] || mkdir -p /usr/local/etc/syslog.d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to create the syslog.d directory&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next, create the syslog config file&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Send log messages to log server
*.*                                          @192.168.30.12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/syslog.d/remote.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally, restart syslogd&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service syslogd restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to restart syslogd&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;ldap-setup&#34;&gt;LDAP Setup&lt;/h2&gt;
&lt;p&gt;Now that the server is running we need to setup the structure and add some user accounts.&lt;/p&gt;
&lt;h3 id=&#34;setup-structure&#34;&gt;Setup structure&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#
# Simple LDAP Schema
#
dn: dc=example,dc=org
objectclass: dcObject
objectclass: organization
dc: example
o: example.org LDAP Server
description: Root entry for example.org

# First level
dn: ou=users,dc=example,dc=org
objectclass: organizationalUnit
ou: users
description: All people in organization

dn: ou=groups,dc=example,dc=org
objectclass: organizationalUnit
ou: groups
description: All groups in organization

dn: ou=domains,dc=example,dc=org
objectclass: organizationalUnit
ou: domains
description: All domains in organization

dn: ou=serviceaccounts,dc=example,dc=org
objectclass: organizationalUnit
ou: serviceaccounts
description: All sevice accounts in organization
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;init_schema.ldif&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Load the ldif data to the openldap server&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ldapadd -W -H ldap://127.0.0.1:389 -D &amp;quot;cn=ldapadmin,dc=example,dc=org&amp;quot; -f init_schema.ldif
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;add-a-user-account&#34;&gt;Add a user account&lt;/h3&gt;
&lt;p&gt;Now the server is up and running let&#39;s add a user account. Updates to the LDAP server are made using LDIF files. Create a LDIF file from the example below with the appropriate user information.&lt;/p&gt;
&lt;p&gt;Create a SSHA password&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export PASS=letmein
export RND_SALT=`openssl rand -base64 6`
export PASS_HASH=`echo -n &amp;quot;$PASS$RND_SALT&amp;quot; | openssl dgst -sha1 -binary | openssl enc -base64 -A`
export LDAP_PASS_HASH=`(echo -n &amp;quot;$PASS_HASH&amp;quot; | openssl base64 -d -A; echo -n &amp;quot;$RND_SALT&amp;quot;;) | openssl enc -base64 -A | awk &#39;{print &amp;quot;{SSHA}&amp;quot;$0 }&#39;`
echo $LDAP_PASS_HASH
unset RND_SALT
unset PASS_HASH
unset LDAP_PASS_HASH
unset PASS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;shell to create a SSHA password&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Edit the ldif as needed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# Create User alice
#
dn: uid=2e86409ff65ff189,ou=users,dc=example,dc=org
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: posixAccount
uid: 2e86409ff65ff189
cn: alice
sn: user
uidNumber: 5000
gidNumber: 5000
homeDirectory: /home/alice
givenName: alice
displayName: alice user
mail: alice@example.org
mail: @example.org
userPassword:: e1NTSEF9WmdwcCt6ZE90UC9KcEhXNjIxUGd5Wm5MZlV0RGExUjZNVzk2V0E9PQ=
 =
description: $2b$10$JxGxv8v2hGw4/dytUMRjbOlHrzg7wyCRqZX6YtFdAEaruH.OsIvWm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;add_user.ldif&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Load the user account.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ldapadd -W -H ldap://127.0.0.1:389 -D &amp;quot;cn=ldapadmin,dc=example,dc=org&amp;quot; -f add_user.ldif
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;modify-a-user-account&#34;&gt;Modify a user account&lt;/h3&gt;
&lt;p&gt;From time to time updates/changes are going to need to be made to a user account. Updates to LDAP also use LDIF files to preform updates. Use the follow script to create an update LDIF.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dn: uid=juser,ou=users,dc=example,dc=org
changetype: modify
replace: userPassword
userPassword: {SSHA}adsfasdfasdfadfasdfadfas
-
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;update_user.ldif&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Load the updated password&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ldapmodify -W -H ldap://127.0.0.1:389 -D &amp;quot;cn=ldapadmin,dc=example,dc=org&amp;quot; -f update_user.ldif
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;delete-a-user-account&#34;&gt;Delete a user account&lt;/h3&gt;
&lt;p&gt;User accounts that are no longer needed should be deleted. Use the following script to help delete an old user account.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uid=juser,ou=users,dc=example,dc=org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;delete_user.ldif&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Load the delete ldif&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ldapdelete -W -H ldap://127.0.0.1:389 -D &amp;quot;cn=ldapadmin,dc=dc=example,dc=org&amp;quot; -f delete_user.ldif
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;an-easier-way&#34;&gt;An easier way...&lt;/h3&gt;
&lt;p&gt;While we can manipulate the LDAP users and attributes by hand I have created a few scripts that will make the daily maintenance of users much easier. These scripts can be fetched from &lt;a href=&#34;https://github.com/mgraves00/misc-scripts/tree/master/ldap&#34;&gt;mgraves00/misc-scripts/ldap&lt;/a&gt;.&lt;/p&gt;
&lt;h1 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;While this seems like a lot of work to setup a self hosted environment for just one person (and it is), once we have the infrastructure setup, adding new self hosted service will be much easier.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Self Hosted: Episode 5 - Logging</title>
    <link href="https://obtusenet.com/blog/self-hosted-episode-5-logging.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/self-hosted-episode-5-logging.html</id>
    <published>2025-10-04T05:00:01Z</published>
    <updated>2025-10-11T13:01:54Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>Software Used: * FreeBSD 14.2 (jail) Introduction If you can&#39;t observe what is happening then you can&#39;t find and fix what is broken Setting up a logging server at this point may seem premature.</summary>
    <category term="self-hosted"/>
    <category term="logging"/>
    <category term="jail"/>
    <category term="freebsd"/>
    <content type="html">&lt;h3 id=&#34;software-used&#34;&gt;Software Used:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;FreeBSD 14.2 (jail)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&#34;introduction&#34;&gt;Introduction&lt;/h1&gt;
&lt;p&gt;If you can&#39;t observe what is happening then you can&#39;t find and fix what is broken&lt;/p&gt;
&lt;p&gt;Setting up a logging server at this point may seem premature. But as we move forward, things will get more complicated and more interdependent. Having a logging server that can handle logs from all services will aide is troubleshooting what is going wrong.&lt;/p&gt;
&lt;h1 id=&#34;logging-discussion&#34;&gt;Logging Discussion&lt;/h1&gt;
&lt;p&gt;What is it we want to get out of the logging server? At this point we just need a place to store logs from other systems. There are many logging servers that have nice graphical interfaces. We may get to self hosting one of those in the future, but for now we want a simple, syslog like logging server. Each log should be timestamped with the time it arrived and the hostname of the server sending the log. If no server hostname is available, we should us the IP address. In addition the syslog server should support TLS encrypted logs.&lt;/p&gt;
&lt;h2 id=&#34;update-dns-with-new-address&#34;&gt;Update DNS with new address&lt;/h2&gt;
&lt;p&gt;To begin with we will need to be able to resolve the new log server. To do this update the &lt;strong&gt;example.org&lt;/strong&gt; and &lt;strong&gt;30.168.192.in-addr.arpa&lt;/strong&gt; zone files on the DNS servers respectively.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logger     IN      A      192.168.30.12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;record for example.org zone file&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;12      IN      PTR      logger.example.org.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;record for the 30.168.192.in-addr.arpa zone file&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Don&#39;t forget to update the zone serial number and restart the &lt;strong&gt;named&lt;/strong&gt; service.&lt;/p&gt;
&lt;h2 id=&#34;setting-up-the-syslog-jail&#34;&gt;Setting up the syslog jail&lt;/h2&gt;
&lt;p&gt;The syslog server will be created using a FreeBSD jail. If you have not already setup the Jail infrastructure review &lt;a href=&#34;https://obtusenet.com/blog/self-hosted-episode-3-dns-dhcp.html&#34;&gt;Episode 3 - Setting up FreeBSD Jail&lt;/a&gt;. For this jail we will be using the following file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Log server jail file

logger {
	# STARTUP / LOGGING
	exec.start = &amp;quot;/bin/sh /etc/rc&amp;quot;;
	exec.stop = &amp;quot;/bin/sh /etc/rc.shutdown&amp;quot;;
	exec.consolelog = &amp;quot;/var/log/jail_console_${name}.log&amp;quot;;

	# PERMISSIONS
	allow.raw_sockets;
	exec.clean;
	mount.devfs;
	allow.reserved_ports = true;

	# HOSTNAME / PATH
	host.hostname = &amp;quot;${name}&amp;quot;;
	path = &amp;quot;/usr/local/jails/containers/${name}&amp;quot;;

	# NETWORK
	vnet;
	vnet.interface = &amp;quot;${epair}b&amp;quot;;

	# NETWORKS/INTERFACES
	$id = &amp;quot;12&amp;quot;;
	$ip = &amp;quot;192.168.30.${id}&amp;quot;;
	$mask = &amp;quot;255.255.255.0&amp;quot;;
	$gateway = &amp;quot;192.168.30.1&amp;quot;;
	$bridge = &amp;quot;net-dmz&amp;quot;;
	$epair = &amp;quot;epair${id}&amp;quot;;

	exec.prestart += &amp;quot;ifconfig ${epair} create up&amp;quot;;
	exec.prestart += &amp;quot;ifconfig ${epair}a up descr jail:${name}&amp;quot;;
	exec.prestart += &amp;quot;ifconfig ${bridge} addm ${epair}a up&amp;quot;;
	exec.start    += &amp;quot;ifconfig ${epair}b ${ip} netmask ${mask} up&amp;quot;;
	exec.start    += &amp;quot;route add default ${gateway}&amp;quot;;
	exec.poststop += &amp;quot;ifconfig ${bridge} deletem ${epair}a&amp;quot;;
	exec.poststop += &amp;quot;ifconfig ${epair}a destroy&amp;quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/jail.conf.d/logger.conf&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;start-the-log-server-jail&#34;&gt;Start the log server jail&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;service jail start logger
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;configure-the-log-server&#34;&gt;Configure the Log server&lt;/h3&gt;
&lt;p&gt;First we will create the &lt;strong&gt;/usr/local/etc/syslog.d&lt;/strong&gt; directory if not already created.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ -f /usr/local/etc/syslog.d ] || mkdir -p /usr/local/etc/syslog.d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to create the syslog.d directory if it doesn&#39;t exist&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next create the directory to keep the logs in&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ -f /var/log/remote ] || mkdir -p /var/log/remote
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to create the remote directory if it doesn&#39;t exist&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next create the config file for receiving the remote logs. The config file will be kept in the &lt;strong&gt;/usr/local/etc/syslog.d&lt;/strong&gt; directory&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Send any remote logs to a the remote file
:hostname, startswith, &amp;quot;192.168&amp;quot;
*.*                               /var/log/remote/remote.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/syslog.d/remote.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally we will update the syslogd flags&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sysrc syslogd_flags=&amp;quot;-n -O rcf3164 &amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we do not want the log file growing infinitely, so we need to rotate the log every so often. Everyday seems right. Let&#39;s also keep a week of old logs. Adjust the time frame and retention to suit your needs.&lt;/p&gt;
&lt;p&gt;First we create a directory for our newsyslog config file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ -f /usr/local/etc/newsyslog.conf.d ] || mkdir -p /usr/local/etc/newsyslog.conf.d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to create the newsyslog.d directory&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally we create the config file. The newsyslog command gets run every hour so we do not need to make any additional changes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/var/log/remote/remote.log     640 7 * $D0 CNTZ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/newsyslog.conf.d/remote.conf&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;allocate-the-certificate&#34;&gt;Allocate the certificate&lt;/h3&gt;
&lt;p&gt;Review &lt;em&gt;Episode 4 section Deploy Root cert and pull new cert&lt;/em&gt; to add a certificate to the logging server.&lt;/p&gt;
&lt;h3 id=&#34;enable-and-start-the-log-server&#34;&gt;Enable and start the log server&lt;/h3&gt;
&lt;p&gt;Finally let&#39;s start the log server. Since syslog may already be started by default jail we will call the restart command to restart if necessary.&lt;/p&gt;
&lt;p&gt;Then we will create the config file&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service syslogd restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to restart syslogd&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;update-logging-in-existing-servers&#34;&gt;Update Logging in existing servers&lt;/h2&gt;
&lt;h3 id=&#34;firewall&#34;&gt;Firewall&lt;/h3&gt;
&lt;p&gt;First login to the firewall and su(1) to root. Update the &lt;strong&gt;/etc/syslog.conf&lt;/strong&gt; file and add the following line to the bottom of the file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Send log messages to log server
*.*                                              @192.168.30.12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;append to /etc/syslog.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally, restart the syslogd service&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rcctl restart syslogd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to restart syslogd&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;dnsdhcp-server&#34;&gt;DNS/DHCP server&lt;/h3&gt;
&lt;p&gt;First, login to the DNS/DHCP server and su(1) to root. Create the /usr/local/etc/syslog.d directory if it doesn&#39;t exist.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ -f /usr/local/etc/syslog.d ] || mkdir -p /usr/local/etc/syslog.d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to create syslog.d directory&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next create the syslog config file&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Send log messages to log server
*.*                                          @192.168.30.12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/syslog.d/remote.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally restart syslogd&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service syslogd restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to restart syslogd&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;certificate-authority-server&#34;&gt;Certificate Authority server&lt;/h3&gt;
&lt;p&gt;First, login to the CA server and su(1) to root. Create the /usr/local/etc/syslog.d directory if it doesn&#39;t exist.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ -f /usr/local/etc/syslog.d ] || mkdir -p /usr/local/etc/syslog.d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to create syslog.d directory&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next create the syslog config file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Send log messages to log server
*.*                                          @192.168.30.12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/syslog.d/remote.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally restart syslogd&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service syslogd restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to restart syslogd&lt;/em&gt;&lt;/p&gt;
&lt;h1 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Now there is an issue with something, there is only one place to go to review the logs for all servers.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Self Hosted: Episode 4 - Certificates</title>
    <link href="https://obtusenet.com/blog/self-hosted-episode-4-certificates.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/self-hosted-episode-4-certificates.html</id>
    <published>2025-09-27T05:00:30Z</published>
    <updated>2026-02-14T21:01:50Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>Software used: * FreeBSD 14.2 (jail) * Step CA 0.28.1 (from ports) Updated: 2026-02-14 Introduction If it isn&#39;t DNS...</summary>
    <content type="html">&lt;h3 id=&#34;software-used&#34;&gt;Software used:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;FreeBSD 14.2 (jail)&lt;/li&gt;
&lt;li&gt;Step CA 0.28.1 (from ports)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Updated: 2026-02-14&lt;/p&gt;
&lt;h1 id=&#34;introduction&#34;&gt;Introduction&lt;/h1&gt;
&lt;p&gt;If it isn&#39;t DNS... then it&#39;s certificates!&lt;/p&gt;
&lt;h1 id=&#34;certificates-are-simple-but-its-complicated&#34;&gt;Certificates are simple... but it&#39;s complicated!&lt;/h1&gt;
&lt;p&gt;Certificates themselves are very simple... ignoring the all the crypto algorithms, hashes, etc. They are just two complementary long numbers that when put through those algorithms are able encrypt and decrypt messages. They are stored in two different files called the &amp;quot;certificate&amp;quot;/&amp;quot;public certificate&amp;quot;/&amp;quot;public cert&amp;quot;/&amp;quot;pem file&amp;quot;, etc and the &amp;quot;private cert&amp;quot;/&amp;quot;private key&amp;quot;/&amp;quot;key file&amp;quot;.&lt;/p&gt;
&lt;p&gt;That&#39;s it! Simple!&lt;/p&gt;
&lt;p&gt;Here&#39;s where things get complicated. There is this notion of &amp;quot;chain of trust&amp;quot;. This chain is made up of one or more links. Each link must be trusted in order to trust the cert at the end of the chain. (example to follow) The head of the chain is called the &amp;quot;root&amp;quot;. Root(s) (and there are many) &amp;quot;sign&amp;quot; a specific certificate file. This signing says to anyone receiving this certificate file, that if they &amp;quot;trust the root&amp;quot; they can then &amp;quot;trust the certificate file&amp;quot;. This trust can go many layers deep.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;:  &lt;em&gt;You trust Jim. (I&#39;m not trying to figure out why you trust Jim, but you do. I mean, Jim is a good guy. Has good hair and a winning smile... so why not). Now Jim has given Sally a card that says that she can be trusted too. In addition Sally&#39;s card says that she can issue cards to others that she trusts. Sally gives Larry a card that she trusts him. Now when Larry shows you his card, should you trust Larry? Well you see that Larrys card is signed by Sally and Sallys card is signed by Jim. So if you trust Jim, then you trust Sally and thus you can trust Larry.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now what happens if you don&#39;t trust Jim? Pretty simple, you don&#39;t trust anyone who Jim has extended his trust to. Or what happens if suddenly Sally becomes untrustworthy? Now anyone she extended her trust too is not untrust worthy.&lt;/p&gt;
&lt;p&gt;The real question is how do you know who to trust. Fortunately?!, device and OS manufactures have created a list of trusted sources (referred to as Certificate Authorities or CAs)... and it&#39;s a long list. If you had to choose, you probably wouldn&#39;t have as long a list as this. Because anyone in this list is considered totally trust worthy and anyone they say is trustworthy is also totally trust worthy. (most probably are... but not all!)&lt;/p&gt;
&lt;p&gt;So who can we really trust?!?! Ourselves! Now, let&#39;s &lt;strong&gt;create our own Certificate Authority!!!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Wait! What? Why would we do this?&lt;/p&gt;
&lt;p&gt;Three reasons. First is that all the &amp;quot;default&amp;quot; CAs are public and only issue certificates to public reachable entities. (We will eventually make use of these public CAs). And our Self Hosted environment will not all be accessible to the greater Internet. (If we setup our firewall correctly!) Second, certificates from these public CAs can cost money. Sometime lots of money. Third, eventually we will want to do what is called mutual authentication (but more on that later).&lt;/p&gt;
&lt;p&gt;Now in order to make running our own certificate authority we will want to automate as much of the certificate functions as possible. The issuing, expiration, revocation, etc should be automated. Fortunately, the ACME protocol, made popular by, &lt;a href=&#34;https://letsencrypt.org/&#34;&gt;&lt;strong&gt;Lets Encrypt&lt;/strong&gt;&lt;/a&gt; can be used on our systems to automatically allocated and update certificates on our internal servers as long as our CA supports the ACME protocol (and it does).&lt;/p&gt;
&lt;h2 id=&#34;update-dns-with-new-address&#34;&gt;Update DNS with new address&lt;/h2&gt;
&lt;p&gt;To begin with we will need to be able to resolve the new certificate authority. To do this update the &lt;strong&gt;example.org&lt;/strong&gt; and &lt;strong&gt;30.168.192.in-addr.arpa&lt;/strong&gt; zone files on the DNS servers respectively.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;certauth     IN      A      192.168.30.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;record for example.org zone file&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;11      IN      PTR      certauth.example.org.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;record for the 30.168.192.in-addr.arpa zone file&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Don&#39;t forget to update the zone serial number and restart the &lt;strong&gt;named&lt;/strong&gt; service.&lt;/p&gt;
&lt;h2 id=&#34;setup-the-ca&#34;&gt;Setup the CA&lt;/h2&gt;
&lt;p&gt;The CA server will be created using a FreeBSD jail. If you have not already setup the Jail infrastructure review &lt;a href=&#34;https://obtusenet.com/blog/self-hosted-episode-3-dns-dhcp.html&#34;&gt;Episode 3 - Setting up FreeBSD Jail&lt;/a&gt;. For this jail we will be using the following file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Certificate server jail file

certauth {
	# STARTUP / LOGGING
	exec.start = &amp;quot;/bin/sh /etc/rc&amp;quot;;
	exec.stop = &amp;quot;/bin/sh /etc/rc.shutdown&amp;quot;;
	exec.consolelog = &amp;quot;/var/log/jail_console_${name}.log&amp;quot;;

	# PERMISSIONS
	allow.raw_sockets;
	exec.clean;
	mount.devfs;
    allow.reserved_ports;

	# HOSTNAME / PATH
	host.hostname = &amp;quot;${name}&amp;quot;;
	path = &amp;quot;/usr/local/jails/containers/${name}&amp;quot;;

	# NETWORK
	vnet;
	vnet.interface = &amp;quot;${epair}b&amp;quot;;

	# NETWORKS/INTERFACES
	$id = &amp;quot;11&amp;quot;;
	$ip = &amp;quot;192.168.30.${id}&amp;quot;;
	$mask = &amp;quot;255.255.255.0&amp;quot;;
	$gateway = &amp;quot;192.168.30.1&amp;quot;;
	$bridge = &amp;quot;net-dmz&amp;quot;;
	$epair = &amp;quot;epair${id}&amp;quot;;

	exec.prestart += &amp;quot;ifconfig ${epair} create up&amp;quot;;
	exec.prestart += &amp;quot;ifconfig ${epair}a up descr jail:${name}&amp;quot;;
	exec.prestart += &amp;quot;ifconfig ${bridge} addm ${epair}a up&amp;quot;;
	exec.start    += &amp;quot;ifconfig ${epair}b ${ip} netmask ${mask} up&amp;quot;;
	exec.start    += &amp;quot;route add default ${gateway}&amp;quot;;
	exec.poststop += &amp;quot;ifconfig ${bridge} deletem ${epair}a&amp;quot;;
	exec.poststop += &amp;quot;ifconfig ${epair}a destroy&amp;quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/jail.conf.d/certauth.conf&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;start-the-jail&#34;&gt;Start the Jail&lt;/h3&gt;
&lt;p&gt;Start the certauth jail.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service jail start certauth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to start the jail&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;install-the-ca-service-in-the-jail&#34;&gt;Install the CA service in the jail&lt;/h3&gt;
&lt;p&gt;Once the jail has been started we need to update the FreeBSD package system and install the CA packages into the jail.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pkg -j certauth update
pkg -j certauth install -y step-certificates step-cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to install the CA software&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;connect-to-the-new-jail&#34;&gt;Connect to the new jail&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;jexec certauth /bin/sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to enter the jail&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;generate-the-configuration&#34;&gt;Generate the configuration&lt;/h3&gt;
&lt;p&gt;First export some common used vars and check if directories exist&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export SDIR=/usr/local/etc/step
export STEPPATH=$SDIR/ca
export FQDN=$(hostname)
test -d $SDIR || mkdir -p $SDIR
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to create the CA directories&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next generate the password key. This is the key that will be used to encrypt all cert passwords. (KEEP IT SAFE!) It is not out of the realm to have this key be stored on some sort of hardware token. The process for doing that is outside of this article.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openssl rand -base64 20 &amp;gt; $SDIR/password.txt
chown root:wheel $SDIR/password.txt
chmod 400 $SDIR/password.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to create the CA password&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next initialize the the certificate authority. This will create the root and intermediate certificates.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test -f $SDIR/ca/config/ca.json || /usr/local/bin/step ca init --address :8443 --deployment-type standalone --dns $FQDN --name &amp;quot;Root CA&amp;quot; --with-ca-url https://$FQDN --password-file $SDIR/password.txt --provisioner certificate_authority --provisioner-password-file $SDIR/password.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to create and initialize the CA&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Setup the ACME provisioner.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/usr/local/bin/step ca provisioner add acme --type ACME
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to enable an ACME provisioner in the CA&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now setup nginx to act as a proxy.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#user  nobody;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;
        server_name  certauth.example.org;
        return 302 https://$host$request_uri;
    }

    # HTTPS server
    server {
        listen       443 ssl;
        server_name  cetauth.example.org;

        ssl_certificate      /usr/local/etc/ssl/certauth.example.org.crt;
        ssl_certificate_key  /usr/local/etc/ssl/certauth.example.org.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

        location / {
            proxy_pass   https://127.0.0.1:8443;
            proxy_set_header Host &amp;quot;certauth.example.org&amp;quot;;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/nginx/nginx.conf&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;start-the-ca-service&#34;&gt;Start the CA service&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;service step_ca enable
service step_ca start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to enable and start the CA&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;create-a-web-server-certificate&#34;&gt;Create a web server certificate&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;test -f /usr/local/etc/ssl/certauth.example.org.crt || STEPPATH=/usr/local/etc/step/ca /usr/local/bin/step ca certificate ${FQDN} /usr/local/etc/ssl/${FQDN}.crt /usr/local/etc/ssl/${FQDN}.key --ca-config /usr/local/etc/step/ca/config/ca.json --offline --password-file /usr/local/etc/step/password.txt --san ${FQDN} --provisioner certificate_authority
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to create web certificate&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now start the web server&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service nginx enable
service nginx start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to start nginx web server&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;deploy-the-new-root-certificate&#34;&gt;Deploy the new root certificate&lt;/h3&gt;
&lt;p&gt;To deploy the new root certificate to the certificate host (can confirm things are working with nginx proxy) first fetch the cert into the /&lt;strong&gt;usr/local/share/certs&lt;/strong&gt; directory.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test -d /usr/local/share/certs || mkdir -p /usr/local/share/certs
fetch --no-verify-peer --no-verify-hostname -o /usr/local/share/certs/root_ca.crt https://certauth.example.org/roots.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to fetch the root CA certificate&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally update the system certificate store&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;certctl rehash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to install and trust the new CA certificate&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;deploy-root-cert-and-pull-new-cert&#34;&gt;Deploy root cert and pull new cert&lt;/h2&gt;
&lt;h3 id=&#34;dns-server&#34;&gt;DNS server&lt;/h3&gt;
&lt;p&gt;Add and trust the root cert&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test -d /usr/local/share/certs || mkdir -p /usr/local/share/certs
fetch --no-verify-peer --no-verify-hostname -o /usr/local/share/certs/root_ca.crt https://certauth.example.org/roots.pem
certctl rehash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to install and trust the new CA certificate&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Install the acme package&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pkg install -y acme.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to install the acme.sh package&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Generate the certificate.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export CRTP=/usr/local/etc/ssl
test -f $CRTP || mkdir -p $CRTP
acme.sh --issue --standalone -d $(hostname) --cert-file $CRTP/$(hostname -s).crt --key-file $CRTP/$(hostname -s).key --fullchain-file $CRTP/$(hostname -s)_fullchain.pem --server https://certauth.example.org/acme/acme/directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Commands to get a new certificate issued.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Update cron to renew the certificate&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &#39;0 0 * * * &amp;quot;/usr/local/sbin/acme.sh --cron --home /root/.acme.sh&amp;quot; &amp;gt; /dev/null&#39; &amp;gt; /etc/cron.d/acme_sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to deploy a cron job to update certificate when it expires.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;firewall&#34;&gt;Firewall&lt;/h3&gt;
&lt;p&gt;Add and trust the root cert.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ftp -S dont -o /etc/ssl/example.org_root_ca.pem https://192.168.30.11/roots.pem
cat /etc/ssl/example.org_root_ca.pem &amp;gt;&amp;gt; /etc/ssl/cert.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to install and trust the new CA root certificate.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Create the &lt;strong&gt;/etc/acme-client.conf&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
authority example.org {
        api url &amp;quot;https://certauth.example.org/acme/acme/directory&amp;quot;
        account key &amp;quot;/etc/acme/example.org-privkey.pem&amp;quot;
}

domain firewall.example.org {
        domain key &amp;quot;/etc/ssl/private/firewall.key&amp;quot;
        domain full chain certificate &amp;quot;/etc/ssl/firewall.fullchain.pem&amp;quot;
        sign with example.org
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/acme-client.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Create the /etc/httpd.conf&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 
server &amp;quot;*&amp;quot; {
        listen on * port 80
        location &amp;quot;/.well-known/acme-challenge/*&amp;quot; {
                root &amp;quot;/acme&amp;quot;
                request strip 2
        }
        location * {
                block 
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/httpd.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Start the web server&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rcctl enable httpd
rcctl start httpd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to start the web server&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Generate the certificate&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;acme-client $(hostname)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to install a new device certificate&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Update daily cron file to check / renew certificate.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &#39;/usr/sbin/acme-client $(hostname)&#39; &amp;gt;&amp;gt; /etc/daily.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to setup cron jobe to renew certificate automatically&lt;/em&gt;&lt;/p&gt;
&lt;h1 id=&#34;create-a-manual-certificates&#34;&gt;Create a manual certificates&lt;/h1&gt;
&lt;h3 id=&#34;host-certificates&#34;&gt;Host Certificates&lt;/h3&gt;
&lt;p&gt;To manually create a host certificate the following command can be used on the certauth server.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;step certificate create blah.example.org blah.crt blah.key \
  --force --profile=leaf \
  --ca=/usr/local/etc/step/ca/certs/intermediate_ca.crt \
  --ca-key=/usr/local/etc/step/ca/secrets/intermediate_ca_key \
  --ca-password-file=/usr/local/etc/step/password.txt \
  --no-password --insecure --not-after=$((24*390))h

step certificate p12 blah.p12 blah.crt blah.key
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;useremail-certificates&#34;&gt;User/Email Certificates&lt;/h3&gt;
&lt;p&gt;At some time we may want to create a certificate for an email address and not a host. To do this create the following file as a template &#39;&lt;em&gt;email.tpl&lt;/em&gt;&#39;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
 &amp;quot;sans&amp;quot;: {{ toJson .SANs }},
 &amp;quot;keyUsage&amp;quot;: [&amp;quot;digitalSignature&amp;quot; ],
 &amp;quot;extKeyUsage&amp;quot;: [&amp;quot;clientAuth&amp;quot;, &amp;quot;emailProtection&amp;quot; ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;email.tpl&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Then use this command to create the certificate.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;step certificate create oscar@example.org oscar.crt oscar.key \
  --force --ca=/usr/local/etc/step/ca/certs/intermediate_ca.crt \
  --ca-key=/usr/local/etc/step/ca/secrets/intermediate_ca_key \
  --ca-password-file=/usr/local/etc/step/password.txt \
  --no-password --insecure --not-after=$((24*390))h \
  --template=email.tpl

step certificate p12 oscar.p12 oscar.crt oscar.key
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;Now that we have DNS, DHCP, Certificates and the firewall working, can we begin to start thinking about adding something that is actually useful? Well not quite yet... :/&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Self Hosted: Episode 3 - DNS/DHCP/NTP</title>
    <link href="https://obtusenet.com/blog/self-hosted-episode-3-dns-dhcp.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/self-hosted-episode-3-dns-dhcp.html</id>
    <published>2025-09-20T05:00:06Z</published>
    <updated>2026-01-22T16:01:00Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>Used Software: * FreeBSD 14.2 (as jail) * ISC Bind 9.2.0 (from ports) * ISC DHCP 4.4 (from ports) Introduction It all begins with DNS...</summary>
    <category term="self-hosted"/>
    <category term="dns"/>
    <category term="dhcp"/>
    <category term="freebsd"/>
    <category term="jail"/>
    <content type="html">&lt;h3 id=&#34;used-software&#34;&gt;Used Software:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;FreeBSD 14.2 (as jail)&lt;/li&gt;
&lt;li&gt;ISC Bind 9.2.0 (from ports)&lt;/li&gt;
&lt;li&gt;ISC DHCP 4.4 (from ports)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&#34;introduction&#34;&gt;Introduction&lt;/h1&gt;
&lt;p&gt;It all begins with DNS...&lt;/p&gt;
&lt;h1 id=&#34;building-the-jail-environment&#34;&gt;Building the Jail environment&lt;/h1&gt;
&lt;p&gt;The DNS/DHCP server will be built using a FreeBSD jail. This is done for several reason. First, FreeBSD Jails come up nearly as quick as the hypervisor itself, meaning if everything is (re)started at the same time the DNS server should be the first service up an running. Second, is that FreeBSD jails provide isolation from other processes on the same system. Third, they are light weight.&lt;/p&gt;
&lt;p&gt;What is a FreeBSD Jail (referred to as just a &#39;jail&#39; from now on)? A jail is an isolated virtual space to run processes in that is separate from the host OS, and all other jails. From the Linux perspective jails are very close to Docker containers (although they pre-date Docker by a decade).&lt;/p&gt;
&lt;h2 id=&#34;setup-the-jail-sever&#34;&gt;Setup the Jail sever&lt;/h2&gt;
&lt;p&gt;To setup the jail server first we need a space to store all the jails. Jails consist of the base FreeBSD OS version of our choice. We will be making use of ZFS snapshots to make deploying and updating jails quickly.&lt;/p&gt;
&lt;h3 id=&#34;create-jail-from-template&#34;&gt;Create Jail from template&lt;/h3&gt;
&lt;p&gt;To create a jail from the template use the ZFS clone command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zfs clone data01/jails/templates/14.2-RELEASE@base data01/jails/dns
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to clone a jail drive&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;setup-the-dnsdhcp-server-jail&#34;&gt;Setup the DNS/DHCP server jail&lt;/h3&gt;
&lt;p&gt;Next we will create a jail configuration file. These files will be stored in the &lt;strong&gt;/etc/jails.conf.d&lt;/strong&gt; directory. Each jail will have its own file. The file is used to setup the properties of the jail, what it is allowed to do within the host OS and any pre/port commands that need to be run in the host OS when the jail is started/stopped.&lt;/p&gt;
&lt;p&gt;First we need to add/update the &lt;strong&gt;/etc/devfs.conf&lt;/strong&gt; file. This file controls what resources are available to the jail. In this case we will need to expose the BPF devices to listen for DHCP requests. The following lines need to be added to the &lt;strong&gt;/etc/devfs.conf&lt;/strong&gt; file&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[devfsrules_jail_bpf=10]
add include $devfsrules_jail
add path bpf* unhide
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;partial /etc/devfs.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now create the &lt;strong&gt;/etc/jail.conf.d/dns.conf&lt;/strong&gt; file for defining and starting the jail.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# DNS/DHCP server jail file

dns {
	# STARTUP / LOGGING
	exec.stop = &amp;quot;/bin/sh /etc/rc.shutdown&amp;quot;;
	exec.consolelog = &amp;quot;/var/log/jail_console_${name}.log&amp;quot;;

	# PERMISSIONS
	allow.raw_sockets;
	exec.clean;
	mount.devfs;
    devfs_ruleset = 10;
    allow.reserved_ports = true;

	# HOSTNAME / PATH
	host.hostname = &amp;quot;${name}&amp;quot;;
	path = &amp;quot;/data01/jails/${name}&amp;quot;;

	# NETWORK
	vnet;
	vnet.interface = &amp;quot;${epair}b&amp;quot;;

	# NETWORKS/INTERFACES
	$id = &amp;quot;10&amp;quot;;
	$ip = &amp;quot;192.168.30.${id}&amp;quot;;
	$mask = &amp;quot;255.255.255.0&amp;quot;;
	$gateway = &amp;quot;192.168.30.1&amp;quot;;
	$bridge = &amp;quot;net-dmz&amp;quot;;
	$epair = &amp;quot;epair${id}&amp;quot;;

	exec.prestart += &amp;quot;ifconfig ${epair} create up&amp;quot;;
	exec.prestart += &amp;quot;ifconfig ${epair}a up descr jail:${name}&amp;quot;;
	exec.prestart += &amp;quot;ifconfig ${bridge} addm ${epair}a up&amp;quot;;
	exec.start    += &amp;quot;ifconfig ${epair}b ${ip} netmask ${mask} up&amp;quot;;
	exec.start    += &amp;quot;route add default ${gateway}&amp;quot;;
  	exec.start    += &amp;quot;/bin/sh /etc/rc&amp;quot;;
	exec.poststop += &amp;quot;ifconfig ${bridge} deletem ${epair}a&amp;quot;;
	exec.poststop += &amp;quot;ifconfig ${epair}a destroy&amp;quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/jails.conf.d/dns.conf&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;start-the-jail&#34;&gt;Start the jail&lt;/h3&gt;
&lt;p&gt;Once the configuration file has been complete the jail should be started.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service jail start dns
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to start the jail&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;install-the-dnsdhcp-service-in-the-jail&#34;&gt;Install the DNS/DHCP service in the jail&lt;/h3&gt;
&lt;p&gt;Once the jail has been started we need to update the FreeBSD package system and install the DNS and DHCP packages into the jail.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pkg -j dns update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to update the packages in the jail&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;setup-the-new-jail&#34;&gt;Setup the new jail&lt;/h3&gt;
&lt;p&gt;To enter the jail from the host OS use the jexec(8) command and specify the command to run inside the jail.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jexec -l dns /bin/sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to connect to the jail&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once in the jail you will be the root user. The jail itself will have no service running by default, not even SSH. Let&#39;s enable the SSH service... but first we need to create a user account because we don&#39;t want to SSH into the server as root... (or at least we shouldn&#39;t do this... it is up to you).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jexec -l dns pw user add alfred -m -s /bin/sh -u 1001 -G wheel
jexec -l dns passwd alfred
jexec -l dns passwd root
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to create a jail user and set the passwords&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Fix sshd to disallow root to login via SSH. Edit the /&lt;strong&gt;etc/ssh/sshd_config&lt;/strong&gt; and explicitly disable root login.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jexec -l dns sed -i -r -e &#39;s/^#?PermitRootLogin.*/PermitRootLogin no/&#39; /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to edit sshd_config to prevent root logins&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now lets enable and start the SSH service within the jail&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service -j dns sshd enable
service -j dns sshd start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to enable and start sshd&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now we test. SSH to 192.168.30.10 (remember no DNS yet). Once that is working we can move on to the DNS configuration.&lt;/p&gt;
&lt;h3 id=&#34;dns-server-setup-discussion&#34;&gt;DNS server setup discussion...&lt;/h3&gt;
&lt;p&gt;Before we configure the DNS settings lets create a list of items we need to accomplish to be successful.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Resolve internal names from internal sources&lt;/li&gt;
&lt;li&gt;Resolve external names from internal sources&lt;/li&gt;
&lt;li&gt;Resolve DMZ names from external sources (future episode)&lt;/li&gt;
&lt;li&gt;Filter external names from internal sources (bad domains, advertisements, etc) (future episode)&lt;/li&gt;
&lt;li&gt;Provide Dynamic DNS updates from internal hosts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We can accomplish this is three different ways (there are probably more... but only three I&#39;ll mention). Each way has it&#39;s own complications.&lt;/p&gt;
&lt;p&gt;The first way is to use DNS views (or DNS split view). Based on where the client request comes from the client will be presented with one view or another view of the domain. This can be tricky to troubleshoot.&lt;/p&gt;
&lt;p&gt;The second method, that is easier to troubleshoot, is to use two different domains. One domain is used for internal applications and the other domain is used for external applications. This is easier to troubleshoot, but become complicated when we need to expose an internal resource externally.&lt;/p&gt;
&lt;p&gt;The third method is to use a single domain but two authoritative name servers, and internal and external name server. This method requires the most work of all since we will need to maintain two separate servers, but from the client standpoint, it is very easy to understand. External clients connect to the external name server, Internal clients use the internal name server.&lt;/p&gt;
&lt;p&gt;In series I will present the third option. While the goal is to &amp;quot;self host&amp;quot; everything, we will require internet facing services at predictable addresses. This will allow us to utilize one of the many Internet DNS providers out there while maintaining maximum control.&lt;/p&gt;
&lt;h3 id=&#34;configuring-dns-server&#34;&gt;Configuring DNS server&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;(this section assumes that we are ssh&#39;d into the jail)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;On FreeBSD the configuration files are generally stored in the &lt;strong&gt;/usr/local/etc/namedb&lt;/strong&gt; directory.&lt;/p&gt;
&lt;p&gt;First we need to create the &lt;strong&gt;rndc.conf&lt;/strong&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rndc-confgen -a -k DNS_UPDATE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to create the rndc.conf file&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This will create a file in the &lt;strong&gt;/usr/local/etc/namedb/&lt;/strong&gt; directory.&lt;/p&gt;
&lt;p&gt;Next we will crate the &lt;strong&gt;named.conf&lt;/strong&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//
options {
        // All file and path names are relative to the chroot directory,
        // if any, and should be fully qualified.
        directory       &amp;quot;/usr/local/etc/namedb/working&amp;quot;;
        pid-file        &amp;quot;/var/run/named/pid&amp;quot;;
        dump-file       &amp;quot;/var/dump/named_dump.db&amp;quot;;
        statistics-file &amp;quot;/var/stats/named.stats&amp;quot;;
        listen-on       {
                                127.0.0.1;
                                192.168.30.10;
                         };
// These zones are already covered by the empty zones listed below.
// If you remove the related empty zones below, comment these lines out.
        disable-empty-zone &amp;quot;255.255.255.255.IN-ADDR.ARPA&amp;quot;;
        disable-empty-zone &amp;quot;0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.IP6.ARPA&amp;quot;;
        disable-empty-zone &amp;quot;1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.IP6.ARPA&amp;quot;;

        # allow recursion
        allow-recursion { 0.0.0.0/0; };
};

include &amp;quot;/usr/local/etc/namedb/rndc.key&amp;quot;;
controls {
        inet 127.0.0.1 port 953 allow { 127.0.0.1; } keys { &amp;quot;DNS_UPDATE&amp;quot;; };
};

zone &amp;quot;.&amp;quot; { type hint; file &amp;quot;/usr/local/etc/namedb/named.root&amp;quot;; };

zone &amp;quot;example.org&amp;quot; {
        type primary;
        file &amp;quot;/usr/local/etc/namedb/primary/example.org&amp;quot;;
        allow-update { key DNS_UPDATE; };
};

zone &amp;quot;10.168.192.in-addr.arpa&amp;quot; {
        type primary;
        file &amp;quot;/usr/local/etc/namedb/primary/10.168.192.in-addr.arpa&amp;quot;;
        allow-update { key DNS_UPDATE; };
};
zone &amp;quot;20.168.192.in-addr.arpa&amp;quot; {
        type primary;
        file &amp;quot;/usr/local/etc/namedb/primary/20.168.192.in-addr.arpa&amp;quot;;
        allow-update { key DNS_UPDATE; };
};
zone &amp;quot;30.168.192.in-addr.arpa&amp;quot; {
        type primary;
        file &amp;quot;/usr/local/etc/namedb/primay/30.168.192.in-addr.arpa&amp;quot;;
        allow-update { key DNS_UPDATE; };
};
zone &amp;quot;40.168.192.in-addr.arpa&amp;quot; {
        type primary;
        file &amp;quot;/usr/local/etc/namedb/primary/40.168.192.in-addr.arpa&amp;quot;;
        allow-update { key DNS_UPDATE; };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/namedb/named.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally we create the zone files stored in the &lt;strong&gt;/usr/local/etc/namedb/primary/&lt;/strong&gt; directory.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; 
$TTL 60
$ORIGIN example.org.
@       IN      SOA     dns.example.org. noone.example.org. (
                10
                1h
                3h
                2h
                4h
)
;
; NS records
        IN      NS      dns.example.org.
; MX records
; A records
justatest       IN      A       192.168.10.254
dns     IN      A       192.168.30.10
firewall-dmz      IN      A      192.168.30.1
firewall-iot      IN      A      192.168.20.1
firewall      IN      A      192.168.10.1
; CNAME records
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/namedb/primary/example.org&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; 
$TTL 60
$ORIGIN 10.168.192.in-addr.arpa.
@       IN      SOA     dns.example.org. noone.example.org. (
        10
        1h
        3h
        2h
        4h
)
; NS records
        IN      NS      dns.example.org.
; PTR records
254     IN      PTR     justatest.example.org.
1       IN      PTR     firewall.example.org.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/namedb/primary/10.168.192.in-addr.arpa&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; 
$TTL 60
$ORIGIN 20.168.192.in-addr.arpa.
@       IN      SOA     dns.example.org. noone.example.org. (
        10
        1h
        3h
        2h
        4h
)
; NS records
        IN      NS      dns.example.org.
; PTR records
1       IN      PTR     firewall-iot.example.org.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/namedb/primary/20.168.192.in-addr.arpa&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; 
$TTL 60
$ORIGIN 30.168.192.in-addr.arpa.
@       IN      SOA     dns.example.org. noone.example.org. (
        10
        1h
        3h
        2h
        4h
)
; NS records
        IN      NS      dns.example.org.
; PTR records
10      IN      PTR     dns.example.org.
1       IN      PTR     firewall-dmz.example.org.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/namedb/primary/30.168.192.in-addr.arpa&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; 
$TTL 60
$ORIGIN 40.168.192.in-addr.arpa.
@       IN      SOA     dns.example.org. noone.example.org. (
        10
        1h
        3h
        2h
        4h
)
; NS records
        IN      NS      dns.example.org.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/namedb/primary/40.168.192.in-addr.arpa&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once bind has been configured we need to enable it so that it starts automatically.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service named enable
service named start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to enable and start named service&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;confirm-dns-server-is-working&#34;&gt;Confirm DNS server is working&lt;/h3&gt;
&lt;p&gt;To confirm that DNS is working the following commands can be run.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dig @127.0.0.1 google.com
dig @127.0.0.1 justatest.example.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to test recursive and authoritative resolution&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Both commands should succeed. Assuming it is working, update the &lt;strong&gt;/etc/resolv.conf&lt;/strong&gt; file&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nameserver 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/resolv.conf&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;dhcp-server-discussion&#34;&gt;DHCP server discussion&lt;/h3&gt;
&lt;p&gt;As with DNS server, let&#39;s take a moment to discuss what we want to accomplish with the DHCP server.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provide DHCP for the COMMON segment.&lt;/li&gt;
&lt;li&gt;Provide DHCP for the IOT segment.&lt;/li&gt;
&lt;li&gt;Dynamically update DNS records for the assigned IPs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One important thing to note is that DHCP clients use a broadcast to find a DHCP server. This broadcast does not extend past the local subnet (without help).&lt;/p&gt;
&lt;h3 id=&#34;install-dhcp-server-software&#34;&gt;Install DHCP server software&lt;/h3&gt;
&lt;p&gt;First step is to install the DHCP server software&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pkg install isc-dhcp44-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;command to install the dhcp server software&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;configuring-dhcp-server&#34;&gt;Configuring DHCP server&lt;/h3&gt;
&lt;p&gt;Use the following config for the DHCP server&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#

# option definitions common to all supported networks...
option domain-name &amp;quot;example.org&amp;quot;;

default-lease-time 86400;
max-lease-time 86400;

ddns-update-style standard;
deny client-updates;
ddns-updates on;

authoritative;

log-facility local7;

include &amp;quot;/usr/local/etc/namedb/rndc.key&amp;quot;;

zone example.org. {
        primary 127.0.0.1;
        key DNS_UPDATE;
}

zone 10.168.192.in-addr.arpa. {
        primary 127.0.0.1;
        key DNS_UPDATE;
}
zone 20.168.192.in-addr.arpa. {
        primary 127.0.0.1;
        key DNS_UPDATE;
}
zone 30.168.192.in-addr.arpa. {
        primary 127.0.0.1;
        key DNS_UPDATE;
}
zone 40.168.192.in-addr.arpa. {
        primary 127.0.0.1;
        key DNS_UPDATE;
}

subnet 192.168.10.0 netmask 255.255.255.0 {
        range 192.168.10.10 192.168.10.254;
        option routers 192.168.10.1;
        option domain-name-servers 192.168.30.10;
}
subnet 192.168.20.0 netmask 255.255.255.0 {
        range 192.168.20.10 192.168.20.254;
        option routers 192.168.20.1;
        option domain-name-servers 192.168.30.10;
}
subnet 192.168.30.0 netmask 255.255.255.0 {
        range 192.168.30.10 192.168.30.254;
        option routers 192.168.30.1;
        option domain-name-servers 192.168.30.10;
}
subnet 192.168.40.0 netmask 255.255.255.0 {
        range 192.168.40.10 192.168.40.254;
        option routers 192.168.40.1;
        option domain-name-servers 192.168.30.10;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/dhcpd.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next we enable the DHCP service&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service isc-dhcpd enable
service isc-dhcpd start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;commands to enable and start the dhcp server&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;setup-dhcp-relay-on-firewall&#34;&gt;Setup DHCP relay on Firewall&lt;/h2&gt;
&lt;p&gt;As mentioned above DHCP uses a link local broadcast to find a DHCP server. In order to provide the help necessary to forward the broadcasts from different LAN segments we will setup the firewall to DHCP relaying.&lt;/p&gt;
&lt;p&gt;On the firewall we will enable and start the DHCP relay service.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln -s /etc/rc.d/dhcrelay /etc/rc.d/dhcrelay_iot
ln -s /etc/rc.d/dhcrelay /etc/rc.d/dhcrelay_common
rcctl enable dhcrelay_common dhcrelay_iot
rcctl set dhcrelay_common flags -i vio2 192.168.30.10
rcctl set dhcrelay_iot flags -i vio1 192.168.30.10
rcctl start dhcrelay_common dhcrelay_iot
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;This setups up the initial DNS and DHCP services. However, as this series progresses we will add DNS records.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Self Hosting: Episode 2 - Firewall</title>
    <link href="https://obtusenet.com/blog/self-hosting-episode-2-firewall.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/self-hosting-episode-2-firewall.html</id>
    <published>2025-09-14T17:00:31Z</published>
    <updated>2025-09-14T17:00:31Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>Software Used: * OpenBSD 7.7 Introduction The firewall will separate the segments and provide protection for services that must cross the segment boundaries.</summary>
    <category term="self-hosted"/>
    <category term="openbsd"/>
    <content type="html">&lt;h3 id=&#34;software-used&#34;&gt;Software Used:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://cdn.openbsd.org/pub/OpenBSD/7.7/&#34;&gt;OpenBSD 7.7&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&#34;introduction&#34;&gt;Introduction&lt;/h1&gt;
&lt;p&gt;The firewall will separate the segments and provide protection for services that must cross the segment boundaries. To that end firewall will have several interfaces. To keep rules as manageable as possible we will treat all hosts behind each segment/interface the same. This way we should only have to really worry about the exceptions.&lt;/p&gt;
&lt;h2 id=&#34;build-the-firewall-os&#34;&gt;Build the firewall OS&lt;/h2&gt;
&lt;p&gt;The firewall will run on OpenBSD.&lt;/p&gt;
&lt;h4 id=&#34;hypervisor-setup&#34;&gt;Hypervisor setup&lt;/h4&gt;
&lt;p&gt;We will create a VM on the hypervisor for OpenBSD. We will add it to each virtual network so it will have an interface in each one. In addition we will connect it to the external segment.&lt;/p&gt;
&lt;p&gt;To install OpenBSD in the bhyve hypervisor first create the VM.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vm create -t openbsd77 -s 10G -m 1G -c 2 firewall
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next add the different networks to the VM. This will create interfaces in the VM on the specific segments specified. Note: the order because that is the order the interfaces will show up within the guest OS.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vm add -t network -s net-external
vm add -t network -s net-iot
vm add -t network -s net-common
vm add -t network -s net-dmz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now download the install image&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vm iso https://cdn.openbsd.org/pub/OpenBSD/7.7/amd64/install77.iso
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the download is complete. Start the VM install.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vm install firewall install77.iso
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and connect to the console&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vm console firewall
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now install OpenBSD like normal. One change that some may not be used to, since the VM does not have a &amp;quot;screen&amp;quot; we will need to tell the boot loader that use the TTY (com0) as the console. To do this on the boot prompt &amp;quot;&lt;em&gt;boot&amp;gt;&lt;/em&gt;&amp;quot; type &amp;quot;&lt;em&gt;set tty com0&lt;/em&gt;&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;assets/self-hosting-episode-2-firewall/capture_2025-09-07_11-42-55.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Now type &amp;quot;boot&amp;quot; to boot the installer. Once the boot is finished you&#39;ll be asked what you want to do. Select &amp;quot;I&amp;quot; for install and follow the prompts. OpenBSD has sensible defaults, so when in doubt, accept the default.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;assets/self-hosting-episode-2-firewall/capture_2025-09-07_11-43-10.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;boot&amp;gt; boot
cannot open cd0a:/etc/random.seed: No such file or directory
booting cd0a:/7.7/amd64/bsd.rd: 4232111+1745920+3887480+0+716800 [109+483816+334631]=0xae2100
entry point at 0x1001000
Copyright (c) 1982, 1986, 1989, 1991, 1993
        The Regents of the University of California.  All rights reserved.
Copyright (c) 1995-2025 OpenBSD. All rights reserved.  https://www.OpenBSD.org

OpenBSD 7.7 (RAMDISK_CD) #613: Sun Apr 13 08:35:08 MDT 2025
    deraadt@amd64.openbsd.org:/usr/src/sys/arch/amd64/compile/RAMDISK_CD
real mem = 1032982528 (985MB)
avail mem = 995553280 (949MB)
random: good seed from bootblocks
mainbus0 at root
bios0 at mainbus0: SMBIOS rev. 2.8 @ 0x3fbcf000 (11 entries)
bios0: vendor BHYVE version &amp;quot;14.0&amp;quot; date 10/17/2021
bios0: FreeBSD BHYVE
efi0 at bios0: UEFI 2.7
efi0: BHYVE rev 0x10000
acpi0 at bios0: ACPI 5.1
acpi0: tables DSDT FACP APIC HPET MCFG SPCR
acpimadt0 at acpi0 addr 0xfee00000: PC-AT compat
cpu0 at mainbus0: apid 0 (boot processor)
cpu0: Intel(R) Xeon(R) CPU E5-2603 v3 @ 1.60GHz, 1597.73 MHz, 06-3f-02
cpu0: cpuid 1 edx=1f8bfbff&amp;lt;FPU,VME,DE,PSE,TSC,MSR,PAE,MCE,CX8,APIC,SEP,MTRR,PGE,MCA,CMOV,PAT,PSE36,CFLUSH,MMX,FXSR,SSE,SSE2,SS,HTT&amp;gt; ecx=f6da3203&amp;lt;SSE3,PCLMUL,SSSE3,FMA3,CX16,PCID,SSE4.1,SSE4.2,MOVBE,POPCNT,AES,XSAVE,AVX,F16C,RDRAND,HV&amp;gt;
cpu0: cpuid 6 eax=4&amp;lt;ARAT&amp;gt;
cpu0: cpuid 7.0 ebx=7a9&amp;lt;FSGSBASE,BMI1,AVX2,SMEP,BMI2,ERMS,INVPCID&amp;gt; edx=400&amp;lt;MD_CLEAR&amp;gt;
cpu0: cpuid d.1 eax=1&amp;lt;XSAVEOPT&amp;gt;
cpu0: cpuid 80000001 edx=2c100800&amp;lt;NXE,PAGE1GB,RDTSCP,LONG&amp;gt; ecx=21&amp;lt;LAHF,ABM&amp;gt;
cpu0: cpuid 80000007 edx=100&amp;lt;ITSC&amp;gt;
cpu0: MELTDOWN
cpu0: 32KB 64b/line 8-way D-cache, 32KB 64b/line 8-way I-cache, 256KB 64b/line 8-way L2 cache, 15MB 64b/line 20-way L3 cache
cpu0: apic clock running at 134MHz
cpu at mainbus0: not configured
ioapic0 at mainbus0: apid 0 pa 0xfec00000, version 11, 32 pins
acpihpet0 at acpi0: 16777216 Hz
acpiprt0 at acpi0: bus 0 (PC00)
acpipci0 at acpi0 PC00
&amp;quot;PNP0303&amp;quot; at acpi0 not configured
&amp;quot;PNP0F13&amp;quot; at acpi0 not configured
com0 at acpi0 COM1 addr 0x3f8/0x8 irq 4: ns16550a, 16 byte fifocom0: console
com1 at acpi0 COM2 addr 0x2f8/0x8 irq 3: ns16550a, 16 byte fifo
com2 at acpi0 COM3 addr 0x3e8/0x8 irq 4: ns16550a, 16 byte fifo
com3 at acpi0 COM4 addr 0x2e8/0x8 irq 3: ns16550a, 16 byte fifo
acpicmos0 at acpi0
&amp;quot;Bhyve_V_Gen_Counter_V1&amp;quot; at acpi0 not configured
cpu0: using VERW MDS workaround
pvbus0 at mainbus0: bhyve
pci0 at mainbus0 bus 0
pchb0 at pci0 dev 0 function 0 unknown vendor 0x1275 product 0x1275 rev 0x00
ahci0 at pci0 dev 3 function 0 &amp;quot;Intel 82801H AHCI&amp;quot; rev 0x00: msi, AHCI 1.3
ahci0: port 0: 6.0Gb/s
scsibus0 at ahci0: 32 targets
cd0 at scsibus0 targ 0 lun 0: &amp;lt;BHYVE, BHYVE DVD-ROM, 001&amp;gt; removable
virtio0 at pci0 dev 4 function 0 &amp;quot;Qumranet Virtio Storage&amp;quot; rev 0x00
vioblk0 at virtio0
virtio0: msix per-VQ
scsibus1 at vioblk0: 1 targets
sd0 at scsibus1 targ 0 lun 0: &amp;lt;VirtIO, Block Device, &amp;gt;
sd0: 0MB, 512 bytes/sector, 32 sectors
virtio1 at pci0 dev 5 function 0 &amp;quot;Qumranet Virtio Network&amp;quot; rev 0x00
vio0 at virtio1: 1 queue, address 58:9c:fc:00:73:1c
virtio1: msix per-VQ
virtio2 at pci0 dev 6 function 0 &amp;quot;Qumranet Virtio RNG&amp;quot; rev 0x00
viornd0 at virtio2
virtio2: msix per-VQ
&amp;quot;Intel 82371SB ISA&amp;quot; rev 0x00 at pci0 dev 31 function 0 not configured
isa0 at mainbus0
pckbc0 at isa0 port 0x60/5 irq 1 irq 12
pckbd0 at pckbc0 (kbd slot)
wskbd0 at pckbd0 mux 1
efifb at mainbus0 not configured
softraid0 at root
scsibus2 at softraid0: 256 targets
root on rd0a swap on rd0b dump on rd0b
WARNING: CHECK AND RESET THE DATE!
erase ^?, werase ^W, kill ^U, intr ^C, status ^T

Welcome to the OpenBSD/amd64 7.7 installation program.
(I)nstall, (U)pgrade, (A)utoinstall or (S)hell? i
At any prompt except password prompts you can escape to a shell by
typing &#39;!&#39;. Default answers are shown in []&#39;s and are selected by
pressing RETURN.  You can exit this program at any time by pressing
Control-C, but this can leave your system in an inconsistent state.

Terminal type? [vt220] 
System hostname? (short form, e.g. &#39;foo&#39;) firewall

Available network interfaces are: vio0 vlan0.
Network interface to configure? (name, lladdr, &#39;?&#39;, or &#39;done&#39;) [vio0] 
IPv4 address for vio0? (or &#39;autoconf&#39; or &#39;none&#39;) [autoconf] 
IPv6 address for vio0? (or &#39;autoconf&#39; or &#39;none&#39;) [none] 
Available network interfaces are: vio0 vlan0.
Network interface to configure? (name, lladdr, &#39;?&#39;, or &#39;done&#39;) [done] 

Password for root account? (will not echo) 
Password for root account? (again) 
Start sshd(8) by default? [yes] 
Change the default console to com0? [yes] 
Available speeds are: 9600 19200 38400 57600 115200.
Which speed should com0 use? (or &#39;done&#39;) [9600] 115200
Setup a user? (enter a lower-case loginname, or &#39;no&#39;) [no] alfred
Full name for user alfred? [alfred] 
Password for user alfred? (will not echo) 
Password for user alfred? (again) 
WARNING: root is targeted by password guessing attacks, pubkeys are safer.
Allow root ssh login? (yes, no, prohibit-password) [no] 
What timezone are you in? (&#39;?&#39; for list) [UTC] 

Available disks are: sd0.
Which disk is the root disk? (&#39;?&#39; for details) [sd0] 
Encrypt the root disk with a (p)assphrase or (k)eydisk? [no] 
No valid MBR or GPT.
Use (W)hole disk MBR, whole disk (G)PT or (E)dit? [gpt] 
Setting OpenBSD GPT partition to whole sd0...done.
The auto-allocated layout for sd0 is:
#                size           offset  fstype [fsize bsize   cpg]
  a:          1177.2M           532544  4.2BSD   2048 16384     1 # /
  b:           256.0M          2943424    swap                    
  c:         10240.0M                0  unused                    
  d:          3072.0M          3467712  4.2BSD   2048 16384     1 # /usr
  e:          2048.0M          9759168  4.2BSD   2048 16384     1 # /home
  i:           260.0M               64   MSDOS                    
Use (A)uto layout, (E)dit auto layout, or create (C)ustom layout? [a] 
/dev/rsd0a: 1177.2MB in 2410880 sectors of 512 bytes
6 cylinder groups of 202.50MB, 12960 blocks, 25920 inodes each
/dev/rsd0e: 2048.0MB in 4194304 sectors of 512 bytes
11 cylinder groups of 202.50MB, 12960 blocks, 25920 inodes each
/dev/rsd0d: 3072.0MB in 6291456 sectors of 512 bytes
16 cylinder groups of 202.50MB, 12960 blocks, 25920 inodes each
/dev/sd0a (bc82b06ecfc64241.a) on /mnt type ffs (rw, asynchronous, local)
/dev/sd0e (bc82b06ecfc64241.e) on /mnt/home type ffs (rw, asynchronous, local, nodev, nosuid)
/dev/sd0d (bc82b06ecfc64241.d) on /mnt/usr type ffs (rw, asynchronous, local, nodev)

Let&#39;s install the sets!
Location of sets? (cd0 disk http nfs or &#39;done&#39;) [cd0] 
Pathname to the sets? (or &#39;done&#39;) [7.7/amd64] 

Select sets by entering a set name, a file name pattern or &#39;all&#39;. De-select
sets by prepending a &#39;-&#39;, e.g.: &#39;-game*&#39;. Selected sets are labelled &#39;[X]&#39;.
    [X] bsd           [X] base77.tgz    [X] game77.tgz    [X] xfont77.tgz
    [X] bsd.mp        [X] comp77.tgz    [X] xbase77.tgz   [X] xserv77.tgz
    [X] bsd.rd        [X] man77.tgz     [X] xshare77.tgz
Set name(s)? (or &#39;abort&#39; or &#39;done&#39;) [done] 
Directory does not contain SHA256.sig. Continue without verification? [no] yes
Installing bsd          100% |**************************| 31152 KB    00:09    
Installing bsd.mp       100% |**************************| 31257 KB    00:09    
Installing bsd.rd       100% |**************************|  4687 KB    00:01    
Installing base77.tgz   100% |**************************|   483 MB    03:01    
Extracting etc.tgz      100% |**************************|   275 KB    00:00    
Installing comp77.tgz   100% |**************************| 81731 KB    00:38    
Installing man77.tgz    100% |**************************|  8265 KB    00:04    
Installing game77.tgz   100% |**************************|  2746 KB    00:01    
Installing xbase77.tgz  100% |**************************| 60706 KB    00:26    
Extracting xetc.tgz     100% |**************************|  8474       00:00    
Installing xshare77.tgz 100% |**************************|  4554 KB    00:03    
Installing xfont77.tgz  100% |**************************| 23021 KB    00:08    
Installing xserv77.tgz  100% |**************************| 20074 KB    00:08    
Installing BUILDINFO    100% |**************************|    54       00:00    
Location of sets? (cd0 disk http nfs or &#39;done&#39;) [done] 
Saving configuration files... done.
Making all device nodes... done.
Multiprocessor machine; using bsd.mp instead of bsd.
fw_update: add intel; update none
Relinking to create unique kernel... done.

CONGRATULATIONS! Your OpenBSD install has been successfully completed!

When you login to your new system the first time, please read your mail
using the &#39;mail&#39; command.

Exit to (S)hell, (H)alt or (R)eboot? [reboot] halt
syncing disks... done

The operating system has halted.
Please press any key to reboot.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the VM has been installed break out of the console using &lt;strong&gt;~.&lt;/strong&gt; escape. This will take you back to a the hypervisor. From the hypervisor make sure the VM is halted using the &#39;vm list&#39; command. The state should be &amp;quot;Stopped&amp;quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@hyperv01:/data01/vms # vm list
NAME      DATASTORE  LOADER  CPU  MEMORY  VNC  AUTO  STATE
firewall  default    uefi    2    1G      -    No    Stopped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If not, then use the &#39;&lt;em&gt;vm poweroff firewall&lt;/em&gt;&#39; command to stop the VM.&lt;/p&gt;
&lt;p&gt;Now that OpenBSD has been installed on the VM, restart the VM using the &#39;&lt;em&gt;vm start firewall&lt;/em&gt;&#39; command.&lt;/p&gt;
&lt;h3 id=&#34;firewall-setup&#34;&gt;Firewall Setup&lt;/h3&gt;
&lt;p&gt;The firewall needs to be able to route packets. Edit the &lt;strong&gt;/etc/sysctl.conf&lt;/strong&gt; file to enable IP forwarding.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;net.ip.forwarding=1
net.ip6.forwarding=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/sysctl.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next we will setup the &lt;strong&gt;/etc/pf.conf&lt;/strong&gt; and other firewall config files. First we will create a directory to keep the files organized.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir /etc/pf.d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since we want to treat each segment/interface the same we will make use of tags to help organize the traffic. The way tags work is traffic is matched and tagged on input. The tag can then be used to allow/deny the traffic on exit.&lt;/p&gt;
&lt;p&gt;One thing to note with PF rules, they are processed in what is called &amp;quot;last match&amp;quot;. Meaning that the last matching rule for a packet is the action that is taken. With this in mind we will specify the general actions at the top of the rule file and then get more specific at the end of the rule file.&lt;/p&gt;
&lt;p&gt;The following files will be used to start. We will update the files as required.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# IOT Segment
anchor iot on IOT {
  match in tag IOT
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/pf.d/pf.anchor.iot&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Common Segment
anchor common on COMMON {
  match in tag COMMON
  deny out tagged IOT
  deny out tagged DMZ
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/pf.d/pf.anchor.common&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# DMZ Segment
anchor dmz on DMZ {
  match in tag DMZ
  deny out tagged IOT
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/pf.d/pf.anchor.dmz&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# External Segment
anchor external on EXTERNAL {
  deny in drop
  deny out drop
  deny out tagged IOT return
  # NAT outbound traffic to external interfaces
  pass out tagged { DMZ, COMMON } nat-to ($extif)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/pf.d/pf.anchor.extranet&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;And finally create the &lt;strong&gt;/etc/pf.conf&lt;/strong&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# pf.conf
#

####
#### MACROS
####
ext_if=&amp;quot;vio0&amp;quot;

# skip internal interfaces
set skip on lo
set skip on enc
set state-defaults pflow

table &amp;lt;all_nets&amp;gt; { 192.168.0.0/16 }
table &amp;lt;all6_nets&amp;gt; { 2001:db8::/32 }

####
#### Common RULES
####
# default block.  Remove log once all rules are sorted
block log
# capture NTP from any internal sources and respond for them
pass in proto udp from &amp;lt;all_nets&amp;gt; to port 123 rdr-to 127.0.0.1 port 123
pass in inet6 proto udp from &amp;lt;all6_nets&amp;gt; to port 123 rdr-to ::1 port 123
# Allow Neighbor solicitations
pass in quick on $ext_if inet6 proto icmp6 all icmp6-type { routeradv neighbrsol neighbradv }
# Allow DHCPv6
pass in quick on $ext_if inet6 proto udp \
	from fe80::/10 port dhcpv6-server \
	to fe80::/10 port dhcpv6-client no state
# Allow ping????
#pass in  quick on $ext_if inet6 proto icmp6 all icmp6-type echoreq
# Allow Link Local communications
pass in  quick on $ext_if inet6 proto icmp6 from fe80::/10 to fe80::/10
pass in  quick on $ext_if inet6 proto icmp6 from ff02::/16 to fe80::/10
pass out quick on $ext_if inet6 proto icmp6 from fe80::/10 to ff02::/16

# Include the anchor rules
include &amp;quot;/etc/pf.d/pf.anchor.common&amp;quot;
include &amp;quot;/etc/pf.d/pf.anchor.iot&amp;quot;
include &amp;quot;/etc/pf.d/pf.anchor.dmz&amp;quot;
include &amp;quot;/etc/pf.d/pf.anchor.external&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/pf.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once all the rules are in place we can test them using the pfctl(8) command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pfctl -nf /etc/pf.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Any errors, typo, etc will be displayed. They will need to be fixed before the rules can be in place.&lt;/p&gt;
&lt;p&gt;Adding the &lt;strong&gt;-v&lt;/strong&gt; flag will show an output of all the rules.&lt;/p&gt;
&lt;h2 id=&#34;finally&#34;&gt;Finally...&lt;/h2&gt;
&lt;p&gt;Once the rules are loading successfully, the firewall can be rebooted to make sure the rules are applied automatically.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;reboot
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;This will get the basic firewall up and running. We will be revisiting the firewall config as we build out more of the environment.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Self Hosting: Episode 1 - Hypervisor</title>
    <link href="https://obtusenet.com/blog/self-hosting-episode-1-hypervisor.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/self-hosting-episode-1-hypervisor.html</id>
    <published>2025-09-07T16:22:33Z</published>
    <updated>2025-09-07T16:22:33Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>Software Used: * FreeBSD 14.2 * Sanoid/Syncoid 2.3.0 Introduction Before we can deploy any services we need two things.</summary>
    <category term="self-hosted"/>
    <category term="freebsd"/>
    <category term="vm"/>
    <content type="html">&lt;h3 id=&#34;software-used&#34;&gt;Software Used:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.freebsd.org/releases/14.2R/announce/&#34;&gt;FreeBSD 14.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/jimsalterjrs/sanoid/releases/tag/v2.3.0&#34;&gt;Sanoid/Syncoid 2.3.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&#34;introduction&#34;&gt;Introduction&lt;/h1&gt;
&lt;p&gt;Before we can deploy any services we need two things. First a place to store those services and two a place to run those services from (i.e storage and compute). These two requirements do not need to run on the same system and for larger self hosted sites or businesses, they probably wont. But for these examples I will present them using a single system.&lt;/p&gt;
&lt;p&gt;In these examples I will be utilizing FreeBSD as my compute and storage system. There are other systems out there: &lt;a href=&#34;https://proxmox.com/&#34;&gt;Proxmox&lt;/a&gt;, &lt;a href=&#34;https://xcp-ng.org/&#34;&gt;XCP-NG&lt;/a&gt;, etc. Please feel free to use any/all of these. My preference is FreeBSD because it gives me all the underlying services I need in a rock solid operating system.&lt;/p&gt;
&lt;h2 id=&#34;installing-base-os&#34;&gt;Installing Base OS&lt;/h2&gt;
&lt;p&gt;Installing FreeBSD is straight forward. &lt;a href=&#34;https://docs.freebsd.org/en/books/handbook/bsdinstall/&#34;&gt;Chapter 2&lt;/a&gt; of the &lt;a href=&#34;https://docs.freebsd.org/en/books/handbook/&#34;&gt;FreeBSD handbook&lt;/a&gt; will walk you through installing FreeBSD on a bare system.&lt;/p&gt;
&lt;h2 id=&#34;network-prep&#34;&gt;Network Prep&lt;/h2&gt;
&lt;p&gt;Within the hypervisor we will create separate virtual networks. Each virtual network will be on a separate VLAN. The hypervisor is not required to have an interface on any of the virtual networks. In fact, we don&#39;t want it to.&lt;/p&gt;
&lt;h3 id=&#34;create-bridge-for-virtual-networks&#34;&gt;Create Bridge for Virtual Networks&lt;/h3&gt;
&lt;p&gt;On FreeBSD (as in Linux) a bridge network interface is used to create the virtual network. These commands will create the bridge and rename it to something more meaningful.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ifconfig bridge1 create
ifconfig bridge1 name net-common
ifconfig bridge2 create
ifconfig bridge2 name net-iot
ifconfig bridge3 create
ifconfig bridge3 name net-dmz
ifconfig bridge4 create
ifconfig bridge4 name net-external
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Attach each bridge to the appropriate VLAN. Creating VLANs allows this system to communicate with other systems while maintaining the segments. For this example I will assume that the physical interface of the hypervisor is &amp;quot;vmx0&amp;quot;. Replace this with the actual physical interface name.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ifconfig vlan10 create
ifconfig vlan10 vlan 10 vlandev vmx0
ifconfig net-common addm vlan10
ifconfig vlan20 create
ifconfig vlan20 vlan 20 vlandev vmx0
ifconfig net-iot addm vlan20
ifconfig vlan30 create
ifconfig vlan30 vlan 30 vlandev vmx0
ifconfig net-dmz addm vlan30
ifconfig vlan90 create
ifconfig vlan90 vlan 90 vlandev vmx0
ifconfig net-external addm vlan90
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next we will update /etc/rc.conf to make sure the changes will take affect on next reboot.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sysrc ifconfig_vlan10=&amp;quot;vlan 10 vlandev vmx0
sysrc ifconfig_bridge1=&amp;quot;name net-common addm vlan10&amp;quot;
sysrc ifconfig_vlan20=&amp;quot;vlan 20 vlandev vmx0
sysrc ifconfig_bridge2=&amp;quot;name net-iot addm vlan20&amp;quot;
sysrc ifconfig_vlan30=&amp;quot;vlan 30 vlandev vmx0
sysrc ifconfig_bridge3=&amp;quot;name net-dmz addm vlan30&amp;quot;
sysrc ifconfig_vlan90=&amp;quot;vlan 90 vlandev vmx0
sysrc ifconfig_bridge4=&amp;quot;name net-external addm vlan90&amp;quot;

sysrc cloned_interfaces+=&amp;quot; bridge1 vlan10&amp;quot;
sysrc cloned_interfaces+=&amp;quot; bridge2 vlan20&amp;quot;
sysrc cloned_interfaces+=&amp;quot; bridge3 vlan30&amp;quot;
sysrc cloned_interfaces+=&amp;quot; bridge4 vlan90&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;storage-prep&#34;&gt;Storage Prep&lt;/h2&gt;
&lt;p&gt;FreeBSD comes with ZFS. If you are not familiar with it see &lt;a href=&#34;https://en.wikipedia.org/wiki/ZFS&#34;&gt;here&lt;/a&gt;, &lt;a href=&#34;https://openzfs.org/wiki/Main_Page&#34;&gt;here&lt;/a&gt; and &lt;a href=&#34;https://docs.freebsd.org/en/books/handbook/zfs/&#34;&gt;here&lt;/a&gt; for explanations. ZFS provides the many features we will need to hosting our environment.&lt;/p&gt;
&lt;h3 id=&#34;create-data-dirs&#34;&gt;Create data_dirs&lt;/h3&gt;
&lt;p&gt;This environment will have data that we want to protect. While ZFS will do everything reasonable to keep the data safe that isn&#39;t a 100% guarantee. Setting up our &lt;strong&gt;data_dirs&lt;/strong&gt; will help us determine what data we consider important.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zpool create data01 da2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we create a dataset for virtual machine images&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zfs create data01/vms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we create a dataset for jail&#39;d processes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zfs create data01/jails
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a dataset for shared user data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zfs create data01/user_data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally we create a dataset for backups&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zfs create data01/backup_data
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;virtual-machines&#34;&gt;Virtual machines&lt;/h2&gt;
&lt;p&gt;Virtual machines can take several different flavors. They can be fully emulated hardware (type 1/2) or just isolated processes in containers (type 3). In this environment both will be used. To start with we will make use of &lt;a href=&#34;https://docs.freebsd.org/en/books/handbook/jails/&#34;&gt;FreeBSD Jails&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;prepare-for-jails&#34;&gt;Prepare for Jails&lt;/h3&gt;
&lt;p&gt;To prepare the server for jails add a &lt;strong&gt;/etc/jail.conf&lt;/strong&gt; file&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
.include &amp;quot;/etc/jail.conf.d/*.conf&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/jail.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;And then enable the jails to start on boot.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service jails enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One nice feature of ZFS is that it can do snapshots. A snapshot is a exact copy of a dataset at the moment the snapshot is taken. Any updates to the dataset will not update the snapshot. We will use this feature of ZFS to create a snapshot of a jail. Then we will use that jail snapshot when we create new jails. By doing this we make adding new jail very quick.&lt;/p&gt;
&lt;p&gt;To prepare the jail template first we create a new dataset.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zfs create -p data01/jails/templates/14.2-RELEASE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next we will download the FreeBSD base archive and extract into the new dataset.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fetch -o - https://download.freebsd.org/ftp/releases/amd64/amd64/14.2-RELEASE/base.txz | tar -xf - -C /data01/jailes/templates/14.2-RELEASE --unlink
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the &lt;strong&gt;resolv.conf&lt;/strong&gt; and &lt;strong&gt;localtime&lt;/strong&gt; settings.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp /etc/resolv.conf /data01/jails/templates/14.2-RELEASE/etc/resolv.conf
cp /etc/localtime /data01/jails/templates/14.2-RELEASE/etc/localtime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Optimally we can perform any OS updates between now and when the base.txz was originally created.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;freebsd-update -b /data01/jails/templates/14.2-RELEASE fetch install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now take a snapshot of the dataset&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zfs snapshot data01/jails/templats/14.2-RELEASE@base
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An when we are ready to deploy a new jail we will &amp;quot;clone&amp;quot; the dataset for our jail.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zfs clone data01/jails/templates/14.2-RELEASE@base data01/jails/containers/&amp;lt;jailname&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;prepare-for-virtual-machines&#34;&gt;Prepare for Virtual Machines&lt;/h3&gt;
&lt;p&gt;The virtual machines using &lt;a href=&#34;https://docs.freebsd.org/en/books/handbook/virtualization/#virtualization-host-bhyve&#34;&gt;Bhyve&lt;/a&gt; (the FreeBSD hypervisor) takes a few more steps. First install some packages&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pkg install -y vm-bhyve bhyve-firmware edk2-bhyve grub-bhyve
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the packages are installed update the &lt;strong&gt;rc.conf&lt;/strong&gt; with the following variables&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sysrc vm_delay=5
sysrc vm_dir=zfs:/data01/vms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Enable the VMs to start on boot&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service vm enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Initialize the VM configuration&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/usr/local/sbin/vm init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And finally update the VM config file adding the different segments so they were be available to any VMs.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vm switch create -t manual -b net-iot net-iot
vm switch create -t manual -b net-dmz net-dmz
vm switch create -t manual -b net-common net-common
vm switch create -t manual -b net-external net-external
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the hypervisor is ready for VMs&lt;/p&gt;
&lt;h2 id=&#34;backups&#34;&gt;Backups&lt;/h2&gt;
&lt;p&gt;While ZFS is incredibly stable and will do everything it can to protect your data, problems can happen outside of ZFS&#39;s control. For those case we need to create a backup. (See discussion on 3-2-1 backup scheme: &lt;a href=&#34;https://www.veeam.com/blog/321-backup-rule.html&#34;&gt;here&lt;/a&gt;, &lt;a href=&#34;https://www.backblaze.com/blog/the-3-2-1-backup-strategy/&#34;&gt;here&lt;/a&gt;, &lt;a href=&#34;https://www.seagate.com/blog/what-is-a-3-2-1-backup-strategy/&#34;&gt;here&lt;/a&gt;). Perhaps the best tool I&#39;ve found to automate these backups is &lt;a href=&#34;https://github.com/jimsalterjrs/sanoid&#34;&gt;Sanoid/Syncoid&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;sanoid&#34;&gt;Sanoid&lt;/h3&gt;
&lt;p&gt;Sanoid is a script that can be called from cron that will create and manage snapshots automatically. This will allow us to protect the files on the filesystem from accidental deletion. However we are not fully protected. If something were to happen to the hose or to enough hard drives all data could be lost. So we need to copy the data off of this system to someplace else.&lt;/p&gt;
&lt;p&gt;First install sanoid on the system&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pkg install sanoid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next configure the &lt;strong&gt;sanoid.conf&lt;/strong&gt; file located in &lt;strong&gt;/usr/local/etc/sanoid&lt;/strong&gt; directory&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
[ data01/vms ]
        use_template = production
        recursive = yes

[ data01/jails ]
        use_template = production
        recursive = yes

[ data01/data ]
        use_template = production

#############################
# templates below this line #
#############################

# name your templates template_templatename. you can create your own, and use them in your module definitions above.

[template_demo]
        daily = 60

[template_production]
        frequently = 0
        hourly = 36
        daily = 30
        monthly = 3
        yearly = 0
        autosnap = yes
        autoprune = yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/usr/local/etc/sanoid/sanoid.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally update crontab using &#39;&lt;em&gt;crontab -e&lt;/em&gt;&#39; command add the sanoid command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*/15 * * * * TZ=UTC /usr/local/bin/sanoid --cron
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;syncoid&#34;&gt;Syncoid&lt;/h3&gt;
&lt;p&gt;Syncoid is also a script run from cron. It will take the snapshots created by Sanoid and ZFS send the snapshots to another system running ZFS. This systems does not need to be FreeBSD.&lt;/p&gt;
&lt;p&gt;Syncoid uses the &lt;strong&gt;sanoid.conf&lt;/strong&gt; file to determine what to sync so we only need to specify the destination server. Update cron and specify &lt;em&gt;bkupuser&lt;/em&gt;, &lt;em&gt;bkuphost&lt;/em&gt;, and &lt;em&gt;bkuppath&lt;/em&gt; to tell syncoid where to sync the snapshots.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 1 * * * TZ=UTC /usr/local/bin/syncoid --no-privilege-elivation --create-bookmark data01/vms &amp;lt;bkupuser&amp;gt;@&amp;lt;bkuphost&amp;gt;:&amp;lt;backuppath&amp;gt;
15 1 * * * TZ=UTC /usr/local/bin/syncoid --no-privilege-elivation --create-bookmark data01/jails &amp;lt;bkupuser&amp;gt;@&amp;lt;bkuphost&amp;gt;:&amp;lt;backuppath&amp;gt;
30 1 * * * TZ=UTC /usr/local/bin/syncoid --no-privilege-elivation --create-bookmark data01/data &amp;lt;bkupuser&amp;gt;@&amp;lt;bkuphost&amp;gt;:&amp;lt;backuppath&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;With the physical systems out of the way, we can now begin creating the different services that will be needed to setup our self hosted environment.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Self Hosting: Episode 0</title>
    <link href="https://obtusenet.com/blog/self-hosting-episode-0.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/self-hosting-episode-0.html</id>
    <published>2025-09-07T15:59:02Z</published>
    <updated>2025-09-07T15:59:02Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>As with all endeavors, success is not guaranteed.</summary>
    <category term="self-hosted"/>
    <content type="html">&lt;p&gt;As with all endeavors, success is not guaranteed. However, without a plan... failure is.&lt;/p&gt;
&lt;p&gt;In this series I hope to provide instruction on how to setup and self host an environment that can provide everything necessary to function in todays connected world. While I will be simplifying setups and configurations, it is my goal that these instructions can be extended to businesses.&lt;/p&gt;
&lt;p&gt;As with all things on the Internet, this is my opinion. I may not do things exactly as you would. That is fine. Please take this as a starting point and extend in whatever way you see fit. Now to the planning....&lt;/p&gt;
&lt;h2 id=&#34;segmentation-plan&#34;&gt;Segmentation Plan&lt;/h2&gt;
&lt;p&gt;Before we start creating servers, VMs, containers, etc. we need to have a quick think about segmentation. This environment can and will have all sorts of devices on it. These devices will be used for different services and have different levels of trust. For this series the following Places In the Network (PIN) will be used as a way to segment different devices.&lt;/p&gt;
&lt;h3 id=&#34;external-segment&#34;&gt;External Segment&lt;/h3&gt;
&lt;p&gt;This segment represents the outside untrusted world. Only the firewall(s) should ever be connected to this segment. There should be a default deny on all inbound traffic.&lt;/p&gt;
&lt;p&gt;Wither that default deny is a DROP (all traffic is just thrown away. No return traffic is ever sent) or a RETURN (ICMP return traffic is sent on bad packets) is a topic of much debate. With the DROP setup the firewall appears to be non-existent from the Internet. Only &amp;quot;known&amp;quot; communications will ever connect. However, this might slow down some services that will try to &amp;quot;test&amp;quot; for the presence of a service. If you do not know you need the RETURN response then do DROP for the external interface.&lt;/p&gt;
&lt;h3 id=&#34;dmz-segment&#34;&gt;DMZ Segment&lt;/h3&gt;
&lt;p&gt;This segment will be where devices that can be accessed from outside our environment will be placed. These devices can/should be proxied in some form or fashion but since direct access to them will be provided we should separate these devices. Devices in this environment should not be able to reach back to Common Segment. Reachback to the IOT segment should require specific rules.&lt;/p&gt;
&lt;h3 id=&#34;iot-segment&#34;&gt;IOT Segment&lt;/h3&gt;
&lt;p&gt;This segment will be where devices that are IOT in nature are placed. (Cameras, outlets, doorbells, etc) Devices in this environment will not be able to reach out to the greater Internet or any other segment without explicit rules.&lt;/p&gt;
&lt;h3 id=&#34;common-segment&#34;&gt;Common Segment&lt;/h3&gt;
&lt;p&gt;This segment will be where all other devices will be placed. Access from this segment to DMZ or IOT is generally allowed.&lt;/p&gt;
&lt;h3 id=&#34;other-segments&#34;&gt;Other Segments&lt;/h3&gt;
&lt;p&gt;Other segments can be created as necessary. Word of caution, each segment carries with it a new IP space, possibly a new domain, and additional firewall rules and considerations on how this segment should interact with all other segments. In some cases this interaction is simple and in others it can be complex. Adding additional segments has an exponential cost. I am definitely not saying &amp;quot;Don&#39;t add any more segments.&amp;quot;, I am only stating, understand the cost.&lt;/p&gt;
&lt;p&gt;Here is a quick table that will be referenced by other episodes.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Segment&lt;/th&gt;
&lt;th&gt;Vlan&lt;/th&gt;
&lt;th&gt;Bridge&lt;/th&gt;
&lt;th&gt;Address/Mask&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Common&lt;/td&gt;
&lt;td&gt;Vlan 10&lt;/td&gt;
&lt;td&gt;Bridge 1&lt;/td&gt;
&lt;td&gt;192.168.10.0/24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IOT&lt;/td&gt;
&lt;td&gt;Vlan 20&lt;/td&gt;
&lt;td&gt;Bridge 2&lt;/td&gt;
&lt;td&gt;192.168.20.0/24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DMZ&lt;/td&gt;
&lt;td&gt;Vlan 30&lt;/td&gt;
&lt;td&gt;Bridge 3&lt;/td&gt;
&lt;td&gt;192.168.30.0/24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External&lt;/td&gt;
&lt;td&gt;Vlan 90&lt;/td&gt;
&lt;td&gt;Bridge 4&lt;/td&gt;
&lt;td&gt;10.0.0.0/24&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;iterative-approach&#34;&gt;Iterative approach&lt;/h2&gt;
&lt;p&gt;With this series we will touch on many topics. I will purposely limit my discussion on various topics to just the item at hand and when the time if right my come back to the topic to extend with additional items as required. As I know that people will skip around when reading the articles, I will do my best to refer back to previous articles where a similar topic was covered.&lt;/p&gt;
&lt;h2 id=&#34;services&#34;&gt;Services&lt;/h2&gt;
&lt;p&gt;Finally, the good stuff. The following services will be required to build the self hosted environment. These services can run on whatever hardware you have available. (Or virtual hardware of the cloud). How you choose to deploy these services is up to you and your budget. In these examples I will assume that all hardware an services are &amp;quot;local&amp;quot; unless otherwise specified.&lt;/p&gt;
&lt;h3 id=&#34;list-of-services&#34;&gt;List of services&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt; - We will need some place to store our OS&#39;s, files etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Virtual Machines&lt;/strong&gt; - We will need a place to create virtual machines and services to run within the environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firewall&lt;/strong&gt; - Perhaps the most important service (only slightly ahead of DNS). This will allow communication between the different segments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS/DHCP&lt;/strong&gt; - It all starts with DNS. We will create a DNS environment that we can build and add onto.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Certificates&lt;/strong&gt; - Right behind DNS we will create our own Certificate Authority and manage our own certificates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accounts&lt;/strong&gt; - Even if this environment is only used for a single person, different types of accounts will be necessary.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSO/MFA&lt;/strong&gt; - With all these accounts, being forced to login to every machine/service every time gets annoying. Setting up a SSO/MFA solution will help consolidate the authentication.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logging&lt;/strong&gt; - What happens with things go wrong... and they will. We need logs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Email&lt;/strong&gt; - This is what it is all about? Well not totally, but email still plays an important role with interacting with the outside world.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not the complete of services that we can host, but this is my list of required services that we must host in order to really host anything else.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Throughout this series I will be running different operating systems and software. I will endeavor to list the version used.&lt;/p&gt;
&lt;p&gt;Now we have a plan. Success still isn&#39;t guaranteed... but we can now measure how close we came.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Password sensibilities...</title>
    <link href="https://obtusenet.com/blog/password-sensibilities.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/password-sensibilities.html</id>
    <published>2024-10-11T18:27:26Z</published>
    <updated>2024-10-12T15:00:17Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>Finally some password sense! NIST has released some updates for password verifiers (SP 800-63B). While this isn&#39;t everyones cup of tea, it is nice when things like this come out to with discussions around security best practices.</summary>
    <category term="editorial"/>
    <category term="security"/>
    <category term="password"/>
    <content type="html">&lt;p&gt;&lt;img src=&#34;assets/password-sensibilities/photo-1727887746394-8b5c23c98b40.jpg&#34; alt=&#34;Password sensibilities...&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Photo by &lt;a href=&#34;https://unsplash.com/@dcanadianphotographer?utm_source=ghost&amp;amp;utm_medium=referral&amp;amp;utm_campaign=api-credit&#34;&gt;David Gomez&lt;/a&gt; / &lt;a href=&#34;https://unsplash.com/?utm_source=ghost&amp;amp;utm_medium=referral&amp;amp;utm_campaign=api-credit&#34;&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally, some password sense! NIST has released some updates for password verifiers (&lt;a href=&#34;https://pages.nist.gov/800-63-4/sp800-63b.html&#34;&gt;SP 800-63B&lt;/a&gt;). While this isn&#39;t everyones cup of tea, it is nice when things like this come out to with discussions around security best practices.&lt;/p&gt;
&lt;h2 id=&#34;highlights&#34;&gt;Highlights&lt;/h2&gt;
&lt;p&gt;Some highlights from section 3.1.1.2 and my thoughts...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. Verifiers and CSPs SHALL require passwords to be a minimum 
of eight characters in length and SHOULD require passwords 
to be a minimum of 15 characters in length.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should already be in place.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2. Verifiers and CSPs SHOULD permit a maximum password
length of at least 64 characters.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This seems slightly silly. Passwords should &lt;em&gt;&lt;strong&gt;not&lt;/strong&gt;&lt;/em&gt; be stored in plaintext. If required, their hashes should be stored and those will use a fixed amount of space. Now, if the max length needs to be limited to prevent some denial of service, that would make sense, but set the length to 1000 characters.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3. Verifiers and CSPs SHOULD accept all printing ASCII
[RFC20] characters and the space character in passwords.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yes! Once again, passwords should not be store plain text. So if the passwords are being hashed, then the characters shouldn&#39;t matter!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5. Verifiers and CSPs SHALL NOT impose other composition
rules (e.g., requiring mixtures of different character 
types) for passwords.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;100 x Yes! Making passwords harder for people to type and impossible for them to remember doesn&#39;t make more secure passwords.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;6. Verifiers and CSPs SHALL NOT require users to change 
passwords periodically. However, verifiers SHALL force a 
change if there is evidence of compromise of the 
authenticator.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1000 x Yes! Forcing periodic password changes forces people into bad habits.. (e.g. $uperP@ss01, $uperP@ss02, etc). Only change when required!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;7. Verifiers and CSPs SHALL NOT permit the subscriber to 
store a hint that is accessible to an unauthenticated 
claimant.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why was this even a thing? If I have to use this, just send me through the password reset process.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;8. Verifiers and CSPs SHALL NOT prompt subscribers to use
knowledge-based authentication (KBA) (e.g., “What was the 
name of your first pet?”) or security questions when 
choosing passwords.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Awesome! I no longer have to make up random crap and store that crap. Once again, if I can&#39;t remember what I entered, I&#39;m headed for the password reset process.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;9. Verifiers SHALL verify the entire submitted password
(i.e., not truncate it).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should really go without saying. Once again, hash the password and verify the hash!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Verifiers SHALL allow the use of password managers. 
Verifiers SHOULD permit claimants to use the “paste” 
functionality when entering a password to facilitate 
their use.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I was shocked when a company I worked for disallowed browser integrated password managers, forcing a person to copy the password to the clipboard (which is visible to all applications) and then to the password field. Now I know this doesn&#39;t exactly say &amp;quot;browser integrated password managers&amp;quot;, but if it&#39;s not easy... people will not use it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Email SHALL NOT be used for out-of-band authentication because
it may be vulnerable to:

 -  Accessibility using only a password
 -  Interception in transit or at intermediate mail servers
 -  Rerouting attacks, such as those caused by DNS spoofing
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Email is easy to use and easy to abuse. While hopefully, an email user is using MFA / passkey / OTP to login, but there is no guarantee. And since DNSSEC and SMTPS are not ubiquitous, interception of the out-of-band authentication is possible. Although allowing some to login via a link in email, is easy... no tech experience required. :/&lt;/p&gt;
&lt;p&gt;Section 5.1.1 even covers how browser cookies should be used for session maintenance.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Now I don&#39;t expect the password silliness to change over night. But now there is a government document that companies can refer to as &amp;quot;best practices&amp;quot;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Notes on DPI...</title>
    <link href="https://obtusenet.com/blog/notes-on-dpi.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/notes-on-dpi.html</id>
    <published>2024-09-06T13:40:03Z</published>
    <updated>2024-09-06T13:40:03Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>Some quick notes on setting DPI for High Density Displays and X11.</summary>
    <category term="cwm"/>
    <category term="openbsd"/>
    <category term="window-managers"/>
    <category term="x11"/>
    <category term="notes"/>
    <content type="html">&lt;p&gt;&lt;img src=&#34;assets/notes-on-dpi/photo-1502043150060-b01aa3030556.jpg&#34; alt=&#34;Notes on DPI...&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Photo by &lt;a href=&#34;https://unsplash.com/@christiankaindl?utm_source=ghost&amp;amp;utm_medium=referral&amp;amp;utm_campaign=api-credit&#34;&gt;Christian Kaindl&lt;/a&gt; / &lt;a href=&#34;https://unsplash.com/?utm_source=ghost&amp;amp;utm_medium=referral&amp;amp;utm_campaign=api-credit&#34;&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Some quick notes on setting DPI for High Density Displays and X11.&lt;/p&gt;
&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;On &amp;quot;normal&amp;quot; displays within X11 it is assumed that there are 96 pixels/dots per inch (DPI). This is fine for most cases. However, with &amp;quot;newer&amp;quot; displays, including on laptops, having double or more DPI this can make text really hard to see. Here are some tips that I&#39;ve collected to help bring the fonts back in line.&lt;/p&gt;
&lt;h2 id=&#34;setting-the-correct-dpi&#34;&gt;Setting the correct DPI...&lt;/h2&gt;
&lt;p&gt;My daily window manager is &lt;a href=&#34;https://man.openbsd.org/cwm&#34;&gt;cwm(1)&lt;/a&gt;. within my ~/.xsession script I have the following.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DPI=`xdpyinfo | grep resolution | awk &#39;{ print $2 }&#39; | cut -f1 -dx`
DPI=${DPI:-96}
echo &amp;quot;DPI=${DPI}&amp;quot;
xrandr --dpi ${DPI}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;~/.xsession snippet&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This uses &lt;a href=&#34;https://man.openbsd.org/xdpyinfo&#34;&gt;xdpyinfo(1)&lt;/a&gt; to fetch the DPI identified by X11. If not there or something goes wrong with the parse, assume 96 DPI and then output the setting (which is captured in ~/.xsession-errors) and use &lt;a href=&#34;https://man.openbsd.org/xrandr&#34;&gt;xrandr(1)&lt;/a&gt; to set the DPI.&lt;/p&gt;
&lt;h2 id=&#34;setting-the-pointer&#34;&gt;Setting the pointer...&lt;/h2&gt;
&lt;p&gt;This will make the mouse pointers really small. To fix this I export the following settings (from the .xsession script).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export XCURSOR_PATH=&amp;quot;~/.icons:/usr/local/lib/X11/icons:$XCURSOR_PATH&amp;quot;
export XCURSOR_SIZE=&amp;quot;64&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;~/.xsession snippet&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The first export adds some new paths to look for new cursors. Use pkg_add(1) script to fine cursors that can be installed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pkg_add -Q cursor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also set the size to 64. (This should be multiples of 8). But on my display 64 seems to be a good size.&lt;/p&gt;
&lt;h2 id=&#34;setting-the-xresources&#34;&gt;Setting the Xresources...&lt;/h2&gt;
&lt;p&gt;Updating the ~/.Xresources file will also help for programs that use this.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;! set cursor size
Xcursor.theme: dmz-aa
Xcursor.size: 64

! default face name to use
*faceName: CodeNewRoman Nerd Font:style=Bold:size=20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;~/.Xresources snippet&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This also sets the cursor theme to use and size (duplicates the exports, but doesn&#39;t hurt). This also sets the default face name, style and size to use if not overridden by more specific settings.&lt;/p&gt;
&lt;h2 id=&#34;browsers&#34;&gt;Browsers...&lt;/h2&gt;
&lt;p&gt;Naturally the browsers don&#39;t seem to obey any of the X11 settings. Here are some specific settings that seem to help.&lt;/p&gt;
&lt;h3 id=&#34;firefox&#34;&gt;Firefox&lt;/h3&gt;
&lt;p&gt;Open &lt;em&gt;about:config&lt;/em&gt; in the &lt;a href=&#34;https://openports.eu/ports/www/mozilla-firefox&#34;&gt;firefox&lt;/a&gt; multibar. Find &lt;em&gt;layout.css.devPixelsPerPx&lt;/em&gt; and set to an appropriate number. Less than 1.0 makes things smaller and greater than 1.0 make thing larger. Updates take place immediately so it&#39;s nice that changes can be seen without restarting.&lt;/p&gt;
&lt;h3 id=&#34;chrome&#34;&gt;Chrome&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://openports.eu/ports/www/chromium&#34;&gt;Chrome&lt;/a&gt; is better/worse depending on your point of view. There doesn&#39;t seem to be any config setting to set. Instead a CLI option &lt;em&gt;--force-device-scale-factor=x&lt;/em&gt; is used. As with firefox, number less than 1.0 will make things smaller and number larger than 1.0 will make thing bigger.&lt;/p&gt;
&lt;p&gt;Since I usually launch chrome from cwm application menu, I have a simple shell script that add the CLI option.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh

SF=${SCALE_FACTOR:-1.0}
ARGS=&amp;quot;--force-device-scale-factor=${SF}&amp;quot;

/usr/local/bin/chrome ${ARGS} $@
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;start_chrome.sh&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In the ~/.xsession script I set an environment variable of SCALE_FACTOR and then use that in this script to set the scale factor for chrome.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Other window managers / desktop environments like &lt;a href=&#34;https://openports.eu/ports/meta/xfce&#34;&gt;XFCE&lt;/a&gt;, &lt;a href=&#34;https://openports.eu/ports/meta/gnome&#34;&gt;Gnome&lt;/a&gt;, &lt;a href=&#34;https://openports.eu/ports/meta/kde&#34;&gt;KDE&lt;/a&gt; seem to have their own ways of handling the display DPI discrepancy. They each have their plusses and minuses, but all require more dependencies. Choose what is right for you.&lt;/p&gt;
&lt;p&gt;This doesn&#39;t take care of 100% of the applications, but covers the ones I use most.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>HA @ Home</title>
    <link href="https://obtusenet.com/blog/ha-home.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/ha-home.html</id>
    <published>2024-08-05T20:48:35Z</published>
    <updated>2024-08-05T20:51:12Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>At work I had build a global, highly redundant, scalable network that always provided best experience no matter what was thrown at it. At home... well, you&#39;ve heard of the cobblers children?</summary>
    <category term="openbsd"/>
    <category term="network"/>
    <category term="home"/>
    <category term="howto"/>
    <content type="html">&lt;p&gt;&lt;img src=&#34;assets/ha-home/photo-1452626212852-811d58933cae.jpg&#34; alt=&#34;HA @ Home&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Photo by &lt;a href=&#34;https://unsplash.com/@nwphoto?utm_source=ghost&amp;amp;utm_medium=referral&amp;amp;utm_campaign=api-credit&#34;&gt;Nathan Walker&lt;/a&gt; / &lt;a href=&#34;https://unsplash.com/?utm_source=ghost&amp;amp;utm_medium=referral&amp;amp;utm_campaign=api-credit&#34;&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I, like many of you, have been working from home since the pandemic. As a network engineer, I worked from anywhere I needed to as the job demanded. I met with my team and customers over WebEx, Zoom, Meet and later Teams. I was already pandemic ready, so when I had to stay home, it really wasn&#39;t a huge change... except for the network.&lt;/p&gt;
&lt;p&gt;At work I had build a global, highly redundant, scalable network that always provided best experience no matter what was thrown at it. At home... well, you&#39;ve heard of the cobblers children? Don&#39;t get me wrong, the network worked. I could watch easily watch streaming shows all day long. However, throw streaming plus online gaming, online learning, et cetera and the network was not something I was going to show off.&lt;/p&gt;
&lt;p&gt;I am not going to go through all of the changes/upgrades I made in this article. I will cover how I went from a single ISP point of failure to highly available, resilient internet connection.&lt;/p&gt;
&lt;p&gt;First step... get another ISP. I do not plan to name the ISP&#39;s I ended up using. Depending on where you are in the world, you may only have access to a limited number of ISPs. For me, my original ISP (cable provider) was charging nearly $200 a month. The service wasn&#39;t bad, for the most part but this wasn&#39;t a business class service. So my first step was to go 5G. Getting that setup was fairly easy. The modem I was provided had the ability for an external antenna so I pick one up as well. That seemed to help some, but not as much as I would have hoped. While the service was good, I did notice that there was a large amount of jitter and the latency increased and the bandwidth increased. So I canceled the cable and ordered a &#39;fiber to the home ISP&#39; that just happened to be expanding into my area.&lt;/p&gt;
&lt;h2 id=&#34;basic-setup&#34;&gt;Basic Setup&lt;/h2&gt;
&lt;p&gt;I run OpenBSD as my firewall (big surprise if you have read any of my other articles). Here is my basic setup.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# Firewall PF
#
all_nets = &amp;quot;192.168.0.0/16&amp;quot;
all6_nets = &amp;quot;2001:db8::/32&amp;quot;
vpn_subnet = &amp;quot;192.0.2.0/24&amp;quot;
wan_if=&amp;quot;em3&amp;quot;
lan_if=&amp;quot;em1&amp;quot;

set skip on lo
set skip on enc
set state-defaults pflow

table &amp;lt;inet_doh&amp;gt; persist file &amp;quot;/var/db/pf.internet_doh_servers&amp;quot;
table &amp;lt;internal_dns&amp;gt; persist file &amp;quot;/var/db/pf.internal_dns_servers&amp;quot;
table &amp;lt;banned&amp;gt; persist file &amp;quot;/var/db/pf.banned&amp;quot;
table &amp;lt;sshguard&amp;gt; persist
table &amp;lt;time_blocked&amp;gt; persist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;partial /etc/pf.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Pretty standard, first specify macros, next setup &#39;skip&#39; and other global settings, followed up by common tables.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# default block
block log
# block anything in the time_blocked table
block quick from &amp;lt;time_blocked&amp;gt;
# block bad hosts
block quick from &amp;lt;banned&amp;gt;
# capture NTP from any internal sources and respond for them
pass in proto udp from $all_nets to port 123 rdr-to 127.0.0.1 port 123
pass in inet6 proto udp from $all6_nets to port 123 rdr-to ::1 port 123

pass out inet6 proto icmp6 from self to $all6_nets

# pass all and pfsync carp protocol
pass out quick on carp proto carp
pass quick on $lan_if proto pfsync
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;partial /etc/pf.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Next setup the common rules. Default block and log, block time based addresses, quick block of banned addresses. Capture all NTP and respond from ourselves. And finally permit CARP and PFSYNC.&lt;/p&gt;
&lt;p&gt;Finally specify my zones.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;include &amp;quot;/etc/pf.d/pf.anchor.lan&amp;quot;
include &amp;quot;/etc/pf.d/pf.anchor.wan&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;partial /etc/pf.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;My actual configuration has a few more zones than this, but for this example we&#39;ll focus on 2.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# WAN Anchor
# NOTE: (WAN:0) will be different per FW pair
#

anchor &amp;quot;wan&amp;quot; on WAN {
  # block everything in
  block in log
  # match outbound traffic to WAN interfaces
  match out nat-to (WAN:0)
  # pass traffic out
  pass out set queue(DEFAULT, ACK)
  # stop DNS/DOH bypass
  block out quick log proto tcp to &amp;lt;inet_doh&amp;gt; port 443 ! tagged DNS
  block out quick log proto { udp tcp } to port { 53 853 } ! tagged DNS
  # allow ourselves out
  pass out proto { udp tcp } to port { 53 853 } tagged DNS set queue(DNS)
  pass out proto { tcp udp } from self user = &amp;quot;_unbound&amp;quot; set queue(DNS)
  pass out proto { tcp udp } to port { 80, 443, 8443 } set queue(WEB, ACK)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/pf.d/pf.anchor.wan&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Anything tagged as a WAN interface will have this NAT applied. Basically it will block all inbound traffic and NAT all outbound traffic. It will block traffic to known DoH servers but allow my DNS servers out.&lt;/p&gt;
&lt;p&gt;For the LAN interface&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# PF LAN
anchor &amp;quot;lan&amp;quot; on LAN {
  pass log set queue(LAN_DEF,LAN_ACK)
  pass in inet6 from any to 64:ff9b::/96 af-to inet from (WAN:0) set queue(LAN_DEF,LAN_ACK)
  match in from &amp;lt;internal_dns&amp;gt; tag DNS set queue(LAN_DEF,LAN_ACK)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/pf.d/pf.anchor.lan&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Anything on the LAN is good?! Right. For now let&#39;s assume so, so pass it. If traffic comes from my internal DNS servers tag it for the WAN rule processing.&lt;/p&gt;
&lt;p&gt;One thing that may not be familiar is the use of the &lt;em&gt;queue()&lt;/em&gt; syntax. More on that later when we talk about buffer bloat.&lt;/p&gt;
&lt;p&gt;Now with the PF rules in place, just set the groups on the interfaces.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;group WAN
inet autoconf
up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/hostname.em3&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;group LAN
inet 192.168.1.2/24
inet6 2001:db8::2 64
up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/hostname.em1&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Don&#39;t forget to enable IP forwarding or the firewall will not work as expected.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# sysctl net.inet.ip.forwarding=1
# sysctl net.inet6.ip6.forwarding=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;sysctl forwarding commands&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Since we plan on having redundant firewalls to eliminate a device failure we will need to setup &lt;a href=&#34;https://man.openbsd.org/carp&#34;&gt;carp(4)&lt;/a&gt;. This will provide a first hop redundancy protocol (FHRP). The setup of carp is very easy.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;group LAN
vhid 3
carpdev em1
pass letmein
state master
inet 192.168.1.1/24
up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/hostname.carp1&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Match the group of the main interfaces. Set the ID. This much match on the peer router. Tie the carp interface to a physical interfaces and set a password (optional). Set the default state. Once should be &#39;master&#39; the other &#39;backup&#39;. Finally set the floating IP and bring the interface up.&lt;/p&gt;
&lt;p&gt;By default the &lt;a href=&#34;https://man.openbsd.org/sysctl&#34;&gt;&lt;em&gt;sysctl(8)&lt;/em&gt;&lt;/a&gt; for carp are setup to have carp enabled. To see what the current settings are use the following command&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# sysctl net.inet.carp
net.inet.carp.allow=1
net.inet.carp.preempt=0
net.inet.carp.log=2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;sysctl command and output&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;buffer-bloat-simply-put&#34;&gt;Buffer Bloat... simply put&lt;/h2&gt;
&lt;p&gt;Network devices, especially router, have different speed interfaces. Today it is not unusual to have 1G / 10G on a home LAN and less than that for the WAN interface. Sometime an order of magnitude smaller. To compensate for this difference bandwidth, routers use temporary storage, called buffers, so store packets while they wait their turn to be sent on the slower interfaces. Without special configurations, packets are processed in a FIFO (first in, first out) queue. Meaning that if a larger packet is queued before a smaller packet, the smaller packet will need to wait until the larger packet is sent before it gets a turn.&lt;/p&gt;
&lt;p&gt;Now let&#39;s consider that the smaller packet is an ACK. And that ACK is being sent in response to a on-line game. Do we want that packet waiting for some other non-time sensitive packets... say a web page? Probably not.&lt;/p&gt;
&lt;h2 id=&#34;fighting-the-bloat-queuing&#34;&gt;Fighting the bloat... Queuing&lt;/h2&gt;
&lt;p&gt;Fortunately, OpenBSD has an answer ready to go. Since version 6.2 OpenBSD has a queuing mechanism in PF that will help us overcome buffer bloat (called FQ-CoDel for those that are curious). We can specify our upload speed of the main FIFO queue and setup flow based sub-queues for our traffic.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# WAN
queue INET on $wan_if bandwidth 95M max 95M
queue ACK parent INET bandwidth 1M max 10M flows 1024 qlimit 1024
queue DNS parent INET bandwidth 5M flows 1024 qlimit 1024
queue WEB parent INET bandwidth 50M flows 1024 qlimit 1024
queue SSH parent INET bandwidth 20M flows 1024 qlimit 1024
queue GAME parent INET bandwidth 20M flows 1024 qlimit 1024
queue DEFAULT parent INET bandwidth 90M flows 1024 qlimit 1024 default
# LAN
queue LAN on $lan_if bandwidth 1G
queue LAN_ACK parent LAN bandwidth 1G flows 1024 qlimit 1024
queue LAN_DEF parent LAN bandwidth 1G flows 1024 qlimit 1024 default
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;partial /etc/pf.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As mentioned the INET queue has a bandwidth set to match the upload speed of the connection as well as the max bandwidth setting. It is important, for queuing to work correctly that the bandwidth be set around 95% of the available upstream bandwidth.&lt;/p&gt;
&lt;p&gt;Next create one or more child queues and set the bandwidth they can should consume. If max is not specified, the queues will be allowed to take more, if available. Here a qlimit and flow limit for each queue is also specified increasing from the default. This is optional. To see if a queue is getting maxed out and dropping traffic you can use the following command. Make notes of any &lt;em&gt;dropped pkts&lt;/em&gt; or &lt;em&gt;qlength&lt;/em&gt; being larger than 0.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# pfctl -v -sq
queue INET on em3 bandwidth 100M
  [ pkts:          0  bytes:          0  dropped pkts:      0 bytes:      0 ]
  [ qlength:   0/ 50 ]
queue ACK parent INET flows 1024 bandwidth 1M qlimit 1024
  [ pkts:   31459517  bytes: 1770765239  dropped pkts:      0 bytes:      0 ]
  [ qlength:   0/1024 ]
queue DNS parent INET flows 1024 bandwidth 5M qlimit 1024
  [ pkts:     465724  bytes:   43885028  dropped pkts:      0 bytes:      0 ]
  [ qlength:   0/1024 ]
queue WEB parent INET flows 1024 bandwidth 50M qlimit 1024
  [ pkts:    2913536  bytes: 2104425594  dropped pkts:      0 bytes:      0 ]
  [ qlength:   0/1024 ]
queue SSH parent INET flows 1024 bandwidth 20M qlimit 1024
  [ pkts:          0  bytes:          0  dropped pkts:      0 bytes:      0 ]
  [ qlength:   0/1024 ]
queue GAME parent INET flows 1024 bandwidth 20M qlimit 1024
  [ pkts:       5751  bytes:     596945  dropped pkts:      0 bytes:      0 ]
  [ qlength:   0/1024 ]
queue DEFAULT parent INET flows 1024 bandwidth 90M default qlimit 1024
  [ pkts:    1723990  bytes:  633385764  dropped pkts:      0 bytes:      0 ]
  [ qlength:   0/1024 ]
queue LAN on em1 bandwidth 1G
  [ pkts:          0  bytes:          0  dropped pkts:      0 bytes:      0 ]
  [ qlength:   0/ 50 ]
queue LAN_ACK parent LAN flows 1024 bandwidth 1G qlimit 1024
  [ pkts:     749181  bytes:   50240090  dropped pkts:      0 bytes:      0 ]
  [ qlength:   0/1024 ]
queue LAN_DEF parent LAN flows 1024 bandwidth 1G default qlimit 1024
  [ pkts: 1229933940  bytes: 320469964876  dropped pkts:      0 bytes:      0 ]
  [ qlength:   0/1024 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&#34;https://man.openbsd.org/systat&#34;&gt;&lt;em&gt;systat(1)&lt;/em&gt;&lt;/a&gt; command can also give you a real time look at the queues in action.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# systat queue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;systat command example&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;got-one-now-repeat-for-two&#34;&gt;Got one... now repeat for two&lt;/h2&gt;
&lt;p&gt;Now that we have one firewall up and working we can repeat the process for the second firewall with the second ISP. After that we will need to tie them together.&lt;/p&gt;
&lt;p&gt;Since both firewalls are up and functioning, let&#39;s make sure that CARP is working as well as PFSYNC. The command&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ifconfig carp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;example showing CARP interfaces&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;will show all carp and pfsync interfaces and their current state. On one firewall you should see the interface be MASTER and on the other firewall it should state BACKUP. If it states anything else, CARP is not working and this problem will need to be solved first.&lt;/p&gt;
&lt;p&gt;Now to test failover of CARP. The easiest way to do this is on the master systems demote the carp interface. To check the current demotion use this command&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ifconfig -g carp
carp: carp demote count 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;example showing carp demotion counter&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now let&#39;s demote the master firewall and check again.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ifconfig -g carp carpdemote
# ifconfig -g carp
carp: carp demote count 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;example demoting carp&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now check the status of the &amp;quot;backup&amp;quot; firewall. We should see the carp interfaces goto MASTER state. Checking the dmesg should also show the CARP interface state transition.&lt;/p&gt;
&lt;p&gt;To remove the demotion us the following command&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ifconfig -g carp -carpdemote
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we have not enabled preempt in the sysctls, the MASTER status will not fail back over to the &amp;quot;primary&amp;quot; firewall until the &amp;quot;backup&amp;quot; firewall is demoted.&lt;/p&gt;
&lt;p&gt;There may be reasons why we want to keep one firewall/ISP as the &amp;quot;primary&amp;quot; unless it has failed. In those cases make sure the set the &lt;em&gt;net.inet.cap.preempt&lt;/em&gt; to 1.&lt;/p&gt;
&lt;h2 id=&#34;automatic-demotion&#34;&gt;Automatic demotion&lt;/h2&gt;
&lt;p&gt;Now that we have the firewalls and ISP setup and we&#39;ve tested performance and failover we need failover to happen automatically. In OpenBSD this is easily accomplished with &lt;a href=&#34;https://man.openbsd.org/ifstated&#34;&gt;&lt;em&gt;ifstated(8)&lt;/em&gt;&lt;/a&gt; and some shell scripts.&lt;/p&gt;
&lt;p&gt;Before we get to the config, lets consider what we want to happen. First if the WAN interface is down, we can&#39;t pass packet we want the failover to occur. But what happens if the interface is up but he ISP&#39;s router isn&#39;t available. We should test for the default gateway to be up. Next what happens if the default gateway is up but there is no path to the Internet? Next what happens if there is a path to the Internet but performance is really bad? So here we have 4 tests...&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WAN interface up&lt;/li&gt;
&lt;li&gt;WAN default gateway present&lt;/li&gt;
&lt;li&gt;Internet accessable&lt;/li&gt;
&lt;li&gt;Internet performance ok&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We can check all those items with &lt;em&gt;ifstated&lt;/em&gt; daemon. Below is a config an helper script to continually test for the &amp;quot;health&amp;quot; of the connection.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 
dmz_if_up = &#39;em3.link.up&#39;
dmz_if_down = &#39;em3.link.down&#39;

external_gw=&#39;&amp;quot;ping -q -c 1 -w 1 `route -n show -inet | grep default | awk \&#39;{ print $2 }\&#39;` &amp;gt;/dev/null 2&amp;gt;&amp;amp;1&amp;quot; every 60&#39;

performance=&#39;&amp;quot;/root/check_perf.sh X.X.X.X &amp;gt;/dev/null 2&amp;gt;&amp;amp;1&amp;quot; every 60&#39;

# ifstated starts up with the first defined state
state neutral {
        if $dmz_if_down {
                run &amp;quot;logger -st ifstated &#39;uplink down&#39;&amp;quot;
                set-state demoted
        }
        if ! $external_gw {
                run &amp;quot;logger -st ifstated &#39;could not reach cable router&#39;&amp;quot;
                set-state demoted
        }
        if ! $performance {
                run &amp;quot;logger -st ifstated &#39;perforamce degraded&#39;&amp;quot;
                set-state demoted
        }
}

state demoted {
        init {
                run &amp;quot;ifconfig -g carp carpdemote&amp;quot; 
        }
        if $dmz_if_up &amp;amp;&amp;amp; $external_gw &amp;amp;&amp;amp; $performance {
                run &amp;quot;logger -st ifstated &#39;cable router ok&#39;&amp;quot;
                # remove our carp demotion
                run &amp;quot;ifconfig -g carp -carpdemote&amp;quot; 
                set-state neutral
        }
}

## commands in the global scope are always run
if carp3.link.up
        run &amp;quot;logger -st ifstated &#39;carp3 is master&#39;&amp;quot;
if carp3.link.down
        run &amp;quot;logger -st ifstated &#39;carp3 is backup&#39;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/etc/ifstated.conf&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh

if [ $# -lt 1 ]; then
        echo &amp;quot;${0##*/} &amp;lt;host&amp;gt;&amp;quot;
        exit 2
fi

LIMIT=${LIMIT:-200}
TRIES=${TRIES:-5}
WAIT=${WAIT:-1}

rtt=`ping -q -c ${TRIES} -w ${WAIT} ${1} 2&amp;gt;&amp;amp;1 | sed -n &#39;/^round-trip/p&#39; | cut -f4 -d&amp;quot; &amp;quot; | cut -f2 -d&amp;quot;/&amp;quot;`

if [ -z &amp;quot;${rtt}&amp;quot; ]; then
  echo &amp;quot;failed to get RTT&amp;quot; &amp;gt;&amp;amp;2
fi
rtt=${rtt%%.*}
if [ $rtt -gt $LIMIT ]; then
  echo &amp;quot;too high&amp;quot;
  exit 1
else
  echo &amp;quot;ok&amp;quot;
fi
exit 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;/root/check_perf.sh script&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The first checks &lt;strong&gt;dmz_if_up/down&lt;/strong&gt; check to make sure the WAN interface is up. The next check &lt;strong&gt;external_gw&lt;/strong&gt; verifies that the gateways is pingable every 60 seconds. The final test &lt;strong&gt;performance&lt;/strong&gt; checks for both internet reachability as well as performance. Select and IP or host what we want to ping every 60 seconds. The script below will send 5 pings and calculate the average response time. If any test fails then the performance/reachability is considered &amp;quot;failed&amp;quot; and state is set to &lt;strong&gt;demoted&lt;/strong&gt;. When entering demoted state, the first thing that is ran is the ifconfig demotion of CARP. Once the tests all succeed, the state is change back and the demotion is reversed.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;After running in this environment for a while it has worked pretty well. By and large my &amp;quot;home user community&amp;quot; has been generally happy. The environment seem to &amp;quot;do the right thing&amp;quot; when failures occur.&lt;/p&gt;
&lt;p&gt;Since traffic is split across 2 different ISP&#39;s when a failover occurs, all sessions are dropped and have to be re-established. By and large this is not a big deal. But since the IP addresses on the WAN interfaces are different there are a ton of errors with pfsync making me wonder I should even be worrying about pfsync across these firewalls.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>OpenBGPD + RTBH = dynamic PF rules</title>
    <link href="https://obtusenet.com/blog/openbgpd-rtbh-dynamic-pf-rules.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/openbgpd-rtbh-dynamic-pf-rules.html</id>
    <published>2023-08-28T15:39:39Z</published>
    <updated>2026-01-08T23:07:35Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>What if you could dynamically update PF tables across all your hosts? Here is one way you can...</summary>
    <content type="html">&lt;p&gt;&lt;img src=&#34;assets/openbgpd-rtbh-dynamic-pf-rules/photo-1649608118930-70b0a62cdf62.jpg&#34; alt=&#34;OpenBGPD + RTBH = dynamic PF rules&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Photo by &lt;a href=&#34;https://unsplash.com/@kirp?utm_source=ghost&amp;amp;utm_medium=referral&amp;amp;utm_campaign=api-credit&#34;&gt;Andrew Kliatskyi&lt;/a&gt; / &lt;a href=&#34;https://unsplash.com/?utm_source=ghost&amp;amp;utm_medium=referral&amp;amp;utm_campaign=api-credit&#34;&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you have been networking for a while you will come across the need to route&lt;br&gt;
traffic to Null0 (black hole).  For the uninitiated, this means that traffic will just be&lt;br&gt;
dropped by the router.   What if there was a way to automate adding these routes to firewall rules.  Good news!  There is.&lt;/p&gt;
&lt;p&gt;OpenBGPD is capable BGP daemon in it&#39;s own right.  It has a hidden capability.  It can add routes to a PF table.&lt;/p&gt;
&lt;p&gt;Why would you want to use this if you do not run a large routing network?  Glad you asked.  Let&#39;s say you have several servers on a hostile network doing server stuff (email, web, blog, etc). And being a good admin the have things pretty locked down.  The servers keep getting harassed by ne&#39;er-do-wells, filling logs with noise, etc. By using RTBH and OpenBGPD, harassment of one server is a block on all servers.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;assets/openbgpd-rtbh-dynamic-pf-rules/RTBHOverview.drawio-1-.svg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;headend-setup&#34;&gt;Headend Setup&lt;/h2&gt;
&lt;p&gt;First thing is to configure the BGP Headend router.  This router, and in fact&lt;br&gt;
all of the routing daemons, will not update the routing tables on the systems. They&lt;br&gt;
are just setup to exchange routes between each other.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-bgpd.conf&#34;&gt;# The ASN does not need to be registered.  In fact having it not registered
# will add protection that these records will not slip out into the
# greater Internet.
ASN=&amp;quot;65001&amp;quot;
# The RTBH community can also be anything we want.
RTBH=&amp;quot;65535&amp;quot;
# This list cooresponds to the list of PF tables.  There can be as many or
# few as required.
# Local
#  001 sshguard
#  002 banned
#  003 aggressive

# global configuration
AS $ASN
# prevent adding routes to host routing table
fib-update no
nexthop qualify via default

# Devices to peer with. Use of a password, may allow the neighbors to be
# wild carded, but may increase risk.
group &amp;quot;peers&amp;quot; {
        announce IPv4 unicast
        announce IPv6 unicast
        remote-as $ASN
        # since we are utilizing iBGP, we need to have the headend
        # be the route-reflector so we will not need a full mesh.
        route-reflector
        # Let out peers come to us.
        passive
        tcp md5sig password &amp;quot;super secure password here&amp;quot;
        neighbor &amp;lt;IP address of spoke&amp;gt;
}
# Allow iBGP routes in and out.  If this configuration is part of a router then
# the allow filters will be more involved.
allow from ibgp
allow to ibgp
# Match any routes that have the RTBH community and add them to the rtbh table.
# Enable if headend box will also participate in RTBH.  Dont forget to update
# the PF config.  Will need, at a minimum, a rtbh table.
#match from ibgp community $RTBH:* set { pftable rtbh }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;headend bgpd.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now enable BGP on the headend and start it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;rcctl enable bgpd
rcctl start bgpd
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;spoke-setup&#34;&gt;Spoke Setup&lt;/h2&gt;
&lt;p&gt;Next each spoke will need 3 parts: PF config update; BGP config; and a helper program. The first part is an update to the PF rules running on the device.  The&lt;br&gt;
following updates will add PF table(s) and a drop rule.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-pf.conf&#34;&gt;###### partial PF configuration
# store the routes learned from BGP
table &amp;lt;rtbh&amp;gt; persist
# locally banned hosts added manually
table &amp;lt;banned&amp;gt; persist
# aggressive hosts added automatically by PF
table &amp;lt;aggressive&amp;gt; persist

block quick from &amp;lt;rtbh&amp;gt; label &amp;quot;rtbh host&amp;quot;
block quick from &amp;lt;banned&amp;gt; label &amp;quot;banned host&amp;quot;
block quick from &amp;lt;aggressive&amp;gt; label &amp;quot;aggressive host&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Validate and restart PF.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;pfctl -nf /etc/pf.conf &amp;amp;&amp;amp; pfctl -f /etc/pf.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second part is the BGP config.  This config will be exactly the same on all&lt;br&gt;
the spoke servers.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-bgpd.conf&#34;&gt;# Almost exactly the same as the headend config
ASN=&amp;quot;65001&amp;quot;
RTBH=&amp;quot;65535&amp;quot;
# Local
#  001 sshguard
#  002 banned
#  003 aggressive

# global configuration
AS $ASN
fib-update no
nexthop qualify via default

group &amp;quot;peers&amp;quot; {
        announce IPv4 unicast
        announce IPv6 unicast
        remote-as $ASN
        tcp md5sig password &amp;quot;super secure password here&amp;quot;
        neighbor &amp;lt;IP address of headend&amp;gt;
}
allow from ibgp
allow to ibgp
match from ibgp community $RTBH:* set { pftable rtbh }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;spoke bgpd.conf&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now enable and start bgpd.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;rcctl enable bgpd
rcctl start bgpd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now for the last part... the watcher daemon.  OpenBGPD has the ability to add&lt;br&gt;
and remove addresses/networks using a PF table.  However, it does not monitor&lt;br&gt;
PF tables for additions/subtractions that are made outside the BGP daemon.&lt;br&gt;
This is where the watcher daemon comes in.  It watches one or more tables for&lt;br&gt;
additions/subtractions and utilizes &lt;em&gt;bgpctl&lt;/em&gt; commands to add/remove routes as&lt;br&gt;
they are added and removed from the table(s).&lt;/p&gt;
&lt;p&gt;The script can be downloaded from &lt;a href=&#34;https://github.com/mgraves00/watchtabled&#34;&gt;github&lt;/a&gt;.  &lt;strong&gt;Word of warning&lt;/strong&gt;, this is a&lt;br&gt;
minimal viable script. There are probably better programs out there. (If you&lt;br&gt;
know of any feel free to drop me an email.) However, it is lightweight and&lt;br&gt;
should run on any flavor of  *nix out there.&lt;/p&gt;
&lt;p&gt;Assuming you are configuring OpenBSD, there is a rc.d script. Enable the&lt;br&gt;
daemon, set the tables to watch, and then start.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;rcctl enable watchtabled
rcctl set watchtabled flag -t banned:65535:102 -t aggressive:65535:103
rcctl start watchtabled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now with everything running lets take a look at the tables.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;pfctl -T show -t table1 | wc -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Typing this can get tedious quickly.  I created a this &lt;a href=&#34;https://github.com/mgraves00/misc-scripts/blob/master/pf/show_pf_lists.sh&#34;&gt;script&lt;/a&gt; to help.&lt;/p&gt;
&lt;p&gt;One thing we should notice is there are no records.  While we have setup the&lt;br&gt;
environment to watch and replicate records, we need to add records.  There are&lt;br&gt;
several ways this can happen.  The most straight forward is to add them&lt;br&gt;
manually to the table.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;pfctl -T add -t banned 1.2.3.4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To see if the address is in the BGP daemon, use the &lt;em&gt;bgpctl&lt;/em&gt; command.  NOTE:&lt;br&gt;
that the community filter will need to match the community that references the&lt;br&gt;
specific PF table.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;bgpctl show ip bgp community 65535:102
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another option for adding records more automatically is to utilize the PF&lt;br&gt;
&#39;overload&#39; option.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#### partial PF config

pass in inet proto tcp from any to ($ext) port ssh keep state \
  (source-track rule, max-src-conn-rate 20/10, \
   overload &amp;lt;aggressive&amp;gt; flush global)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here aggressive hosts will trigger an addition to the aggressive PF&lt;br&gt;
table.  NOTE: the max connection rate rule will need to be tailored to the&lt;br&gt;
service.  SSH on a console server is much different that SSH on an Email&lt;br&gt;
server.&lt;/p&gt;
&lt;p&gt;Last, but not least, is the use of some other application like &lt;a href=&#34;https://www.sshguard.net/&#34;&gt;sshguard&lt;/a&gt; or&lt;br&gt;
&lt;a href=&#34;https://www.fail2ban.org/&#34;&gt;fail2ban&lt;/a&gt;.  The setup of these services is outside the scope of this post, but&lt;br&gt;
once setup to add &#39;bad hosts&#39; to PF tables, these tables can be watched and&lt;br&gt;
trigger RTBH bans.&lt;/p&gt;
&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Hopefully this article has help explain remote triggered blackholes and how they can be used to protect multiple systems from outside attacks.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>&#34;IT Security Is Broken?&#34;</title>
    <link href="https://obtusenet.com/blog/it-security-is-broken.html" rel="alternate" type="text/html"/>
    <id>https://obtusenet.com/blog/it-security-is-broken.html</id>
    <published>2023-01-15T15:01:03Z</published>
    <updated>2023-01-15T15:01:03Z</updated>
    <author><name>Michael Graves</name></author>
    <summary>Great interview between Ethan Banks and Mick Douglas discussion some of the failures in approach to security in business protection.</summary>
    <content type="html">&lt;p&gt;&lt;img src=&#34;assets/it-security-is-broken/photo-1591014827159-2e158ceac55d.jpg&#34; alt=&#34;&amp;quot;IT Security Is Broken?&amp;quot;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Photo by &lt;a href=&#34;https://unsplash.com/@egor_vikhrev?utm_source=ghost&amp;amp;utm_medium=referral&amp;amp;utm_campaign=api-credit&#34;&gt;Egor Vikhrev&lt;/a&gt; / &lt;a href=&#34;https://unsplash.com/?utm_source=ghost&amp;amp;utm_medium=referral&amp;amp;utm_campaign=api-credit&#34;&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Go listen to &lt;a href=&#34;https://packetpushers.net/podcast/day-two-cloud-177-it-security-is-broken-heres-ideas-on-how-to-fix-it/&#34;&gt;Day Two Cloud 177: IT Security is Broken; Here&#39;s Ideas On How To Fix It&lt;/a&gt;.  Great interview between Ethan Banks and Mick Douglas discussing some of the failures in approach to security in business protection.&lt;/p&gt;
&lt;p&gt;Some key points I took from this&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prevention is good.  Detection is key!&lt;/li&gt;
&lt;li&gt;My security is against not only attackers, but also compliance and auditors.  I need to survive all of them.&lt;/li&gt;
&lt;li&gt;Mono culture of OS and cloud deployments mean that the attackers have &amp;quot;my&amp;quot; container, &amp;quot;my&amp;quot; apps in their labs to test against before they get to me.&lt;/li&gt;
&lt;li&gt;Does security really protect the risk? Or am I spending $1M to protect $10.&lt;/li&gt;
&lt;li&gt;And finally... &amp;quot;Security is a journey and not a destination&amp;quot; or maybe better put &amp;quot;Security is a process and not a product&amp;quot;.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
</feed>
