<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shoban Chiddarth</title>
    <description>The latest articles on DEV Community by Shoban Chiddarth (@shobanchiddarth).</description>
    <link>https://dev.to/shobanchiddarth</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3764440%2Fd3ec9df1-b9a8-4982-bce2-0a6e52ec602f.jpg</url>
      <title>DEV Community: Shoban Chiddarth</title>
      <link>https://dev.to/shobanchiddarth</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shobanchiddarth"/>
    <language>en</language>
    <item>
      <title>On Age Verification</title>
      <dc:creator>Shoban Chiddarth</dc:creator>
      <pubDate>Wed, 27 May 2026 02:09:33 +0000</pubDate>
      <link>https://dev.to/shobanchiddarth/on-age-verification-3bn2</link>
      <guid>https://dev.to/shobanchiddarth/on-age-verification-3bn2</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you have been living under a rock (good for you), several states in America and the country of Brazil have introduced a new law that forces operating system developers to implement "age attestation" of the user, you will have to provide your age and then an age bracket will be stored which will be broadcast to every app and service you use in your operating system, and app stores (apt sources) are required to acknowledge this and filter software based on age, and assume lowest age bracket if the age is not given.&lt;/p&gt;

&lt;p&gt;Age verification tracker: &lt;a href="https://github.com/BryanLunduke/DoesItAgeVerify/" rel="noopener noreferrer"&gt;https://github.com/BryanLunduke/DoesItAgeVerify/&lt;/a&gt; (fork that maintains links to original developer statements: &lt;a href="https://github.com/softcookiepp/DoesItAgeVerify" rel="noopener noreferrer"&gt;https://github.com/softcookiepp/DoesItAgeVerify&lt;/a&gt;). This has all information regarding it.&lt;/p&gt;

&lt;p&gt;In this blog post, I will share my opinions on it and explain why it is impossible to enforce these laws as well as impossible to comply with these laws. It is actually illegal to follow the US law according to the US law, I will explain below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Facebook
&lt;/h2&gt;

&lt;p&gt;Meta is behind this. Meta is pulling the strings and getting the law makers to pass such laws. Here is the proof:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://web.archive.org/web/20260313090844/https://www.reddit.com/r/linux/comments/1rshc1f/i_traced_2_billion_in_nonprofit_grants_and_45/" rel="noopener noreferrer"&gt;https://web.archive.org/web/20260313090844/https://www.reddit.com/r/linux/comments/1rshc1f/i_traced_2_billion_in_nonprofit_grants_and_45/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.archive.org/web/20260314074025/https://www.reddit.com/r/linux/comments/1rtd51g/update_i_pulled_irs_filings_for_the_org_that/" rel="noopener noreferrer"&gt;https://web.archive.org/web/20260314074025/https://www.reddit.com/r/linux/comments/1rtd51g/update_i_pulled_irs_filings_for_the_org_that/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=4KyiLyOxux8" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=4KyiLyOxux8&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since Mark Zuckerberg is the mastermind behind all these, and due to how &lt;a href="https://www.thestreet.com/technology/zuckerberg-old-remark-about-facebook-users" rel="noopener noreferrer"&gt;trustworthy&lt;/a&gt; he is&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp4qt99cq22isybzb87n5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp4qt99cq22isybzb87n5.png" alt="tweet-screenshot-of-zuck-calling-people-dumbfucks" width="600" height="728"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;it is logical to expect the very next step from the lawmakers is to make you upload your government issued ID photo to the operating system before setting up the account. So that every action you take in the computer, every local text file you save, every message you send in end to end encrypted messaging apps, every local file you save, every email you send by gpg encrypting it, every social media post you make from an anonymous account, can be pinpointed to who exactly you are and your location, so that the government can keep a social credit score for people and arrest anyone that disagrees with the government and enforce a totalitarian regime like 1984 or present day China. &lt;/p&gt;

&lt;p&gt;This is their plan. It is a slippery slope. It is Facebook after all. Now they push for "age attestation" which is "just a minor thing" according to some people and after it is accepted as a "usual thing" they push for a little more like precise age numbers and then you end up with ID. &lt;/p&gt;

&lt;h2&gt;
  
  
  Linux
&lt;/h2&gt;

&lt;p&gt;The law keeps definitions of "Operating System", "User", "Account" as vague as possible to enforce it into as many places as possible. And anyone with a functioning mind would think the entire Linux community, built around privacy and security, would be vehemently opposed to this law and refuse to follow it and try as much as possible to get this law deleted. But that is where you are wrong. At least most of the people, the users, are against the law.&lt;/p&gt;

&lt;p&gt;Ubuntu will comply and Debian will help downstream distros implement it (it is in the tracker). And Arch Linux, an OS literally made to give you full control over your computer, says &lt;a href="https://www.youtube.com/watch?v=wR-zJKdAkOc&amp;amp;t=363s&amp;amp;pp=ygUsbHVuZHVrZSBhcmNoIGxpbnV4IG9wcG9zaW5nIGFnZSB2ZXJpZmljYXRpb24%3D" rel="noopener noreferrer"&gt;opposing age verification is violation of code of conduct&lt;/a&gt;. There is more on Lunduke's (the only journalist covering these kinds of tech news you wouldn't find on mainstream media) channel. And also &lt;code&gt;systemd&lt;/code&gt;, an init process that is supposed to start the system, has a module called &lt;code&gt;userdb&lt;/code&gt; and it previously stored a lot of PII like name and email and now it stores user birthdate to comply with the law so that xdg-desktop-portal can use it as a reference to implement the age attestation (&lt;a href="https://github.com/systemd/systemd/pull/40954?ref=itsfoss.com" rel="noopener noreferrer"&gt;PR link&lt;/a&gt;). And also &lt;a href="https://www.youtube.com/watch?v=M3erhbwqIAM&amp;amp;pp=ygUlbGludXggcmVkZGl0IGNlbnNvcnMgYWdlIHZlcmlmaWNhdGlvbg%3D%3D" rel="noopener noreferrer"&gt;linux reddit censors age verification related posts&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  systemd
&lt;/h3&gt;

&lt;p&gt;When I started out with Linux I did not understand the hate around systemd. I thought it was just a normal piece of software with unnecessary drama and also it worked kinda fine like it took care of things and was programmable so I didn't understand why people did not like it and moved away from it. But now I do. It is owned by RedHat and is extremely bloated and does things it is not required to do for no reason at all. I hated it more and more as I was going deeper into computers and Linux. It has a local DNS middleman server for god knows why the system needs another middleman when I run my own DNS server. I &lt;a href="https://www.linkedin.com/posts/shobanchiddarth_linux-mint-is-not-affected-by-systemds-userdb-activity-7445124215424335873-k4lf" rel="noopener noreferrer"&gt;had to configure a lot of things&lt;/a&gt; to make sure the &lt;code&gt;systemd-resolved&lt;/code&gt; stays shut down. An init process storing user PII, and modifying it to comply with privacy invasive freedom restricting laws, and resolving DNS, managing IP addresses of network interfaces (&lt;code&gt;systemd-networkd&lt;/code&gt;) is too much for an init process, it increases the attack surface and adds too much unnecessary bloat to the system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Distro hopping
&lt;/h3&gt;

&lt;p&gt;The only developers who oppose this law are listed in the tracker I shared above. I personally use Linux Mint and Mint is currently not affected by systemd storing user birthdate because Mint does not ship with &lt;code&gt;systemd-userdbd&lt;/code&gt; (&lt;a href="https://github.com/BryanLunduke/DoesItAgeVerify/issues/25" rel="noopener noreferrer"&gt;Source&lt;/a&gt;) so even if upstream software like xdg-desktop-portal implement it, it will have no point of reference for the user birthdate. However, that is only true for the current state of the OS (22.3 zena). The developers of Mint have not made any public statements regarding age verification and assuming the worst, if they implement age verification by shipping the next update with &lt;code&gt;systemd-userdbd&lt;/code&gt; or in some other way, I can simply refuse the dist upgrade. Also I am currently refusing all updates to systemd related packages using this command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-mark hold &lt;span class="k"&gt;*&lt;/span&gt;systemd&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the dist upgrades have been refused and systemd related packages have been held back from updating, meaning I have a lot of time to switch to an operating system that publicly opposes age verification as well as does not have systemd. I am choosing to go with Artix (Arch without systemd) for my laptop as it gives full control over my computer, and Devuan (Debian without systemd) for &lt;a href="https://dev.to/shobanchiddarth/setting-up-pi-hole-as-a-custom-dns-server-on-my-home-lab-4jd7"&gt;my server&lt;/a&gt; for stability. If you are using a Linux distro that is going to implement age verification like Ubuntu or Fedora consult the above tracker to make a decision on the OS that fits your needs and opposes age verification and does not have systemd and switch to it as quick as possible. If you are using Windows switch to Linux, it is a corporate controlled proprietary OS and they already do a lot of spying on you and also Zuckerberg, Bill Gates are all a part of the Epstein class and they are all in this together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is it impossible to enforce this law?
&lt;/h2&gt;

&lt;p&gt;Obviously, Linux is open source and if the OS adds age verification, someone will just fork it and create a "libre" version that doesn't have it. And good luck to government jarheads trying to arrest everyone, you won't have enough room in prison.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is it impossible to comply with this law?
&lt;/h2&gt;

&lt;p&gt;Even if every single person in America is willing to comply with this law, it is literally impossible. The law is very very vague about the definitions of an Operating System, an Account, and a User. So almost everything that is technically a "computer" falls into this law. And almost everything that is a computer (that probably runs linux) includes your laptop and desktop and servers (obviously), your smart TV, your smart watch, your calculator (&lt;a href="https://github.com/c3d/db48x/blob/stable/LEGAL-NOTICE.md" rel="noopener noreferrer"&gt;probably&lt;/a&gt;), your "smart" washing machine, fridge, dishwasher and other smart devices, your robot vacuum cleaner, your router, raspberry pis that are part of iot devices, any and all forms of iot devices, your CCTV cameras, your baby and dog monitor, believe it or not switches (the layer 2 network devices) are also technically a computer because they have an OS (Cisco IOS), Nintendo Switch console, the gas station pump, your car probably, the bar code scanner at Walmart, traffic signals, public surveillance cameras, your smoke detector, and a lot of things, the list is so long that it cannot be fit in this blog post.&lt;/p&gt;

&lt;p&gt;If everyone in America wanted to comply with the law by entering their age into all "computers" they own, the entire internet will break, several people will die, and a lot of things will be broken. So many things in this world are computers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Right to Repair
&lt;/h3&gt;

&lt;p&gt;Also it is illegal to comply with this law. &lt;a href="https://www.eff.org/pages/unintended-consequences-fifteen-years-under-dmca" rel="noopener noreferrer"&gt;DMCA Law &lt;/a&gt;in the US states that you are not allowed to use technical skills to circumvent DRM on a device (you are legally not allowed to modify a device you own and paid money for) or to "hack" it to run whatever software you want. You could face 5 years in jail or $500,000 fine for changing the electric circuits of a physical device you purchased with money and own in your hand. &lt;/p&gt;

&lt;p&gt;Nintendo used this law to sue people breaking into Nintendo Switch (the gaming console) to run custom software. BMW implemented subscription based heated seat, using electricity from the battery of the car that you paid for, generated by the fuel you paid for, which costs the company $0 to produce some heat yet they charge users monthly for it. And it is backed by the law, if you circumvent it to have unlimited heated seat then you go to jail. And Tesla implemented a pay walled battery, you have to pay money to use 100% of the battery that you own. BMW is a horrible company by the way they patented BMW screws that can only be purchased from their vendors to make sure you always come to them for repairs and don't repair on your own, that is a completely different story but America is slowly becoming this corpo owned hell hole. &lt;br&gt;
Sources:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.theverge.com/2024/3/4/24090357/nintendo-yuzu-emulator-lawsuit-settlement" rel="noopener noreferrer"&gt;https://www.theverge.com/2024/3/4/24090357/nintendo-yuzu-emulator-lawsuit-settlement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.notebookcheck.net/Nintendo-lawsuit-ends-in-2-million-settlement-against-Mig-Switch-seller-accused-of-aiding-piracy.1107589.0.html" rel="noopener noreferrer"&gt;https://www.notebookcheck.net/Nintendo-lawsuit-ends-in-2-million-settlement-against-Mig-Switch-seller-accused-of-aiding-piracy.1107589.0.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arstechnica.com/gaming/2025/05/nintendo-threatens-to-brick-switch-consoles-for-hacking-piracy/" rel="noopener noreferrer"&gt;https://arstechnica.com/gaming/2025/05/nintendo-threatens-to-brick-switch-consoles-for-hacking-piracy/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://insideevs.com/news/601330/tesla-backtracks-on-asking-4500-usd-unlock-model-s-range-after-web-outrage/" rel="noopener noreferrer"&gt;https://insideevs.com/news/601330/tesla-backtracks-on-asking-4500-usd-unlock-model-s-range-after-web-outrage/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://electrek.co/2023/08/15/teslas-new-model-s-x-same-battery-pack-but-with-software-locked-capacity/" rel="noopener noreferrer"&gt;https://electrek.co/2023/08/15/teslas-new-model-s-x-same-battery-pack-but-with-software-locked-capacity/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.techspot.com/news/111208-bmw-admits-heated-seat-subscriptions-mistake-but-commits.html" rel="noopener noreferrer"&gt;https://www.techspot.com/news/111208-bmw-admits-heated-seat-subscriptions-mistake-but-commits.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.repairerdrivennews.com/2025/12/30/bmw-files-patent-for-screw-that-uses-emblem-as-the-drive-structure/" rel="noopener noreferrer"&gt;https://www.repairerdrivennews.com/2025/12/30/bmw-files-patent-for-screw-that-uses-emblem-as-the-drive-structure/&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Coming back to what I was saying, it is illegal to open up your robot vacuum cleaner and flash a custom operating system into it, in the place where the current linux distro of it is running, by somehow wiring it up with a keyboard mouse and display. So you are not allowed to do it or you face prison. But the age verification law says you are required to enter your age into all "computers" you own and the only way to enter it into your robot vacuum cleaner is to circumvent its DRM by breaking it open, which is illegal. (this applies to not just robot vacuum cleaners).&lt;/p&gt;

&lt;p&gt;America has finally made it. It is illegal to follow the law. Welcome to land of the free baby, where you are legally not allowed to modify devices you paid money to own and cannot have anonymity and privacy.&lt;/p&gt;

&lt;p&gt;Louis Rossmann (the man behind the clippy movement) has been shouting about the "Right to Repair" for a very long time. Please pay attention to that one: &lt;a href="https://www.youtube.com/@rossmanngroup/videos" rel="noopener noreferrer"&gt;https://www.youtube.com/@rossmanngroup/videos&lt;/a&gt; and &lt;a href="https://consumerrights.wiki/w/Main_Page" rel="noopener noreferrer"&gt;https://consumerrights.wiki/w/Main_Page&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud is up there above us
&lt;/h3&gt;

&lt;p&gt;And almost nobody on the internet has covered the cloud side of things related to age verification.&lt;/p&gt;

&lt;p&gt;Docker containers are also technically a computer since it is an OS (alpine mostly). Are people required to enter their age inside docker containers too? Are you only required to enter your age when pushing a docker container to the internet or whenever creating it like dev or testing? What about Virtual Machines? Amazon Web Services offer an OS called "Amazon Linux" so they are also operating system developers. So by law they are required to implement age attestation in their OS, I am not sure what they are gonna do about it. &lt;/p&gt;

&lt;p&gt;AWS is the biggest cloud provider where majority of production infrastructure lives. Since the law requires every "user" that has an "account" on every "computer" to enter their age, will Jeff Bezos be required to enter his age into every single EC2 instance created in American regions?&lt;/p&gt;

&lt;p&gt;Even if he is willing to, let's assume it takes Jeff Bezos 3 seconds to enter his age into 1 EC2 instance (type 2 numbers + enter key + window switching). So that is &lt;code&gt;86,400/3=28,800&lt;/code&gt;. He will be entering his age into 28,800 EC2 instances per day which is a frighteningly low amount of EC2 instances compared to how much actually exist and is being created every day, assuming he is willing to work 24 hours.&lt;/p&gt;

&lt;p&gt;Most of the real production infra does not have EC2 instances manually created and provisioned from the console, they are all automated using tools like Terraform. And 28,800 EC2 instances does not count the several internal "technically a computer" servers that are used for things like lambda, ECS, Fargate, EKS and there is still a ton more including other cloud providers, on prem data centers many companies use, and also the on prem network devices (like switches and routers I mentioned above). Now you see why the entire internet will break, it is impossible for the owner of the "computer" to enter their age into every "computer" they own due to the laws of physics.&lt;/p&gt;

&lt;p&gt;The law is extremely vague about the definitions either because&lt;a href="https://youtu.be/aJllQ9d3pYM?t=2028" rel="noopener noreferrer"&gt; lawmakers are stupid and think cloud means where we get the rain from&lt;/a&gt; or they are evil and want to strip away as much people's rights as possible and I don't know which is worse or it could be both. &lt;/p&gt;

&lt;h2&gt;
  
  
  Why should someone in India (me, and you, even if you are not in India) care about some American law?
&lt;/h2&gt;

&lt;p&gt;Brazil is a country that is not America yet Facebook pulled strings and got the law passed over there. India is next. Indian government isn't exactly pro consumer rights or pro privacy, law requires VPN services to store logs and share them with the government, that is why ProtonVPN moved away from India. And there were talks about the government banning entire Proton suite from India because some idiot sent a fake bomb threat somewhere using proton email address and they were not able to find him. Call me paranoid but I think the someone is the government making up reasons to ban proton mail.&lt;/p&gt;

&lt;p&gt;And even if the law hypothetically never comes to India, if I am using Ubuntu (thankfully I am not) and Ubuntu pushes an update on the day the law takes effect forcing me to enter my age or else lock my drive, I will have no choice but to either comply with the law (which is bad and is a slippery slope like I said above) or to mount the hard drive, somewhere else, copy the files, and reinstall another OS that doesn't have age verification which will be tedious to do after the law has been implemented, hopping to another distro now is easy. This is the same as disaster recovery in Cloud Computing, we don't wait until disaster happens to recover resources, we plan in advance and provision multi AZ or multi region so if disaster affects on AZ or an entire country we would still have other areas up and running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Here's what you are gonna do
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1zjojhvq4exezyeum1k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi1zjojhvq4exezyeum1k.jpg" alt="kid-named-finger" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get as many normies to switch to Linux&lt;/li&gt;
&lt;li&gt;Tell everyone you know about age verification&lt;/li&gt;
&lt;li&gt;Increase awareness about the importance of privacy, anonymity, security, and right to repair&lt;/li&gt;
&lt;li&gt;If someone already uses Linux, get them to switch to an OS that doesn't support age verification (see tracker)&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>linux</category>
      <category>privacy</category>
      <category>freedom</category>
      <category>security</category>
    </item>
    <item>
      <title>Documenting my Physical Home lab in Packet Tracer</title>
      <dc:creator>Shoban Chiddarth</dc:creator>
      <pubDate>Tue, 05 May 2026 17:09:00 +0000</pubDate>
      <link>https://dev.to/shobanchiddarth/documenting-my-physical-home-lab-in-packet-tracer-n2a</link>
      <guid>https://dev.to/shobanchiddarth/documenting-my-physical-home-lab-in-packet-tracer-n2a</guid>
      <description>&lt;p&gt;&lt;strong&gt;Repo link:&lt;/strong&gt; &lt;a href="https://github.com/ShobanChiddarth/physical-homelab-in-packet-tracer" rel="noopener noreferrer"&gt;github.com/ShobanChiddarth/physical-homelab-in-packet-tracer&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I recently finished my 3rd year end sem exams and I am in my summer holidays. I am preparing for the  CCNA certification so I decided to recreate my physical home lab in Cisco Packet Tracer.&lt;/p&gt;

&lt;p&gt;To download the packet tracer lab file you can visit the GitHub repo linked in the top of this post and look at the README.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;I used a WRT300N router to simulate the Tenda AC6 router, and a 1941 (non wireless) router to simulate the Jio Fiber router. I did not need to connect any wireless devices to the Jio router in this packet tracer lab and also in Packet tracer connecting the WAN port (Internet port) of a wireless router to the LAN port of another wireless router wouldn't work like in real life, it refuses to form a connection. So I went with static IPs for the Jio Router's LAN port and Tenda Router's WAN port.&lt;/p&gt;

&lt;h3&gt;
  
  
  Devices inside Tenda router's LAN
&lt;/h3&gt;

&lt;p&gt;As shown in the previous entries of this series, there is a ThinkCenter M81 (represented by Server PT) connected through ethernet to the Tenda router, in which HTTP and DNS services have been enabled (Pi-hole). And my laptop and my smartphone are connected over Wi-Fi to the Tenda router.&lt;/p&gt;

&lt;h3&gt;
  
  
  Devices outside Tenda router's LAN (inside Jio router's LAN)
&lt;/h3&gt;

&lt;p&gt;We will not be worrying about this part as I do not control the Jio router and guests and other family members will be using it. There are mobile phones and a TV connected to it. There was a smart vaccum cleaner connected to it long ago like in the same Jio LAN without any isolation and it stopped working but if I was in charge of managing the network I would have set up proper isolation for IOT devices like this one by putting them on a separate VLAN, setting up guest network and blocking them from communicating with other devices using ACLs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Internet Access
&lt;/h3&gt;

&lt;p&gt;Real internet is not possible inside Cisco Packet tracer so assume the cloud up above is the real internet.&lt;/p&gt;

&lt;h3&gt;
  
  
  IP Configuration
&lt;/h3&gt;

&lt;p&gt;IP configuration is the exact same as my lab in real life&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jio LAN = &lt;code&gt;192.168.29.1/24&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tenda WAN = &lt;code&gt;DHCP&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tenda LAN = &lt;code&gt;192.168.1.1/24&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ThinkCenter M81 (Pi-hole) = &lt;code&gt;192.168.1.2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;My Laptop = &lt;code&gt;DHCP&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;My Phone = &lt;code&gt;DHCP&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Full Lab
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwd0ahw5gyf7ape3352jf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwd0ahw5gyf7ape3352jf.png" alt="001-full-lab" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pi-hole DNS records
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr2tm3xveunhjgw8aevvx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr2tm3xveunhjgw8aevvx.png" alt="002-dns-records" width="800" height="681"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pinging ThinkCenter M81 from My Laptop
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6qkxjro2slfc3sk9e6l7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6qkxjro2slfc3sk9e6l7.png" alt="003-pinging-server" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Accessing Pi-hole dashboard from My Laptop
&lt;/h3&gt;

&lt;p&gt;I edited the default index.html in HTTP server a little bit, this is not an actual computer so installing Pi-hole is not possible. The DNS resolution works that is what is important.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9pe015g23qy2apmq0np.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9pe015g23qy2apmq0np.png" alt="004-pi-hole-from-laptop" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Accessing Pi-hole dashboard from My Phone
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9yjows69s6bhkkwo86wj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9yjows69s6bhkkwo86wj.png" alt="005-pi-hole-from-phone" width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Therefore I successfully created a simulation of my real home lab inside Packet Tracer. I am currently preparing for CCNA certification and this packet tracer lab has helped me map a network whose behaviour I already know onto Packet Tracer's environment. When you build a network from scratch in a simulator, you have no reference point, but when you recreate something you physically own, you immediately notice where the simulator behaves differently from reality and understand its limitations, and that gap is where the actual learning happens.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>cisco</category>
      <category>network</category>
      <category>homelab</category>
    </item>
    <item>
      <title>AWS Terraform infrastructure for a FastAPI+PostgreSQL backend</title>
      <dc:creator>Shoban Chiddarth</dc:creator>
      <pubDate>Mon, 23 Mar 2026 14:47:31 +0000</pubDate>
      <link>https://dev.to/shobanchiddarth/aws-terraform-infrastructure-for-a-fastapipostgresql-backend-3dip</link>
      <guid>https://dev.to/shobanchiddarth/aws-terraform-infrastructure-for-a-fastapipostgresql-backend-3dip</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This blog post showcases the AWS infrastructure written in Terraform for the backend of &lt;code&gt;alumni-connect&lt;/code&gt;, a MVP of a college project written in FastAPI with a PostgreSQL database.&lt;/p&gt;

&lt;p&gt;The terraform repo: &lt;a href="https://github.com/ShobanChiddarth/alumni-connect-terraform" rel="noopener noreferrer"&gt;https://github.com/ShobanChiddarth/alumni-connect-terraform&lt;/a&gt;&lt;br&gt;
The backend repo: &lt;a href="https://github.com/ShobanChiddarth/alumni-connect-backend" rel="noopener noreferrer"&gt;https://github.com/ShobanChiddarth/alumni-connect-backend&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feqaopfzeftq2snnl14r3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feqaopfzeftq2snnl14r3.png" alt="architecture" width="800" height="645"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  VPC Design
&lt;/h3&gt;

&lt;p&gt;Since this is an MVP, I went with the absolute minimal for everything. 1 VPC exists on &lt;code&gt;ap-south-1&lt;/code&gt;. It has an internet gateway. Frontend is hosted in netlify, so API requests as well as management traffic (SSH) will enter through the internet gateway.&lt;/p&gt;
&lt;h3&gt;
  
  
  Subnet Design
&lt;/h3&gt;

&lt;p&gt;There are 4 subnets in total.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A public subnet to handle management traffic (&lt;code&gt;ap-south-1a&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;2 private subnets for backend and database EC2 instances (&lt;code&gt;ap-south-1a&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Another public subnet to satisfy AWS's hard requirement of multi AZ for load balancers (&lt;code&gt;ap-south-1b&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The management subnet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contains the bastion EC2&lt;/li&gt;
&lt;li&gt;Contains a NAT gateway for EC2 instances in the private subnet to reach the internet&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Load Balancer
&lt;/h3&gt;

&lt;p&gt;The backend EC2 has no public IP and sits in a private subnet. Its security group only accepts port 8000 from the ALB security group - there is no direct path from the internet to the backend. The ALB is the only entry point.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;listens on port 80 for HTTP traffic from the public internet&lt;/li&gt;
&lt;li&gt;spans across &lt;code&gt;ap-south-1a&lt;/code&gt; (management subnet) and &lt;code&gt;ap-south-1b&lt;/code&gt; (empty subnet) - AWS requires an internet-facing ALB to span at least 2 AZs&lt;/li&gt;
&lt;li&gt;forwards the traffic from the internet to port 8000 in the backend EC2&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Security Groups Design
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;SSH traffic from the internet is allowed only to the bastion EC2&lt;/li&gt;
&lt;li&gt;SSH traffic to the backend and database EC2s are allowed only from the bastion EC2&lt;/li&gt;
&lt;li&gt;HTTP traffic from the internet is allowed only to the load balancer&lt;/li&gt;
&lt;li&gt;HTTP traffic to the backend is allowed only from the load balancer&lt;/li&gt;
&lt;li&gt;Database traffic (port 5432) to the database EC2 is allowed only from the backend EC2&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  SSH Keys
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bastion EC2's SSH private key stays on the local computer of the person deploying this&lt;/li&gt;
&lt;li&gt;Other EC2s SSH private key stays in the bastion&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  EC2 instances
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Backend EC2 is set to pull and run docker image &lt;code&gt;shobanchiddarth/alumni-connect-backend:0.0.2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Database EC2 is set to pull and run docker image &lt;code&gt;postgres:16&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  NAT Gateway Cost Problem
&lt;/h2&gt;

&lt;p&gt;I wrote a LinkedIn post about this. &lt;a href="https://www.linkedin.com/posts/shobanchiddarth_terraform-aws-vpc-activity-7441719825703366656-8_15" rel="noopener noreferrer"&gt;Click here to view it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you are not able to see it, here are the contents of the post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;I recently faced a problem with making EC2 instances communicate with the internet &lt;span class="k"&gt;while &lt;/span&gt;deploying a FastAPI+Postgres backend architecture on AWS using Terraform. NAT Gateways exist to solve this - a NAT gateway can be placed &lt;span class="k"&gt;in &lt;/span&gt;a public subnet and traffic can be routed through it.

But NAT Gateways are costly. ~&lt;span class="nv"&gt;$0&lt;/span&gt;.045/hr just to exist, plus data transfer charges.

The solution I used: keep the NAT gateway &lt;span class="k"&gt;in &lt;/span&gt;the Terraform code, but destroy it immediately after deployment. Private instances lose internet access after packages are updated, Docker images are pulled, and containers are running.

terraform destroy &lt;span class="nt"&gt;-target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aws_nat_gateway.alumni-nat-gw &lt;span class="nt"&gt;-target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aws_eip.nat-gateway-elastic-ip

I also tried a NAT instance - an EC2 that acts as a router - but ran into configuration issues getting it to actually forward traffic. Ended up not going down that path &lt;span class="k"&gt;for &lt;/span&gt;this project.

This temporary NAT gateway approach is good enough &lt;span class="k"&gt;for &lt;/span&gt;a college project MVP with almost no &lt;span class="nb"&gt;users &lt;/span&gt;that gets destroyed &lt;span class="k"&gt;in &lt;/span&gt;a few hours anyway.

Has anyone dealt with this &lt;span class="k"&gt;in &lt;/span&gt;production? Is there a cleaner pattern &lt;span class="k"&gt;for &lt;/span&gt;private subnet internet access beyond NAT gateway or NAT instance?

&lt;span class="c"&gt;#Terraform #AWS #VPC #CloudComputing #IaC #CloudCost&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Steps to Deploy
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Clone the repo&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ShobanChiddarth/alumni-connect-terraform
&lt;span class="nb"&gt;cd &lt;/span&gt;alumni-connect-terraform/infrastructure
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set AWS credentials&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set the database password&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TF_VAR_db_password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Deploy&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Destroy the NAT gateway after apply&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform destroy &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aws_nat_gateway.alumni-nat-gw &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aws_eip.nat-gateway-elastic-ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To tear down everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Proof of Deployment
&lt;/h2&gt;

&lt;p&gt;These are screenshots I took when it was live. I then destroyed the whole infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnndb82s8biv5na3ptr8q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnndb82s8biv5na3ptr8q.png" alt="working-001" width="800" height="166"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frza18lpz5whs0lvpp6j7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frza18lpz5whs0lvpp6j7.png" alt="working-002" width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5tpcouffa30zv5ncpdvl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5tpcouffa30zv5ncpdvl.png" alt="working-003" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbhdauhnjnr5ro7hwp81.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbhdauhnjnr5ro7hwp81.png" alt="working-004" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I archived the API landing URL (first image) in Wayback Machine. Here is the link: &lt;a href="https://web.archive.org/web/20260322130840/http://alumni-alb-backend-83642402.ap-south-1.elb.amazonaws.com/" rel="noopener noreferrer"&gt;https://web.archive.org/web/20260322130840/http://alumni-alb-backend-83642402.ap-south-1.elb.amazonaws.com/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This is the full Terraform infrastructure for the &lt;code&gt;alumni-connect&lt;/code&gt; backend.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>cloud</category>
      <category>cloudcomputing</category>
    </item>
    <item>
      <title>IAM Development Lab in Keycloak</title>
      <dc:creator>Shoban Chiddarth</dc:creator>
      <pubDate>Fri, 20 Mar 2026 17:17:13 +0000</pubDate>
      <link>https://dev.to/shobanchiddarth/iam-development-lab-in-keycloak-19i7</link>
      <guid>https://dev.to/shobanchiddarth/iam-development-lab-in-keycloak-19i7</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is the ultimate IAM development and employee lifecycle management lab I am doing where I will be making use of the skills I learnt from the &lt;a href="https://www.theforage.com/completion-certificates/ifobHAoMjQs9s6bKS/gmf3ypEXBj2wvfQWC_ifobHAoMjQs9s6bKS_69a0eaeeb1f8e4b8685709b8_1772548764230_completion_certificate.pdf" rel="noopener noreferrer"&gt;TATA virtual internship via Forage: Cybersecurity Analyst - IAM Developer&lt;/a&gt; to develop and implement an IAM solution for a fictional organization.&lt;/p&gt;

&lt;p&gt;I chose Keycloak for this lab instead of SailPoint with Oracle Identity Manager because Keycloak is fully open source. This is a standalone lab independent of OpenLDAP.&lt;/p&gt;

&lt;p&gt;The internship covered&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IAM fundamentals&lt;/li&gt;
&lt;li&gt;Digital identity&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Authorization&lt;/li&gt;
&lt;li&gt;SSO&lt;/li&gt;
&lt;li&gt;least privilege principle
And then walked through designing and planning a full IAM implementation for a fictional enterprise called TechCorp (but the fictional org we will be representing is called Acme). The proposed solution used &lt;/li&gt;
&lt;li&gt;SailPoint for automated user lifecycle management and&lt;/li&gt;
&lt;li&gt;Oracle Identity Manager for RBAC
with a four-phase implementation plan covering deployment, testing, training, and ongoing monitoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This lab takes those same requirements and implements them end-to-end:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A structured org with departments and roles&lt;/li&gt;
&lt;li&gt;RBAC enforced at the role level&lt;/li&gt;
&lt;li&gt;Full user lifecycle management from provisioning to offboarding&lt;/li&gt;
&lt;li&gt;MFA&lt;/li&gt;
&lt;li&gt;SSO via OIDC&lt;/li&gt;
&lt;li&gt;Email-based verification flows&lt;/li&gt;
&lt;li&gt;Brute force protection&lt;/li&gt;
&lt;li&gt;Audit logging 
All running on-premises in VirtualBox with no external dependencies.
## Pre-Requisites&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/shobanchiddarth/the-superior-way-to-make-vms-communicate-with-each-other-as-well-as-host-with-internet-access-42m1"&gt;Superior VM Intercommunication setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shobanchiddarth/setting-up-pi-hole-as-a-custom-dns-server-on-my-home-lab-4jd7"&gt;Pi-Hole&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shobanchiddarth/setting-up-ssl-https-on-my-home-lab-g45"&gt;Mkcert Local CA TLS (for HTTPS)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Build Log
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Keycloak Server Initialization
&lt;/h3&gt;

&lt;p&gt;I cloned a debian server VM, put it in the current VirtualBox host only network I have, assigned a static IP (&lt;code&gt;192.168.57.8&lt;/code&gt;), Then I edited its hostname, and mapped DNS record &lt;code&gt;keycloak.acme.internal -&amp;gt; 192.168.57.8&lt;/code&gt; in Pi-Hole.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@keycloak:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ifconfig enp0s3
&lt;span class="gp"&gt;enp0s3: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;mtu 1500
&lt;span class="go"&gt;        inet 192.168.57.8  netmask 255.255.255.0  broadcast 192.168.57.255
&lt;/span&gt;&lt;span class="gp"&gt;        inet6 fe80::43b5:ca52:a96d:134a  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;        ether 08:00:27:8a:4a:29  txqueuelen 1000  (Ethernet)
        RX packets 28569  bytes 3772704 (3.5 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 28550  bytes 2712767 (2.5 MiB)
        TX errors 0  dropped 2 overruns 0  carrier 0  collisions 0

&lt;/span&gt;&lt;span class="gp"&gt;debian@keycloak:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nslookup keycloak.acme.internal
&lt;span class="go"&gt;Server:     192.168.57.3
&lt;/span&gt;&lt;span class="gp"&gt;Address:    192.168.57.3#&lt;/span&gt;53
&lt;span class="go"&gt;
Name:   keycloak.acme.internal
Address: 192.168.57.8

&lt;/span&gt;&lt;span class="gp"&gt;debian@keycloak:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I installed java on it because Keycloak requires it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;default-jre &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Java version 21 btw&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@keycloak:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;java &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;openjdk 21.0.10 2026-01-20
OpenJDK Runtime Environment (build 21.0.10+7-Debian-1deb13u1)
OpenJDK 64-Bit Server VM (build 21.0.10+7-Debian-1deb13u1, mixed mode, sharing)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I downloaded Keycloak from GitHub, extracted it and put it in &lt;code&gt;/opt&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@keycloak:~/Downloads$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="go"&gt;keycloak-26.5.6.tar.gz
&lt;/span&gt;&lt;span class="gp"&gt;debian@keycloak:~/Downloads$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; keycloak-26.5.6.tar.gz 
&lt;span class="gp"&gt;debian@keycloak:~/Downloads$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="go"&gt;keycloak-26.5.6  keycloak-26.5.6.tar.gz
&lt;/span&gt;&lt;span class="gp"&gt;debian@keycloak:~/Downloads$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo mv &lt;/span&gt;keycloak-26.5.6 /opt/keycloak
&lt;span class="gp"&gt;debian@keycloak:~/Downloads$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt
&lt;span class="gp"&gt;debian@keycloak:/opt$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="go"&gt;keycloak
&lt;/span&gt;&lt;span class="gp"&gt;debian@keycloak:/opt$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And created a new user and set appropriate permissions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;useradd &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /sbin/nologin keycloak
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; keycloak:keycloak /opt/keycloak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Local CA certificates for Keycloak
&lt;/h3&gt;

&lt;p&gt;I did the usual, created certs for &lt;code&gt;keycloak.acme.internal&lt;/code&gt; and moved them to the Keycloak server and pointed Keycloak to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="go"&gt;keycloak.acme.internal-key.pem  keycloak.acme.internal.pem
&lt;/span&gt;&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt/keycloak/conf/certs
&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo cp &lt;/span&gt;keycloak.acme.internal.pem /opt/keycloak/conf/certs/keycloak.crt
&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo cp &lt;/span&gt;keycloak.acme.internal-key.pem /opt/keycloak/conf/certs/keycloak.key
&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; keycloak:keycloak /opt/keycloak/conf/certs
&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;640 /opt/keycloak/conf/certs/keycloak.key
&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /opt/keycloak/conf/keycloak.conf 
&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /opt/keycloak/conf/
&lt;span class="go"&gt;cache-ispn.xml  certs/          keycloak.conf   README.md       truststores/    
&lt;/span&gt;&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /opt/keycloak/conf/keycloak.conf | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;span class="go"&gt;hostname=keycloak.acme.internal
https-certificate-file=/opt/keycloak/conf/certs/keycloak.crt
https-certificate-key-file=/opt/keycloak/conf/certs/keycloak.key
http-enabled=false

&lt;/span&gt;&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keycloak initial build
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; keycloak &lt;span class="nv"&gt;KEYCLOAK_ADMIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin &lt;span class="nv"&gt;KEYCLOAK_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin123 /opt/keycloak/bin/kc.sh build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; keycloak &lt;span class="nv"&gt;KEYCLOAK_ADMIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin &lt;span class="nv"&gt;KEYCLOAK_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;password /opt/keycloak/bin/kc.sh build
&lt;span class="go"&gt;WARNING: Usage of the default value for the db option in the production profile is deprecated. Please explicitly set the db instead.
INFO: The following run time options were found, but will be ignored during build time: kc.https-certificate-key-file, kc.http-enabled, kc.hostname, kc.https-certificate-file

Updating the configuration and installing your custom providers, if any. Please wait.
2026-03-19 20:00:27,827 INFO  [io.quarkus.deployment.QuarkusAugmentor] (main) Quarkus augmentation completed in 10529ms
Server configuration updated and persisted. Run the following command to review the configuration:

    kc.sh show-config

&lt;/span&gt;&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/opt/keycloak/bin/kc.sh show-config
&lt;span class="go"&gt;Current Mode: production
Current Configuration:
    kc.log-level-org.infinispan.transaction.lookup.JBossStandaloneJTAManagerLookup =  null (Derived)
    kc.log-level-io.quarkus.config =  null (Derived)
    kc.hostname =  keycloak.acme.internal (keycloak.conf)
    kc.log-console-output =  default (classpath application.properties)
    kc.log-level-io.quarkus.hibernate.orm.deployment.HibernateOrmProcessor =  null (Derived)
    kc.log-level-liquibase.database.core.PostgresDatabase =  null (Derived)
    kc.optimized =  true (Persisted)
    kc.version =  26.5.6 (SysPropConfigSource)
    kc.log-level-io.smallrye.openapi.runtime.scanner.dataobject =  null (Derived)
    kc.https-certificate-file =  /opt/keycloak/conf/certs/keycloak.crt (keycloak.conf)
    kc.log-level-org.jboss.resteasy.resteasy_jaxrs.i18n =  null (Derived)
    kc.log-level-io.quarkus.arc.processor.BeanArchives =  null (Derived)
    kc.log-level-io.quarkus.deployment.steps.ReflectiveHierarchyStep =  null (Derived)
    kc.http-enabled =  false (keycloak.conf)
    kc.log-level-org.hibernate.SQL_SLOW =  null (Derived)
    kc.https-certificate-key-file =  /opt/keycloak/conf/certs/keycloak.key (keycloak.conf)
    kc.log-level-io.quarkus.arc.processor.IndexClassLookupUtils =  null (Derived)
    kc.log-file =  /opt/keycloak/bin/../data/log/keycloak.log (classpath application.properties)
    kc.log-level-org.hibernate.engine.jdbc.spi.SqlExceptionHelper =  null (Derived)
&lt;/span&gt;&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keycloak daemon build (systemd)
&lt;/h3&gt;

&lt;p&gt;I created a file &lt;code&gt;/etc/systemd/system/keycloak.service&lt;/code&gt; with contents&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="nt"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;Keycloak Identity and Access Management
&lt;span class="nt"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;network.target

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="nt"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;keycloak
&lt;span class="nt"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;keycloak
&lt;span class="nt"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;KEYCLOAK_ADMIN=admin
&lt;span class="nt"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;KEYCLOAK_ADMIN_PASSWORD=admin123
&lt;span class="nt"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/opt/keycloak/bin/kc.sh start
&lt;span class="nt"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;on-failure
&lt;span class="nt"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;10

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="nt"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And enabled the service&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; keycloak
&lt;span class="go"&gt;Created symlink '/etc/systemd/system/multi-user.target.wants/keycloak.service' → '/etc/systemd/system/keycloak.service'.
&lt;/span&gt;&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status keycloak
&lt;span class="go"&gt;● keycloak.service - Keycloak Identity and Access Management
&lt;/span&gt;&lt;span class="gp"&gt;     Loaded: loaded (/etc/systemd/system/keycloak.service;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;enabled&lt;span class="p"&gt;;&lt;/span&gt; preset: enabled&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;     Active: active (running) since Thu 2026-03-19 20:05:40 IST;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;6s ago
&lt;span class="go"&gt; Invocation: cde0a8f017a24bac8dd23934aa7a5bf2
   Main PID: 3250 (kc.sh)
      Tasks: 29 (limit: 2301)
     Memory: 269.9M (peak: 270.2M)
        CPU: 6.151s
     CGroup: /system.slice/keycloak.service
             ├─3250 /bin/sh /opt/keycloak/bin/kc.sh start
&lt;/span&gt;&lt;span class="gp"&gt;             └─3322 java -Djava.util.concurrent.ForkJoinPool.common.threadFactory=io.quarkus.bootstrap.forkjoin.QuarkusForkJoinW&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
Mar 19 20:05:40 keycloak.acme.internal systemd[1]: Started keycloak.service - Keycloak Identity and Access Management.
&lt;/span&gt;&lt;span class="gp"&gt;Mar 19 20:05:46 keycloak.acme.internal kc.sh[3322]: 2026-03-19 20:05:46,328 INFO  [org.hibernate.orm.jdbc.batch] (JPA Startup Th&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;debian@keycloak:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keycloak admin dashboard
&lt;/h3&gt;

&lt;p&gt;I visited &lt;code&gt;https://keycloak.acme.internal:8443&lt;/code&gt; from where the &lt;code&gt;rootCA.pem&lt;/code&gt; was trusted in the OS as well as browser certificate store.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe55x1btu531vml9t709u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe55x1btu531vml9t709u.png" alt="001" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then signed in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl7kl5xkwrryli4kjt0ug.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl7kl5xkwrryli4kjt0ug.png" alt="002" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And then created a permanent admin account as it says. I created a new user with username &lt;code&gt;administrator&lt;/code&gt;, set a password, and then assigned &lt;code&gt;admin&lt;/code&gt; role to it. Then logged in with that user and deleted the temporary admin user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3mr94tnqaboqlu63a6v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh3mr94tnqaboqlu63a6v.png" alt="003" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I created a realm for our fictional org - &lt;code&gt;acme&lt;/code&gt; - with display name "Acme Corp", user self-registration disabled (admin-provisioned org), and email verification enabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mailpit initial setup
&lt;/h3&gt;

&lt;p&gt;Since this is a local lab with no real mail infrastructure, I used Mailpit - a lightweight fake SMTP server that catches all outbound emails and displays them in a web UI instead of actually delivering them. This lets me test Keycloak's email flows (verification, password reset, OTP) without setting up a real mail server or domain. I ran it on the same VM as Keycloak since it's a single binary with minimal resource usage.&lt;/p&gt;

&lt;p&gt;I installed mailpit from GitHub, on the same VM as Keycloak.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://github.com/axllent/mailpit/releases/download/v1.29.3/mailpit-linux-amd64.tar.gz
&lt;span class="nb"&gt;mkdir &lt;/span&gt;mailpit
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; mailpit-linux-amd64.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; mailpit
&lt;span class="nb"&gt;cd &lt;/span&gt;mailpit
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;mailpit /usr/local/bin/
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +x /usr/local/bin/mailpit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then verified it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mailpit version
&lt;span class="go"&gt;mailpit v1.29.3 compiled with go1.26.1 on linux/amd64
Error checking for latest release: failed to fetch releases: received status code 403
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And I created a new user for it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;useradd &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /sbin/nologin mailpit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mailpit daemon build (systemd)
&lt;/h3&gt;

&lt;p&gt;I put the following contents on &lt;code&gt;/etc/systemd/system/mailpit.service&lt;/code&gt; (copied the previously generated keys to &lt;code&gt;/etc/mailpit/certs&lt;/code&gt; and changed ownership of that folder to &lt;code&gt;mailpit&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/mailpit.service&lt;/span&gt;
&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="nt"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;Mailpit SMTP and Web UI
&lt;span class="nt"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;network.target

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="nt"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;mailpit
&lt;span class="nt"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;mailpit
&lt;span class="nt"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/usr/local/bin/mailpit --smtp 0.0.0.0:1025 --listen 0.0.0.0:8025 --ui-tls-cert /etc/mailpit/certs/keycloak.acme.internal.pem --ui-tls-key /etc/mailpit/certs/keycloak.acme.internal-key.pem
&lt;span class="nt"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;on-failure
&lt;span class="nt"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;10

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="nt"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And ran&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; mailpit
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status mailpit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@keycloak:~/Downloads/mailpit$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="go"&gt;sudo systemctl enable --now mailpit
sudo systemctl status mailpit
Created symlink '/etc/systemd/system/multi-user.target.wants/mailpit.service' → '/etc/systemd/system/mailpit.service'.
● mailpit.service - Mailpit SMTP and Web UI
&lt;/span&gt;&lt;span class="gp"&gt;     Loaded: loaded (/etc/systemd/system/mailpit.service;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;enabled&lt;span class="p"&gt;;&lt;/span&gt; preset: enab&amp;gt;
&lt;span class="gp"&gt;     Active: active (running) since Fri 2026-03-20 15:37:36 IST;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;67ms ago
&lt;span class="go"&gt; Invocation: 1046374b8fb24c4f92c8a6fe9683cb5f
   Main PID: 2010 (mailpit)
      Tasks: 3 (limit: 2301)
     Memory: 8.7M (peak: 8.7M)
        CPU: 13ms
     CGroup: /system.slice/mailpit.service
&lt;/span&gt;&lt;span class="gp"&gt;             └─2010 /usr/local/bin/mailpit --smtp 0.0.0.0:1025 --listen 0.0.0.0&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;Mar 20 15:37:36 keycloak.acme.internal systemd[1]: Started mailpit.service - Ma&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;Mar 20 15:37:36 keycloak.acme.internal mailpit[2010]: time="2026/03/20 15:37:36&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;Mar 20 15:37:36 keycloak.acme.internal mailpit[2010]: time="2026/03/20 15:37:36&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;Mar 20 15:37:36 keycloak.acme.internal mailpit[2010]: time="2026/03/20 15:37:36&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;Mar 20 15:37:36 keycloak.acme.internal mailpit[2010]: time="2026/03/20 15:37:36&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;debian@keycloak:~/Downloads/mailpit$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keycloak to use Mailpit for email
&lt;/h3&gt;

&lt;p&gt;After I created the ACME Corp realm on Keycloak, I went to Realm Settings, email, entered these values to make use of Mailpit as email service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcet5cwnc211opa6znxde.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcet5cwnc211opa6znxde.png" alt="004" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8p0v73mtd560a42572ce.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8p0v73mtd560a42572ce.png" alt="005" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I visited Mailpit dashboard at &lt;code&gt;https://keycloak.acme.internal:8025/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh50n687325i63kxcybo7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh50n687325i63kxcybo7.png" alt="006" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpjtaj5wuvvplt6mxn7fq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpjtaj5wuvvplt6mxn7fq.png" alt="007" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Password Policy in Keycloak
&lt;/h3&gt;

&lt;p&gt;I went to Authentication -&amp;gt; Password Policy -&amp;gt; Password Policies and set the password requirements to have&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimum 10 characters&lt;/li&gt;
&lt;li&gt;Minimum 1 Uppercase&lt;/li&gt;
&lt;li&gt;Minimum 1 Lowercase&lt;/li&gt;
&lt;li&gt;Minimum 1 Digit&lt;/li&gt;
&lt;li&gt;Minimum 1 Special Character&lt;/li&gt;
&lt;li&gt;Password history: 3 (prevents reuse of last 3 passwords)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3cmmnrwu4fvc7y1tzo7x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3cmmnrwu4fvc7y1tzo7x.png" alt="008" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also configured brute force detection under Realm Settings -&amp;gt; Security Defenses -&amp;gt; Brute force detection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Brute Force Mode: Lockout permanently after temporary lockout&lt;/li&gt;
&lt;li&gt;Max login failures: 5&lt;/li&gt;
&lt;li&gt;Maximum temporary lockouts: 1&lt;/li&gt;
&lt;li&gt;Strategy to increase wait time: Multiple&lt;/li&gt;
&lt;li&gt;Wait increment: 1 minute&lt;/li&gt;
&lt;li&gt;Max wait: 15 minutes&lt;/li&gt;
&lt;li&gt;Failure reset time: 12 hours&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After 5 failed login attempts the account locks out temporarily. If login failures continue through the temporary lockout, the account gets permanently locked and requires an admin to manually re-enable it. This will be demonstrated later in the user lifecycle section using Burp Suite to capture the login request and a Python script to simulate a brute force attack with a wordlist.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9ayn2da5cbsjpn18v2z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9ayn2da5cbsjpn18v2z.png" alt="009" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Organization Structure
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frwapq5izddmehgkowc6l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frwapq5izddmehgkowc6l.png" alt="org-structure" width="669" height="744"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I created the above organization structure in Keycloak - four groups for departments, three realm roles (&lt;code&gt;employee&lt;/code&gt;, &lt;code&gt;manager&lt;/code&gt;, &lt;code&gt;it-admin&lt;/code&gt;), and seven users provisioned and assigned accordingly.&lt;/p&gt;

&lt;p&gt;All users:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fep3efg6ifyqsauwq0rbz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fep3efg6ifyqsauwq0rbz.png" alt="010-all-users" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Group Engineering:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxehypmuxnz6t7w18lm98.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxehypmuxnz6t7w18lm98.png" alt="011-group-engineering" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Group Finance:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7t4ci4xlr0noaeismyxa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7t4ci4xlr0noaeismyxa.png" alt="012-group-finance" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Group HR:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fih3vw61cu0cb3hu8yoh9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fih3vw61cu0cb3hu8yoh9.png" alt="013-group-hr" width="799" height="483"&gt;&lt;/a&gt;.png)&lt;/p&gt;

&lt;p&gt;Group IT:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftjzgn17s3vm81xvyzb65.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftjzgn17s3vm81xvyzb65.png" alt="014-group-it" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Role employee:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flblwvqr0dtzn4nrdqckm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flblwvqr0dtzn4nrdqckm.png" alt="015-role-employee" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Role manager:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4shg27qxk1deaiyt9tt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4shg27qxk1deaiyt9tt.png" alt="016-role-manager" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Role IT Admin:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foshy0g5ahzgivji4ddte.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foshy0g5ahzgivji4ddte.png" alt="017-role-it-admin" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforcing MFA via TOTP (Google Authenticator)
&lt;/h3&gt;

&lt;p&gt;I set up password based logins for users (Something You Know). Now I will set up Google Authenticator based 2FA (Something You Have). For this lab, I will be using &lt;a href="https://gauth.apps.gbraad.nl" rel="noopener noreferrer"&gt;gauth.apps.gbraad.nl&lt;/a&gt;, a browser based Google Authenticator app implementation, on the host machine, as my Google Authenticator client.&lt;/p&gt;

&lt;p&gt;I duplicated the default &lt;code&gt;browser&lt;/code&gt; authentication flow in Authentication -&amp;gt; Flows, named it &lt;code&gt;acme browser&lt;/code&gt;, and set the &lt;strong&gt;Browser - Conditional OTP&lt;/strong&gt; step from &lt;code&gt;Conditional&lt;/code&gt; to &lt;code&gt;Required&lt;/code&gt;. Then under Authentication -&amp;gt; Bindings, I set the Browser flow to &lt;code&gt;acme browser&lt;/code&gt;. This forces TOTP on every login for all users in the &lt;code&gt;acme&lt;/code&gt; realm.&lt;/p&gt;

&lt;p&gt;I also enabled the following required actions under Authentication -&amp;gt; Required actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update Password&lt;/li&gt;
&lt;li&gt;Verify Email&lt;/li&gt;
&lt;li&gt;Configure OTP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are applied to all users, so on first login every user is forced to change their temporary password, verify their email via Mailpit, and set up TOTP before they can access anything.&lt;/p&gt;

&lt;p&gt;jsmith is forced to setup Google Authenticator login in next successful login:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl2pzt50qq38ndd88fy6c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl2pzt50qq38ndd88fy6c.png" alt="018-jsmith-2fa" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;jsmith added key to Google Authenticator&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5gdna8czvrxwwq0refe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5gdna8czvrxwwq0refe.png" alt="019-jsmith-ga" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;jsmith enters the OTP value and logs in successfully. Every other account did the same.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffj2v34mvx9iaa3g0tpr9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffj2v34mvx9iaa3g0tpr9.png" alt="020-everyone-2fa" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  OIDC Client and SSO Demo
&lt;/h3&gt;

&lt;p&gt;To demonstrate SSO, I registered a client in the &lt;code&gt;acme&lt;/code&gt; realm pointing to the official Keycloak demo app hosted at &lt;code&gt;https://www.keycloak.org/app/&lt;/code&gt;. Since it's a static page that runs entirely in the browser, it talks directly to my local Keycloak instance - no server-side component needed.&lt;/p&gt;

&lt;p&gt;Client settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client type: OpenID Connect&lt;/li&gt;
&lt;li&gt;Client authentication: Off (public client)&lt;/li&gt;
&lt;li&gt;Valid redirect URIs: &lt;code&gt;https://www.keycloak.org/app/*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Valid post logout redirect URIs: &lt;code&gt;https://www.keycloak.org/app/*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Web origins: &lt;code&gt;https://www.keycloak.org&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdhjevx6b1ncsmbv1ycgg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdhjevx6b1ncsmbv1ycgg.png" alt="021-client-setup" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the demo app, I pointed it at my Keycloak instance by entering the server URL, realm, and client ID.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvu6qcd0j6wg70fied7cb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvu6qcd0j6wg70fied7cb.png" alt="022-client-config" width="799" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicking Sign In redirected to the Keycloak login page, went through the full authentication flow - password then TOTP - and landed back on the demo app showing jsmith's authenticated session with the decoded token.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fivuelz6swndezxbotobp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fivuelz6swndezxbotobp.png" alt="023-jsmith-sso-success" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Employee lifecycle management
&lt;/h3&gt;

&lt;p&gt;To demonstrate the full employee lifecycle, I walked through four real-world IAM events using &lt;code&gt;mclark&lt;/code&gt; from the HR department.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. mclark gets promoted to HR manager
&lt;/h4&gt;

&lt;p&gt;mclark was promoted from HR employee to HR manager. In Keycloak, this meant removing the &lt;code&gt;employee&lt;/code&gt; role and assigning &lt;code&gt;manager&lt;/code&gt; under the user's Role Mappings tab.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fca4h7pbli07yv66eijhi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fca4h7pbli07yv66eijhi.png" alt="024-before-mclark-promotion" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2u1tfn5ygtwn8vrr2h92.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2u1tfn5ygtwn8vrr2h92.png" alt="025-after-mclark-promotion" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  2. mclark takes a vacation (account disabled)
&lt;/h4&gt;

&lt;p&gt;mclark went on an extended leave of absence. Rather than deleting the account, it was disabled - access is immediately revoked but the account and its data remain intact for when the employee returns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flnioetjh74v6adxb6f7j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flnioetjh74v6adxb6f7j.png" alt="026-mclark-vacation" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  3. mclark returns (account enabled)
&lt;/h4&gt;

&lt;p&gt;mclark returned from leave. The account was re-enabled, restoring access with all roles and group memberships unchanged.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3shapzh3jjwptdjakpz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3shapzh3jjwptdjakpz.png" alt="027-mclark-returns" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  4. mclark quits (account deleted)
&lt;/h4&gt;

&lt;p&gt;mclark resigned. The account was permanently deleted from the realm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirmation&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5pmkavkenq9kinlch89l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5pmkavkenq9kinlch89l.png" alt="028-delete-mclark-confirmation" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;mclark is no more&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhcxx736qrnknx78ooku3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhcxx736qrnknx78ooku3.png" alt="029-no-mclark" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Brute Force Protection
&lt;/h3&gt;

&lt;p&gt;Now I am going to do a brute force attack on jsmith's account. I am going to manually type in some random password values and hit enter multiple times to see what happens.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm6zs7id9uzwlew5tamhe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm6zs7id9uzwlew5tamhe.png" alt="030-jsmith-temp-locked" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see from the admin dashboard, jsmith is now temporarily locked. &lt;/p&gt;

&lt;h3&gt;
  
  
  Event log
&lt;/h3&gt;

&lt;p&gt;Keycloak captures two categories of events per realm. Login events record every authentication-related action - successful logins, failed attempts, logouts, and token operations. Admin events record every administrative action performed on the realm - user creation, role assignments, deletions, client configuration changes, and so on. Both are visible under Events in the admin console.&lt;/p&gt;

&lt;h4&gt;
  
  
  Login Events
&lt;/h4&gt;

&lt;p&gt;Here is a snippet of lwilson signin related events&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Femyldtl3nqtoa7qdilsw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Femyldtl3nqtoa7qdilsw.png" alt="031-lwilson-signin" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Admin Events
&lt;/h4&gt;

&lt;p&gt;Here is a snippet of admin events related to jsmith getting promoted&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feys5z3clmsngpt4v9p9v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feys5z3clmsngpt4v9p9v.png" alt="032-jsmith-gets-promoted" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The internship asked for a designed IAM solution and an implementation plan. This lab is that plan executed.&lt;/p&gt;

&lt;p&gt;Every requirement from the Tata Forage simulation is covered here in a working deployment: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keycloak handles identity and access management for Acme Corp the same way SailPoint and Oracle Identity Manager would handle it for TechCorp.&lt;/li&gt;
&lt;li&gt;RBAC is enforced through realm roles mapped to job functions.&lt;/li&gt;
&lt;li&gt;User provisioning and de-provisioning follow the same lifecycle model - onboarding with required actions, role changes on promotion, account suspension on leave, and full deletion on resignation.&lt;/li&gt;
&lt;li&gt;MFA via TOTP enforces the "something you know + something you have" authentication standard.&lt;/li&gt;
&lt;li&gt;SSO via OIDC demonstrates seamless access across applications from a single authenticated session.&lt;/li&gt;
&lt;li&gt;Mailpit handles all email flows locally.&lt;/li&gt;
&lt;li&gt;Brute force protection is configured and verified.&lt;/li&gt;
&lt;li&gt;Admin and login events are captured in the audit log.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire stack - Keycloak, Mailpit, TLS via a local CA, DNS via Pi-hole, and network routing via pfSense - runs self-contained in VirtualBox, making it fully reproducible as a home lab without any cloud dependency or paid tooling.&lt;/p&gt;

</description>
      <category>security</category>
      <category>iam</category>
      <category>keycloak</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Integrating a local mail server into my LDAP lab</title>
      <dc:creator>Shoban Chiddarth</dc:creator>
      <pubDate>Thu, 19 Mar 2026 07:38:19 +0000</pubDate>
      <link>https://dev.to/shobanchiddarth/integrating-a-local-mail-server-into-my-ldap-lab-4h5f</link>
      <guid>https://dev.to/shobanchiddarth/integrating-a-local-mail-server-into-my-ldap-lab-4h5f</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/ShobanChiddarth/postfix-dovecot-mailserver-in-openldap-lab" rel="noopener noreferrer"&gt;GitHub Repo: @ShobanChiddarth/postfix-dovecot-mailserver-in-openldap-lab&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is the second stage of my &lt;a href="https://dev.to/shobanchiddarth/openldap-home-lab-cyber-security-technical-write-up-4g42"&gt;OpenLDAP home lab&lt;/a&gt;. I set up a mail server to work with existing LDAP configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-Requisites
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/shobanchiddarth/the-superior-way-to-make-vms-communicate-with-each-other-as-well-as-host-with-internet-access-42m1"&gt;VM Intercommunication setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shobanchiddarth/setting-up-pi-hole-as-a-custom-dns-server-on-my-home-lab-4jd7"&gt;Pi-hole&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shobanchiddarth/setting-up-ssl-https-on-my-home-lab-g45"&gt;Local TLS for HTTPS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shobanchiddarth/openldap-home-lab-cyber-security-technical-write-up-4g42"&gt;OpenLDAP Home Lab&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setup Process
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mail Server Install
&lt;/h3&gt;

&lt;p&gt;In the existing setup, I cloned another debian server VM, gave it static IP &lt;code&gt;192.168.57.7&lt;/code&gt;, set its hostname to &lt;code&gt;mail-server.acme.internal&lt;/code&gt; and then added the DNS record in Pi-hole. Then I went ahead and installed the required packages for postfix and dovecot to work with LDAP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; postfix postfix-ldap dovecot-core dovecot-imapd dovecot-ldap dovecot-lmtpd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I chose "Internet Site" in the postfix install prompt asking about which type of mail server to install and set the domain to &lt;code&gt;acme.internal&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Postfix Config
&lt;/h3&gt;

&lt;p&gt;I edited &lt;code&gt;/etc/postfix/main.cf&lt;/code&gt; to have this configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;myhostname&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;mail-server.acme.internal&lt;/span&gt;
&lt;span class="py"&gt;mydomain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;acme.internal&lt;/span&gt;
&lt;span class="py"&gt;mydestination&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$myhostname, $mydomain, localhost&lt;/span&gt;
&lt;span class="py"&gt;mynetworks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;192.168.57.0/24 127.0.0.0/8&lt;/span&gt;
&lt;span class="py"&gt;inet_interfaces&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
&lt;span class="py"&gt;mailbox_transport&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;lmtp:unix:private/dovecot-lmtp&lt;/span&gt;
&lt;span class="py"&gt;smtpd_sasl_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;dovecot&lt;/span&gt;
&lt;span class="py"&gt;smtpd_sasl_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;private/auth&lt;/span&gt;
&lt;span class="py"&gt;smtpd_sasl_auth_enable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;local_recipient_maps&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ldap:/etc/postfix/ldap-recipients.cf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mailbox_transport&lt;/code&gt; hands mail delivery off to Dovecot via LMTP. The &lt;code&gt;smtpd_sasl_*&lt;/code&gt; settings let Postfix use Dovecot for SMTP authentication. &lt;code&gt;local_recipient_maps&lt;/code&gt; tells Postfix to look up valid recipients in LDAP instead of the local user table - without this, Postfix rejects mail to LDAP users with "User unknown in local recipient table".&lt;/p&gt;

&lt;p&gt;I created &lt;code&gt;/etc/postfix/ldap-recipients.cf&lt;/code&gt; for the recipient lookup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;server_host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ldaps://ldap-server.acme.internal&lt;/span&gt;
&lt;span class="py"&gt;bind&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;bind_dn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;cn=sssd,ou=service-accounts,dc=acme,dc=internal&lt;/span&gt;
&lt;span class="py"&gt;bind_pw&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;SSSDPass123&lt;/span&gt;
&lt;span class="py"&gt;search_base&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ou=users,dc=acme,dc=internal&lt;/span&gt;
&lt;span class="py"&gt;query_filter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;(&amp;amp;(objectClass=posixAccount)(mail=%s))&lt;/span&gt;
&lt;span class="py"&gt;result_attribute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;mail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reloaded Postfix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload postfix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Local CA Config on Mail Server
&lt;/h3&gt;

&lt;p&gt;I copied &lt;code&gt;rootCA.pem&lt;/code&gt; that I created earlier to this server and trusted it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@mail-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="go"&gt;rootCA.pem
&lt;/span&gt;&lt;span class="gp"&gt;debian@mail-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo cp &lt;/span&gt;rootCA.pem /usr/local/share/ca-certificates/acme-rootCA.crt
&lt;span class="go"&gt;[sudo] password for debian: 
&lt;/span&gt;&lt;span class="gp"&gt;debian@mail-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;update-ca-certificates
&lt;span class="go"&gt;Updating certificates in /etc/ssl/certs...
rehash: warning: skipping ca-certificates.crt, it does not contain exactly one certificate or CRL
&lt;/span&gt;&lt;span class="gp"&gt;1 added, 0 removed;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="go"&gt;Running hooks in /etc/ca-certificates/update.d...
done.
&lt;/span&gt;&lt;span class="gp"&gt;debian@mail-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dovecot TLS Config
&lt;/h3&gt;

&lt;p&gt;I generated a TLS cert for &lt;code&gt;mail-server.acme.internal&lt;/code&gt; using mkcert and copied it to the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cp &lt;/span&gt;mail-server.acme.internal.pem /etc/dovecot/private/mail-server.crt
&lt;span class="nb"&gt;sudo cp &lt;/span&gt;mail-server.acme.internal-key.pem /etc/dovecot/private/mail-server.key
&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:dovecot /etc/dovecot/private/mail-server.key
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;640 /etc/dovecot/private/mail-server.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then updated &lt;code&gt;/etc/dovecot/conf.d/10-ssl.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ssl_server_cert_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/etc/dovecot/private/mail-server.crt&lt;/span&gt;
&lt;span class="py"&gt;ssl_server_key_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/etc/dovecot/private/mail-server.key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dovecot LDAP Config
&lt;/h3&gt;

&lt;p&gt;I changed dovecot auth settings to use LDAP auth instead of system auth by editing &lt;code&gt;/etc/dovecot/conf.d/10-auth.conf&lt;/code&gt; - commented out &lt;code&gt;auth-system.conf.ext&lt;/code&gt; and uncommented &lt;code&gt;auth-ldap.conf.ext&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@mail-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/dovecot/conf.d/10-auth.conf | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"include auth"&lt;/span&gt;
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;include auth-deny.conf.ext
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;include auth-master.conf.ext
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;include auth-oauth2.conf.ext
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;include auth-system.conf.ext
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;include auth-sql.conf.ext
&lt;span class="go"&gt;!include auth-ldap.conf.ext
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;include auth-passwdfile.conf.ext
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;include auth-static.conf.ext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dovecot 2.4 changed the config syntax from older versions, so the standard examples online did not work. After checking the example config the package ships with, I set &lt;code&gt;/etc/dovecot/conf.d/auth-ldap.conf.ext&lt;/code&gt; to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;ldap_uris&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ldaps://ldap-server.acme.internal&lt;/span&gt;
&lt;span class="py"&gt;ldap_auth_dn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;cn=sssd,ou=service-accounts,dc=acme,dc=internal&lt;/span&gt;
&lt;span class="py"&gt;ldap_auth_dn_password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;SSSDPass123&lt;/span&gt;
&lt;span class="py"&gt;ldap_base&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ou=users,dc=acme,dc=internal&lt;/span&gt;

&lt;span class="err"&gt;passdb&lt;/span&gt; &lt;span class="err"&gt;ldap&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;ldap_filter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;(&amp;amp;(objectClass=posixAccount)(uid=%{user}))&lt;/span&gt;
  &lt;span class="py"&gt;ldap_bind&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="err"&gt;userdb&lt;/span&gt; &lt;span class="err"&gt;ldap&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;ldap_filter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;(&amp;amp;(objectClass=posixAccount)(uid=%{user}))&lt;/span&gt;
  &lt;span class="err"&gt;fields&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;uid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;vmail&lt;/span&gt;
    &lt;span class="py"&gt;gid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;vmail&lt;/span&gt;
    &lt;span class="py"&gt;home&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/mail/vhosts/%{user | username}&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reuses the same read-only service account from the LDAP lab. &lt;code&gt;ldap_bind = yes&lt;/code&gt; means Dovecot verifies passwords by actually binding to LDAP as the user rather than fetching and comparing the hash. The &lt;code&gt;userdb&lt;/code&gt; block maps all mail storage to a dedicated &lt;code&gt;vmail&lt;/code&gt; system user instead of running as the LDAP user's UID.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: &lt;code&gt;ldap_auth_dn_password&lt;/code&gt; is stored in plaintext, but &lt;code&gt;/etc/dovecot/conf.d/auth-ldap.conf.ext&lt;/code&gt; is owned by root and not readable by other users.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Dovecot Mail Storage and Postfix Integration
&lt;/h3&gt;

&lt;p&gt;Since LDAP users don't have home directories on the mail server, I created a dedicated &lt;code&gt;vmail&lt;/code&gt; system user to own all mailboxes and set up a central mail storage directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;adduser &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--no-create-home&lt;/span&gt; &lt;span class="nt"&gt;--group&lt;/span&gt; vmail
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/mail/vhosts
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; vmail:vmail /var/mail/vhosts
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;770 /var/mail/vhosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I updated &lt;code&gt;/etc/dovecot/conf.d/10-mail.conf&lt;/code&gt; to use this path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;mail_driver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;maildir&lt;/span&gt;
&lt;span class="py"&gt;mail_home&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/mail/vhosts/%{user | username}&lt;/span&gt;
&lt;span class="py"&gt;mail_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;%{home}/Maildir&lt;/span&gt;
&lt;span class="py"&gt;first_valid_uid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I configured the LMTP and auth unix sockets in &lt;code&gt;/etc/dovecot/conf.d/10-master.conf&lt;/code&gt; so Postfix can hand off incoming mail to Dovecot and use Dovecot for SMTP authentication.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;service lmtp&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;service&lt;/span&gt; &lt;span class="err"&gt;lmtp&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;unix_listener&lt;/span&gt; &lt;span class="err"&gt;/var/spool/postfix/private/dovecot-lmtp&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0600&lt;/span&gt;
    &lt;span class="py"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;postfix&lt;/span&gt;
    &lt;span class="py"&gt;group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;postfix&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;service auth&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;service&lt;/span&gt; &lt;span class="err"&gt;auth&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;unix_listener&lt;/span&gt; &lt;span class="err"&gt;/var/spool/postfix/private/auth&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0660&lt;/span&gt;
    &lt;span class="py"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;postfix&lt;/span&gt;
    &lt;span class="py"&gt;group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;postfix&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restarted Dovecot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart dovecot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Sending mail
&lt;/h3&gt;

&lt;p&gt;I sent a test mail from &lt;code&gt;jsmith&lt;/code&gt; to &lt;code&gt;adoe&lt;/code&gt; over SMTP from a desktop VM on the same network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debian@debian:~/acme-certs&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"smtp://mail-server.acme.internal:25"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mail-from&lt;/span&gt; &lt;span class="s2"&gt;"jsmith@acme.internal"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mail-rcpt&lt;/span&gt; &lt;span class="s2"&gt;"adoe@acme.internal"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--upload-file&lt;/span&gt; - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
From: jsmith@acme.internal
To: adoe@acme.internal
Subject: Test from jsmith

Hello Alice, this is John.
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Postfix accepted it and delivered it to Dovecot via LMTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;250 2.0.0 Ok: queued as 98B624018B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verified the mail landed in adoe's mailbox on the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debian@mail-server:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo ls&lt;/span&gt; /var/mail/vhosts/adoe/Maildir/new/
1773903163.M693787P7845.mail-server.acme.internal,S&lt;span class="o"&gt;=&lt;/span&gt;560,W&lt;span class="o"&gt;=&lt;/span&gt;576
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Retrieving mail over IMAPS
&lt;/h3&gt;

&lt;p&gt;I verified that adoe can authenticate and retrieve mail over IMAPS using their LDAP credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;debian@debian:~/acme-certs&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"imaps://mail-server.acme.internal/INBOX"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="s2"&gt;"adoe:Password456"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;* SSL certificate verify ok.
* Connected to mail-server.acme.internal (192.168.57.7) port 993
&amp;lt; A002 OK Logged in
&amp;lt; * LIST (\HasNoChildren) "." INBOX
&amp;lt; A003 OK List completed (0.001 + 0.000 secs).
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TLS handshake succeeded using the mkcert CA, LDAP authentication worked, and the INBOX is accessible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thunderbird Verification
&lt;/h2&gt;

&lt;p&gt;Thunderbird has its own certificate store separate from the system trust store, so I had to manually import &lt;code&gt;rootCA.pem&lt;/code&gt; into Thunderbird settings before it would trust the IMAPS connection.&lt;/p&gt;

&lt;h3&gt;
  
  
  adoe sends an email to jsmith
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgul53pqgp6lark4z3vzx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgul53pqgp6lark4z3vzx.png" alt="001" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  jsmith opens it
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1gxqwxkkahcpp8ukt4k5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1gxqwxkkahcpp8ukt4k5.png" alt="002" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  jsmith sends a reply email to adoe
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fimyam4kjoec5ivbx9hux.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fimyam4kjoec5ivbx9hux.png" alt="003" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  adoe views the reply email
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffmhdz9aw82uk9744hpim.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffmhdz9aw82uk9744hpim.png" alt="004" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Postfix and Dovecot are now running as a mail server for &lt;code&gt;acme.internal&lt;/code&gt;. Users defined in the OpenLDAP directory can send and receive mail using their LDAP credentials. Postfix looks up valid recipients in LDAP so it accepts mail for LDAP users. Dovecot authenticates over LDAPS using the same read-only service account from the LDAP lab and stores mail in a central directory owned by a dedicated &lt;code&gt;vmail&lt;/code&gt; user. IMAP access is over TLS only.&lt;/p&gt;

&lt;p&gt;This mail server will be used in the next part of this series as the SMTP backend for Keycloak notifications.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>linux</category>
      <category>cybersecurity</category>
      <category>mailserver</category>
    </item>
    <item>
      <title>OpenLDAP home lab - Cyber Security technical write up</title>
      <dc:creator>Shoban Chiddarth</dc:creator>
      <pubDate>Tue, 17 Mar 2026 07:55:15 +0000</pubDate>
      <link>https://dev.to/shobanchiddarth/openldap-home-lab-cyber-security-technical-write-up-4g42</link>
      <guid>https://dev.to/shobanchiddarth/openldap-home-lab-cyber-security-technical-write-up-4g42</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/ShobanChiddarth/openldap-iam-lab" rel="noopener noreferrer"&gt;GitHub Repo: @ShobanChiddarth/openldap-iam-lab&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This home lab is a part of my setup for an IAM lab using KeyCloak. In this lab I set up OpenLDAP in a Virtualbox lab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-Requisites
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/shobanchiddarth/the-superior-way-to-make-vms-communicate-with-each-other-as-well-as-host-with-internet-access-42m1"&gt;VM Intercommunication setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shobanchiddarth/setting-up-pi-hole-as-a-custom-dns-server-on-my-home-lab-4jd7"&gt;Pi-hole&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shobanchiddarth/setting-up-ssl-https-on-my-home-lab-g45"&gt;Local TLS for HTTPS&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setup Process
&lt;/h2&gt;

&lt;p&gt;Let's say the fictional organization I am working in is Acme Inc. The domain name would be &lt;code&gt;acme.internal&lt;/code&gt;. I have the VM intercommunication setup that I linked earlier ready. I also setup custom DNS records in the Pi-hole (&lt;code&gt;ldap-server.acme.internal&lt;/code&gt; points to &lt;code&gt;192.168.57.5&lt;/code&gt; and &lt;code&gt;ldap-client.acme.internal&lt;/code&gt; points to &lt;code&gt;192.168.57.6&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up LDAP server
&lt;/h3&gt;

&lt;p&gt;I installed &lt;code&gt;slapd&lt;/code&gt; and &lt;code&gt;ldap-utils&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;slapd ldap-utils
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And set the admin password mid installation, then configured it with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg-reconfigure slapd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It asked me for organization name along with domain, I filled in with the appropriate details. And verified it with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;slapcat
&lt;span class="go"&gt;dn: dc=acme,dc=internal
objectClass: top
objectClass: dcObject
objectClass: organization
o: Acme
dc: acme
structuralObjectClass: organization
entryUUID: 2938c77a-b601-1040-8564-d9abd6bd9b70
creatorsName: cn=admin,dc=acme,dc=internal
createTimestamp: 20260317035728Z
&lt;/span&gt;&lt;span class="gp"&gt;entryCSN: 20260317035728.729122Z#&lt;/span&gt;000000#000#000000
&lt;span class="go"&gt;modifiersName: cn=admin,dc=acme,dc=internal
modifyTimestamp: 20260317035728Z

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating Directory structure
&lt;/h3&gt;

&lt;p&gt;I created &lt;code&gt;structure.ldif&lt;/code&gt; to add two organizational units - one for users and one for groups, and loaded it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano structure.ldif
&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;structure.ldif 
&lt;span class="go"&gt;dn: ou=users,dc=acme,dc=internal
objectClass: organizationalUnit
ou: users

dn: ou=groups,dc=acme,dc=internal
objectClass: organizationalUnit
ou: groups
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ldapadd &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="s2"&gt;"cn=admin,dc=acme,dc=internal"&lt;/span&gt; &lt;span class="nt"&gt;-W&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; structure.ldif
&lt;span class="go"&gt;Enter LDAP Password: 
adding new entry "ou=users,dc=acme,dc=internal"

adding new entry "ou=groups,dc=acme,dc=internal"

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Adding users
&lt;/h3&gt;

&lt;p&gt;I will be creating 2 users "John Smith" and "Alice Doe" with passwords &lt;code&gt;Password123&lt;/code&gt; and &lt;code&gt;Password456&lt;/code&gt; respectively. I generated the password hashes using&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/sbin/slappasswd &lt;span class="nt"&gt;-s&lt;/span&gt; Password123
&lt;span class="go"&gt;{SSHA}2sIS9koZfMFfwEjWoglS4Iu8XpCvamLk
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/sbin/slappasswd &lt;span class="nt"&gt;-s&lt;/span&gt; Password456
&lt;span class="go"&gt;{SSHA}QlqSwZdrWeINW5PS54vTx4Bpqt+6PLdi
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then created 2 users in &lt;code&gt;users.ldif&lt;/code&gt; and loaded them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano users.ldif
&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;users.ldif 
&lt;span class="go"&gt;dn: uid=jsmith,ou=users,dc=acme,dc=internal
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: jsmith
sn: Smith
givenName: John
cn: John Smith
displayName: John Smith
uidNumber: 10001
gidNumber: 10001
userPassword: {SSHA}2sIS9koZfMFfwEjWoglS4Iu8XpCvamLk
loginShell: /bin/bash
homeDirectory: /home/jsmith
mail: jsmith@acme.internal

dn: uid=adoe,ou=users,dc=acme,dc=internal
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: adoe
sn: Doe
givenName: Alice
cn: Alice Doe
displayName: Alice Doe
uidNumber: 10002
gidNumber: 10002
userPassword: {SSHA}QlqSwZdrWeINW5PS54vTx4Bpqt+6PLdi
loginShell: /bin/bash
homeDirectory: /home/adoe
mail: adoe@acme.internal
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ldapadd &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="s2"&gt;"cn=admin,dc=acme,dc=internal"&lt;/span&gt; &lt;span class="nt"&gt;-W&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; users.ldif
&lt;span class="go"&gt;Enter LDAP Password: 
adding new entry "uid=jsmith,ou=users,dc=acme,dc=internal"

adding new entry "uid=adoe,ou=users,dc=acme,dc=internal"

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Adding groups
&lt;/h3&gt;

&lt;p&gt;I created groups engineering and HR, added jsmith to engineering and Alice to HR, and loaded them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano groups.ldif
&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;groups.ldif 
&lt;span class="go"&gt;dn: cn=engineers,ou=groups,dc=acme,dc=internal
objectClass: posixGroup
cn: engineers
gidNumber: 10001
memberUid: jsmith

dn: cn=hr,ou=groups,dc=acme,dc=internal
objectClass: posixGroup
cn: hr
gidNumber: 10002
memberUid: adoe
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ldapadd &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="s2"&gt;"cn=admin,dc=acme,dc=internal"&lt;/span&gt; &lt;span class="nt"&gt;-W&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; groups.ldif
&lt;span class="go"&gt;Enter LDAP Password: 
adding new entry "cn=engineers,ou=groups,dc=acme,dc=internal"

adding new entry "cn=hr,ou=groups,dc=acme,dc=internal"

&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setting up LDAPS (using mkcert)
&lt;/h3&gt;

&lt;p&gt;I created TLS certificates for &lt;code&gt;ldap-server.acme.internal&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@debian:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mkcert &lt;span class="nt"&gt;-install&lt;/span&gt;
&lt;span class="go"&gt;Created a new local CA 💥
The local CA is now installed in the system trust store! ⚡️
ERROR: no Firefox and/or Chrome/Chromium security databases found

&lt;/span&gt;&lt;span class="gp"&gt;debian@debian:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mkcert &lt;span class="nt"&gt;-CAROOT&lt;/span&gt;
&lt;span class="go"&gt;/home/debian/.local/share/mkcert
&lt;/span&gt;&lt;span class="gp"&gt;debian@debian:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;mkcert &lt;span class="nt"&gt;-CAROOT&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/rootCA.pem &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="gp"&gt;debian@debian:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="go"&gt;rootCA-key.pem  rootCA.pem
&lt;/span&gt;&lt;span class="gp"&gt;debian@debian:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mkcert ldap-server.acme.internal
&lt;span class="go"&gt;Note: the local CA is not installed in the Firefox and/or Chrome/Chromium trust store.
Run "mkcert -install" for certificates to be trusted automatically ⚠️

Created a new certificate valid for the following names 📜
 - "ldap-server.acme.internal"

The certificate is at "./ldap-server.acme.internal.pem" and the key at "./ldap-server.acme.internal-key.pem" ✅

It will expire on 17 June 2028 🗓

&lt;/span&gt;&lt;span class="gp"&gt;debian@debian:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="go"&gt;total 16
-rw------- 1 debian debian 1704 Mar 17 09:55 ldap-server.acme.internal-key.pem
-rw-r--r-- 1 debian debian 1472 Mar 17 09:55 ldap-server.acme.internal.pem
-rw-r--r-- 1 debian debian 1619 Mar 17 09:54 rootCA.pem
&lt;/span&gt;&lt;span class="gp"&gt;debian@debian:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copied those 3 files onto the server and set the required permissions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="go"&gt;ldap-server.acme.internal-key.pem  ldap-server.acme.internal.pem  rootCA.pem
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/ldap/tls
&lt;span class="gp"&gt;debian@ldap-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo cp &lt;/span&gt;ldap-server.acme.internal.pem /etc/ldap/tls/ldap-server.crt
&lt;span class="gp"&gt;debian@ldap-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo cp &lt;/span&gt;ldap-server.acme.internal-key.pem /etc/ldap/tls/ldap-server.key
&lt;span class="gp"&gt;debian@ldap-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo cp &lt;/span&gt;rootCA.pem /etc/ldap/tls/rootCA.pem
&lt;span class="gp"&gt;debian@ldap-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; openldap:openldap /etc/ldap/tls
&lt;span class="gp"&gt;debian@ldap-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;640 /etc/ldap/tls/ldap-server.key
&lt;span class="gp"&gt;debian@ldap-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;644 /etc/ldap/tls/ldap-server.crt /etc/ldap/tls/rootCA.pem
&lt;span class="gp"&gt;debian@ldap-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /etc/ldap/tls/
&lt;span class="go"&gt;total 12
-rw-r--r-- 1 openldap openldap 1472 Mar 17 10:51 ldap-server.crt
-rw-r----- 1 openldap openldap 1704 Mar 17 10:51 ldap-server.key
-rw-r--r-- 1 openldap openldap 1619 Mar 17 10:51 rootCA.pem
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ld&lt;/span&gt; /etc/ldap/tls/
&lt;span class="go"&gt;drwxr-xr-x 2 openldap openldap 4096 Mar 17 10:51 /etc/ldap/tls/
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I created tls conf file (&lt;code&gt;tls.ldif&lt;/code&gt;) and applied it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano tls.ldif
&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;tls.ldif 
&lt;span class="go"&gt;dn: cn=config
changetype: modify
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ldap/tls/rootCA.pem
-
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ldap/tls/ldap-server.crt
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ldap/tls/ldap-server.key
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ldapmodify &lt;span class="nt"&gt;-Y&lt;/span&gt; EXTERNAL &lt;span class="nt"&gt;-H&lt;/span&gt; ldapi:/// &lt;span class="nt"&gt;-f&lt;/span&gt; tls.ldif
&lt;span class="go"&gt;SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "cn=config"

&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I set &lt;code&gt;SLAPD_SERVICES&lt;/code&gt; in &lt;code&gt;/etc/default/slapd&lt;/code&gt; to use LDAPS&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SLAPD_SERVICES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ldapi:/// ldaps:///"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And specified LDAP client tools on the server to use the certificate&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"TLS_CACERT /etc/ldap/tls/rootCA.pem"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/ldap/ldap.conf
&lt;span class="go"&gt;TLS_CACERT /etc/ldap/tls/rootCA.pem
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I restarted &lt;code&gt;slapd&lt;/code&gt; service and checked if LDAPS is working&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart slapd
&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ldapsearch &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; ldaps://ldap-server.acme.internal &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  -D "cn=admin,dc=acme,dc=internal" -W \
  -b "dc=acme,dc=internal" "(objectClass=*)"
Enter LDAP Password: 
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;extended LDIF
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;LDAPv3
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;base &amp;lt;&lt;span class="nv"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;acme,dc&lt;span class="o"&gt;=&lt;/span&gt;internal&amp;gt; with scope subtree
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;filter: &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;objectClass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;requesting: ALL
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;acme.internal
&lt;span class="go"&gt;dn: dc=acme,dc=internal
objectClass: top
objectClass: dcObject
objectClass: organization
o: Acme
dc: acme

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;users&lt;/span&gt;, acme.internal
&lt;span class="go"&gt;dn: ou=users,dc=acme,dc=internal
objectClass: organizationalUnit
ou: users

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;groups&lt;/span&gt;, acme.internal
&lt;span class="go"&gt;dn: ou=groups,dc=acme,dc=internal
objectClass: organizationalUnit
ou: groups

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;jsmith, &lt;span class="nb"&gt;users&lt;/span&gt;, acme.internal
&lt;span class="go"&gt;dn: uid=jsmith,ou=users,dc=acme,dc=internal
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: jsmith
sn: Smith
givenName: John
cn: John Smith
displayName: John Smith
uidNumber: 10001
gidNumber: 10001
userPassword:: e1NTSEF9MnNJUzlrb1pmTUZmd0VqV29nbFM0SXU4WHBDdmFtTGs=
loginShell: /bin/bash
homeDirectory: /home/jsmith
mail: jsmith@acme.internal

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;adoe, &lt;span class="nb"&gt;users&lt;/span&gt;, acme.internal
&lt;span class="go"&gt;dn: uid=adoe,ou=users,dc=acme,dc=internal
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: adoe
sn: Doe
givenName: Alice
cn: Alice Doe
displayName: Alice Doe
uidNumber: 10002
gidNumber: 10002
userPassword:: e1NTSEF9UWxxU3daZHJXZUlOVzVQUzU0dlR4NEJwcXQrNlBMZGk=
loginShell: /bin/bash
homeDirectory: /home/adoe
mail: adoe@acme.internal

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;engineers, &lt;span class="nb"&gt;groups&lt;/span&gt;, acme.internal
&lt;span class="go"&gt;dn: cn=engineers,ou=groups,dc=acme,dc=internal
objectClass: posixGroup
cn: engineers
gidNumber: 10001
memberUid: jsmith

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;hr, &lt;span class="nb"&gt;groups&lt;/span&gt;, acme.internal
&lt;span class="go"&gt;dn: cn=hr,ou=groups,dc=acme,dc=internal
objectClass: posixGroup
cn: hr
gidNumber: 10002
memberUid: adoe

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;search result
&lt;span class="go"&gt;search: 2
result: 0 Success

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;numResponses: 8
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;numEntries: 7
&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But there was one small problem with this. The &lt;code&gt;userPassword&lt;/code&gt;'s base64 encoded SSHA has is visible. I am going to fix that using ACLs.&lt;/p&gt;

&lt;p&gt;I created &lt;code&gt;acls.ldif&lt;/code&gt; and set the contents and loaded it and verified it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano acls.ldif
&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;acls.ldif 
&lt;span class="go"&gt;dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to attrs=userPassword
  by self write
  by anonymous auth
  by dn="cn=admin,dc=acme,dc=internal" write
  by * none
olcAccess: {1}to attrs=shadowLastChange
  by self write
  by * read
olcAccess: {2}to *
  by self write
  by dn="cn=admin,dc=acme,dc=internal" write
  by * read
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ldapmodify &lt;span class="nt"&gt;-Y&lt;/span&gt; EXTERNAL &lt;span class="nt"&gt;-H&lt;/span&gt; ldapi:/// &lt;span class="nt"&gt;-f&lt;/span&gt; acls.ldif
&lt;span class="go"&gt;SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "olcDatabase={1}mdb,cn=config"

&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ldapsearch &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; ldaps://ldap-server.acme.internal &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  -b "dc=acme,dc=internal" "(uid=jsmith)" userPassword
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;extended LDIF
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;LDAPv3
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;base &amp;lt;&lt;span class="nv"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;acme,dc&lt;span class="o"&gt;=&lt;/span&gt;internal&amp;gt; with scope subtree
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;filter: &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;jsmith&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;requesting: userPassword 
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;jsmith, &lt;span class="nb"&gt;users&lt;/span&gt;, acme.internal
&lt;span class="go"&gt;dn: uid=jsmith,ou=users,dc=acme,dc=internal

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;search result
&lt;span class="go"&gt;search: 2
result: 0 Success

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;numResponses: 2
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;numEntries: 1
&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setting up LDAP client
&lt;/h3&gt;

&lt;p&gt;I copied &lt;code&gt;rootCA.pem&lt;/code&gt; to the LDAP client and trusted it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-client:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="go"&gt;rootCA.pem
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-client:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo cp &lt;/span&gt;rootCA.pem /usr/local/share/ca-certificates/acme-rootCA.crt
&lt;span class="go"&gt;[sudo] password for debian: 
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-client:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;update-ca-certificates
&lt;span class="go"&gt;Updating certificates in /etc/ssl/certs...
rehash: warning: skipping ca-certificates.crt, it does not contain exactly one certificate or CRL
&lt;/span&gt;&lt;span class="gp"&gt;1 added, 0 removed;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="go"&gt;Running hooks in /etc/ca-certificates/update.d...
done.
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-client:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I installed System Security Services&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; sssd libpam-sss libnss-sss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I now created the configuration for it, set the permissions and enabled the service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/sssd/sssd.conf
&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /etc/sssd/sssd.conf
&lt;span class="go"&gt;[sssd]
services = nss, pam
config_file_version = 2
domains = acme.internal

[domain/acme.internal]
id_provider = ldap
auth_provider = ldap
ldap_uri = ldaps://ldap-server.acme.internal
ldap_search_base = dc=acme,dc=internal
ldap_tls_cacert = /etc/ssl/certs/ca-certificates.crt
ldap_schema = rfc2307
cache_credentials = true
enumerate = true
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /etc/sssd/sssd.conf
&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; sssd
&lt;span class="go"&gt;Synchronizing state of sssd.service with SysV service script with /usr/lib/systemd/systemd-sysv-install.
Executing: /usr/lib/systemd/systemd-sysv-install enable sssd
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only thing left is automatic home dir creation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;pam-auth-update &lt;span class="nt"&gt;--enable&lt;/span&gt; mkhomedir
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will make sure a home directory is automatically created when john ssh-es into the ldap client.&lt;/p&gt;

&lt;h3&gt;
  
  
  LDAP Client Verification
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;id &lt;/span&gt;jsmith
&lt;span class="go"&gt;uid=10001(jsmith) gid=10001(engineers) groups=10001(engineers)
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;getent passwd jsmith
&lt;span class="go"&gt;jsmith:*:10001:10001:John Smith:/home/jsmith:/bin/bash
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;getent group engineers
&lt;span class="go"&gt;engineers:*:10001:jsmith
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LDAP client can see the users and groups set in the LDAP server.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSH verification
&lt;/h3&gt;

&lt;p&gt;From another desktop, copied &lt;code&gt;rootCA.pem&lt;/code&gt; and trusted it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@debian:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="go"&gt;rootCA.pem
&lt;/span&gt;&lt;span class="gp"&gt;debian@debian:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo cp &lt;/span&gt;rootCA.pem /usr/local/share/ca-certificates/acme-rootCA.crt
&lt;span class="go"&gt;[sudo] password for debian: 
&lt;/span&gt;&lt;span class="gp"&gt;debian@debian:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;update-ca-certificates
&lt;span class="go"&gt;Updating certificates in /etc/ssl/certs...
rehash: warning: skipping ca-certificates.crt, it does not contain exactly one certificate or CRL
&lt;/span&gt;&lt;span class="gp"&gt;1 added, 0 removed;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="go"&gt;Running hooks in /etc/ca-certificates/update.d...
done.
&lt;/span&gt;&lt;span class="gp"&gt;debian@debian:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I tried to ssh login&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@debian:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ssh jsmith@ldap-client.acme.internal
&lt;span class="go"&gt;The authenticity of host 'ldap-client.acme.internal (192.168.57.6)' can't be established.
ED25519 key fingerprint is SHA256:wo67g9IGEfMUrZBC1KzzKlHS1G41PidIUGXZ5kTGmV0.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'ldap-client.acme.internal' (ED25519) to the list of known hosts.
jsmith@ldap-client.acme.internal's password: 
Creating directory '/home/jsmith'.
&lt;/span&gt;&lt;span class="gp"&gt;Linux ldap-client.acme.internal 6.12.73+deb13-amd64 #&lt;/span&gt;1 SMP PREEMPT_DYNAMIC Debian 6.12.73-1 &lt;span class="o"&gt;(&lt;/span&gt;2026-02-17&lt;span class="o"&gt;)&lt;/span&gt; x86_64
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;The programs included with the Debian GNU/Linux system are free software;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
&lt;/span&gt;&lt;span class="gp"&gt;jsmith@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;
&lt;span class="go"&gt;/home/jsmith
&lt;/span&gt;&lt;span class="gp"&gt;jsmith@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked. Also the home dir was automatically created. &lt;/p&gt;

&lt;h3&gt;
  
  
  Stopping anonymous bind and setup a service account for LDAP client
&lt;/h3&gt;

&lt;p&gt;I need a password hash for the service account first. So I ran&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/sbin/slappasswd &lt;span class="nt"&gt;-s&lt;/span&gt; SSSDPass123
&lt;span class="go"&gt;[sudo] password for debian: 
{SSHA}ASFdTu5vaW4YSytPSFTpTc1StjoC+giz
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I put it in service accounts conf file and then load it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano service-accounts.ldif
&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;service-accounts.ldif 
&lt;span class="go"&gt;dn: ou=service-accounts,dc=acme,dc=internal
objectClass: organizationalUnit
ou: service-accounts

dn: cn=sssd,ou=service-accounts,dc=acme,dc=internal
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: sssd
description: Read-only service account for SSSD
userPassword: {SSHA}ASFdTu5vaW4YSytPSFTpTc1StjoC+giz
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ldapadd &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; ldaps://ldap-server.acme.internal &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  -D "cn=admin,dc=acme,dc=internal" -W -f service-accounts.ldif
Enter LDAP Password: 
adding new entry "ou=service-accounts,dc=acme,dc=internal"

adding new entry "cn=sssd,ou=service-accounts,dc=acme,dc=internal"

&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then I create another ACL to make sure I only allow that specific account and disallow anonymous binds&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nano acls2.ldif
&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;acls2.ldif 
&lt;span class="go"&gt;dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to attrs=userPassword
  by self write
  by anonymous auth
  by dn="cn=admin,dc=acme,dc=internal" write
  by * none
olcAccess: {1}to attrs=shadowLastChange
  by self write
  by * read
olcAccess: {2}to *
  by self write
  by dn="cn=admin,dc=acme,dc=internal" write
  by dn="cn=sssd,ou=service-accounts,dc=acme,dc=internal" read
  by * none
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ldapmodify &lt;span class="nt"&gt;-Y&lt;/span&gt; EXTERNAL &lt;span class="nt"&gt;-H&lt;/span&gt; ldapi:/// &lt;span class="nt"&gt;-f&lt;/span&gt; acls2.ldif
&lt;span class="go"&gt;SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "olcDatabase={1}mdb,cn=config"

&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-server:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I set the LDAP client to use those accounts and restart the service&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo cat&lt;/span&gt; /etc/sssd/sssd.conf
&lt;span class="go"&gt;[sssd]
services = nss, pam
config_file_version = 2
domains = acme.internal

[domain/acme.internal]
id_provider = ldap
auth_provider = ldap
ldap_uri = ldaps://ldap-server.acme.internal
ldap_search_base = dc=acme,dc=internal
ldap_tls_cacert = /etc/ssl/certs/ca-certificates.crt
ldap_schema = rfc2307
cache_credentials = true
enumerate = true
ldap_default_bind_dn = cn=sssd,ou=service-accounts,dc=acme,dc=internal
ldap_default_authtok = SSSDPass123
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart sssd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: &lt;code&gt;ldap_default_authtok&lt;/code&gt; is stored in plaintext, but &lt;code&gt;sssd.conf&lt;/code&gt; is locked to &lt;code&gt;root&lt;/code&gt; only via the &lt;code&gt;chmod 600&lt;/code&gt; set earlier, so it is not readable by other users on the system.&lt;/p&gt;
&lt;h3&gt;
  
  
  Verifying again
&lt;/h3&gt;
&lt;/blockquote&gt;

&lt;p&gt;LDAP client records fetching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;id &lt;/span&gt;jsmith
&lt;span class="go"&gt;uid=10001(jsmith) gid=10001(engineers) groups=10001(engineers)
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;getent passwd adoe
&lt;span class="go"&gt;adoe:*:10002:10002:Alice Doe:/home/adoe:/bin/bash
&lt;/span&gt;&lt;span class="gp"&gt;debian@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SSH verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;debian@debian:~/acme-certs$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ssh jsmith@ldap-client.acme.internal
&lt;span class="go"&gt;jsmith@ldap-client.acme.internal's password: 
&lt;/span&gt;&lt;span class="gp"&gt;Linux ldap-client.acme.internal 6.12.73+deb13-amd64 #&lt;/span&gt;1 SMP PREEMPT_DYNAMIC Debian 6.12.73-1 &lt;span class="o"&gt;(&lt;/span&gt;2026-02-17&lt;span class="o"&gt;)&lt;/span&gt; x86_64
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;The programs included with the Debian GNU/Linux system are free software;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Mar 17 11:43:45 2026 from 192.168.57.104
&lt;/span&gt;&lt;span class="gp"&gt;jsmith@ldap-client:~$&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is working. Only the LDAP client can call the LDAP servers and anonymous binds are disabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&gt;

&lt;h3&gt;
  
  
  VBoxManager
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo82iondyw62whu5ivxnp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo82iondyw62whu5ivxnp.png" alt="001" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ALL VMs
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz2ma73awcrl8ph8vrf60.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz2ma73awcrl8ph8vrf60.png" alt="002" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;OpenLDAP is now running as the centralized identity backend for &lt;code&gt;acme.internal&lt;/code&gt;. The directory is secured with LDAPS using the lab's mkcert CA. ACLs enforce least-privilege access - password hashes are not exposed to anyone except the admin, and anonymous binds are disabled entirely. A dedicated read-only service account is the only thing allowed to query the directory, which is what SSSD on &lt;code&gt;ldap-client&lt;/code&gt; uses. Users that exist only in LDAP can SSH into &lt;code&gt;ldap-client&lt;/code&gt; with their home directories created automatically on first login.&lt;/p&gt;

&lt;p&gt;This is the identity foundation for the next part of this series - connecting Keycloak to this LDAP directory as its user backend.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>cybersecurity</category>
      <category>linux</category>
      <category>network</category>
    </item>
    <item>
      <title>Setting up HTTPS on my home lab</title>
      <dc:creator>Shoban Chiddarth</dc:creator>
      <pubDate>Mon, 09 Mar 2026 03:22:10 +0000</pubDate>
      <link>https://dev.to/shobanchiddarth/setting-up-ssl-https-on-my-home-lab-g45</link>
      <guid>https://dev.to/shobanchiddarth/setting-up-ssl-https-on-my-home-lab-g45</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is the third part of my physical network engineering home lab series of blogs. In this blog I will be sharing how I set up SSL on my home lab and then accessed locally hosted services without a warning in the browser.&lt;/p&gt;

&lt;p&gt;And no I am not buying a domain like a lot of YouTubers do, I am going to use &lt;code&gt;mkcert&lt;/code&gt; to manage the SSL certificates and install the certificate on each client device and make HTTPS work with no warning from the browser. I chose &lt;code&gt;mkcert&lt;/code&gt; because manually configuring &lt;code&gt;openssl&lt;/code&gt; certificates and maintaining a certificate hierarchy is tedious for a home lab.&lt;/p&gt;

&lt;p&gt;The goal of enabling HTTPS is to securely access the Pi-hole admin panel, and other locally hosted services in the home lab.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Side note: SSL is deprecated and replaced with TLS but the term SSL certificates is used in slang and I will be going with that in this blog&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Requirements
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;A Linux machine&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;package &lt;code&gt;libnss3-tools&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;libnss3-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;package &lt;code&gt;mkcert&lt;/code&gt;. For Linux Mint you can download via apt&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;mkcert
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;but if it is not available via apt you have to download it from &lt;a href="https://github.com/FiloSottile/mkcert/releases/latest" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Creating &lt;code&gt;rootCA&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the highest in the hierarchy of certificates and we will be copying the root certificate to other devices to make them work with HTTPS.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;On your client machine (not the server), run&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkcert &lt;span class="nt"&gt;-install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;After it is installed, run&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkcert &lt;span class="nt"&gt;-CAROOT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This will print the directory where the root key and the root certificate is stored. Make a note of it.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gray@OMEN-Slim-Gaming-Laptop-16:~/home-lab-tls&lt;span class="nv"&gt;$ &lt;/span&gt;mkcert &lt;span class="nt"&gt;-install&lt;/span&gt;
Created a new &lt;span class="nb"&gt;local &lt;/span&gt;CA 💥
The &lt;span class="nb"&gt;local &lt;/span&gt;CA is now installed &lt;span class="k"&gt;in &lt;/span&gt;the system trust store! ⚡️
The &lt;span class="nb"&gt;local &lt;/span&gt;CA is now installed &lt;span class="k"&gt;in &lt;/span&gt;the Firefox and/or Chrome/Chromium trust store &lt;span class="o"&gt;(&lt;/span&gt;requires browser restart&lt;span class="o"&gt;)!&lt;/span&gt; 🦊

gray@OMEN-Slim-Gaming-Laptop-16:~/home-lab-tls&lt;span class="nv"&gt;$ &lt;/span&gt;mkcert &lt;span class="nt"&gt;-CAROOT&lt;/span&gt;
/home/gray/.local/share/mkcert
gray@OMEN-Slim-Gaming-Laptop-16:~/home-lab-tls&lt;span class="nv"&gt;$ &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating Certificates
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;In a separate directory, run the following command. 2 files will be stored on that current directory which you will need to copy to the server.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkcert domain-name.internal
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;I have created only for &lt;code&gt;pi.hole&lt;/code&gt; because that is the only service I want to secure right now.&lt;br&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gray@OMEN-Slim-Gaming-Laptop-16:~/home-lab-tls&lt;span class="nv"&gt;$ &lt;/span&gt;mkcert pi.hole

Created a new certificate valid &lt;span class="k"&gt;for &lt;/span&gt;the following names 📜
 - &lt;span class="s2"&gt;"pi.hole"&lt;/span&gt;

The certificate is at &lt;span class="s2"&gt;"./pi.hole.pem"&lt;/span&gt; and the key at &lt;span class="s2"&gt;"./pi.hole-key.pem"&lt;/span&gt; ✅

It will expire on 8 June 2028 🗓

gray@OMEN-Slim-Gaming-Laptop-16:~/home-lab-tls&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls
&lt;/span&gt;pi.hole-key.pem  pi.hole.pem
gray@OMEN-Slim-Gaming-Laptop-16:~/home-lab-tls&lt;span class="nv"&gt;$ &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Combine those 2 files along with the &lt;code&gt;rootCA.pem&lt;/code&gt; in this specific order
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat pi.hole-key.pem pi.hole.pem  $(mkcert -CAROOT)/rootCA.pem &amp;gt; pi.hole-combined.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Copy the combined file to the server, into the directory &lt;code&gt;/etc/pihole&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;On the server, make sure the file is owned by user and group &lt;code&gt;pihole&lt;/code&gt; and only the owner can read/write to it.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;pihole:pihole pi.hole-combined.pem                 
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 pi.hole-combined.pem 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pointing the Pi-hole server to use these certificates
&lt;/h3&gt;

&lt;p&gt;Edit this section in &lt;code&gt;/etc/pihole/pihole.toml&lt;/code&gt; as shown&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;  &lt;span class="nn"&gt;[webserver.tls]&lt;/span&gt;
    &lt;span class="c"&gt;# Path to the TLS (SSL) certificate file.&lt;/span&gt;
    &lt;span class="c"&gt;#&lt;/span&gt;
    &lt;span class="c"&gt;# All directories along the path must be readable and accessible by the user running&lt;/span&gt;
    &lt;span class="c"&gt;# FTL (typically 'pihole'). This option is only required when at least one of&lt;/span&gt;
    &lt;span class="c"&gt;# webserver.port is TLS. The file must be in PEM format, and it must have both,&lt;/span&gt;
    &lt;span class="c"&gt;# private key and certificate (the *.pem file created must contain a 'CERTIFICATE'&lt;/span&gt;
    &lt;span class="c"&gt;# section as well as a 'RSA PRIVATE KEY' section).&lt;/span&gt;
    &lt;span class="c"&gt;#&lt;/span&gt;
    &lt;span class="c"&gt;# The *.pem file can be created using `cp server.crt server.pem &amp;amp;&amp;amp; cat server.key &amp;gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;# server.pem` if you have these files instead&lt;/span&gt;
    &lt;span class="c"&gt;#&lt;/span&gt;
    &lt;span class="c"&gt;# Allowed values are:&lt;/span&gt;
    &lt;span class="c"&gt;#     A valid TLS certificate file (*.pem)&lt;/span&gt;
    &lt;span class="py"&gt;cert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/pihole/pi.hole-combined.pem"&lt;/span&gt;

    &lt;span class="c"&gt;# Number of days the automatically generated self-signed TLS/SSL certificate will be&lt;/span&gt;
    &lt;span class="c"&gt;# valid for.&lt;/span&gt;
    &lt;span class="c"&gt;#&lt;/span&gt;
    &lt;span class="c"&gt;# Defaults to 47 days. A minimum of 7 days is enforced.&lt;/span&gt;
    &lt;span class="c"&gt;# Some devices may enforce shorter validity ranges. Note that defining a lower&lt;/span&gt;
    &lt;span class="c"&gt;# validity range may require you to accept the self-signed certificate more often in&lt;/span&gt;
    &lt;span class="c"&gt;# your browser.&lt;/span&gt;
    &lt;span class="c"&gt;# Pi-hole will regenerate certificates it created itself two days prior to expiration.&lt;/span&gt;
    &lt;span class="c"&gt;# If you are using your own certificate, you need to regenerate it yourself. In this&lt;/span&gt;
    &lt;span class="c"&gt;# case, it is advised to set the validity range to 0 days, so that Pi-hole does not&lt;/span&gt;
    &lt;span class="c"&gt;# try to regenerate your certificate. If you set the validity range to 0 days and&lt;/span&gt;
    &lt;span class="c"&gt;# still try to generate a certificate, Pi-hole will set a fixed validity range of&lt;/span&gt;
    &lt;span class="c"&gt;# roughly 30 years for the certificate.&lt;/span&gt;
    &lt;span class="py"&gt;validity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;cert&lt;/code&gt; variable with where you stored the combined certificate, and then restart Pi-hole.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo service pihole-FTL restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Side Note: If &lt;code&gt;nginx&lt;/code&gt; is running, disable it. Pi-hole's FTL has a built-in web server that handles TLS directly - Nginx will conflict with it on port 80/443.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl disable nginx --now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verifying HTTPS on current device
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://pi.hole" rel="noopener noreferrer"&gt;https://pi.hole&lt;/a&gt; in the browser of the device you ran &lt;code&gt;mkcert -install&lt;/code&gt; command on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foag9rbgzvc1i1cjv2p3w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foag9rbgzvc1i1cjv2p3w.png" alt="works" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying it on other devices
&lt;/h2&gt;

&lt;p&gt;Copy the file &lt;code&gt;$(mkcert -CAROOT)/rootCA.pem&lt;/code&gt; to other devices, and import to the device's certificates (instructions vary for each device, Google it) store and then open &lt;code&gt;https://pi.hole&lt;/code&gt; on the browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfjxy3d6tl6l6bm1571s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfjxy3d6tl6l6bm1571s.png" alt="works-android" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The Pi-hole admin panel is now accessible over HTTPS with a trusted certificate - no browser warnings, no exceptions. Any other device on the network can be added by copying the &lt;code&gt;rootCA.pem&lt;/code&gt; and importing it into the system trust store.&lt;/p&gt;

&lt;p&gt;From here, the physical lab has DNS, ad-blocking, and HTTPS in place.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>tls</category>
      <category>security</category>
      <category>encryption</category>
    </item>
    <item>
      <title>The Superior Way to make VMs communicate with each other as well as host, with internet access</title>
      <dc:creator>Shoban Chiddarth</dc:creator>
      <pubDate>Sat, 07 Mar 2026 14:05:19 +0000</pubDate>
      <link>https://dev.to/shobanchiddarth/the-superior-way-to-make-vms-communicate-with-each-other-as-well-as-host-with-internet-access-42m1</link>
      <guid>https://dev.to/shobanchiddarth/the-superior-way-to-make-vms-communicate-with-each-other-as-well-as-host-with-internet-access-42m1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;On December 1, 2025, I made &lt;a href="https://www.linkedin.com/posts/shobanchiddarth_linux-networkengineering-virtualization-activity-7401309251840720896-rgD3" rel="noopener noreferrer"&gt;a LinkedIn post&lt;/a&gt; about setting up inter-VM networking with 3 VMs (a DHCP server, a web server and a client). The full details and &lt;a href="https://shobanchiddarth.github.io/3-VMs-build-log/" rel="noopener noreferrer"&gt;a technical document&lt;/a&gt; are available in the post if you want to check it out.&lt;/p&gt;

&lt;p&gt;The way I set up 3 VMs for inter-VM networking with internet access is&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VMs are set to use "Virtualbox Host Only Ethernet Adapter"&lt;/li&gt;
&lt;li&gt;Internet is shared to the adapter using Windows proprietary ICS ("Internet Connection Sharing") from Control Panel.
It worked in a Windows host (I was temporarily using Windows at that time) because internet sharing is fully handled by Windows, and the inter-VM networking worked with no issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now I am using a Linux host and need to set up inter-VM networking for a project, the same thing is not possible as ICS is a Windows proprietary service. A similar solution will require turning the host into a router and manually managing firewall entries and route tables every time a network segment is created or removed - invasive and hard to cleanly reverse. The other option is attaching two adapters to each VM: one for inter-VM and host communication, another for internet access - which gets complicated across several VMs and can cause issues with desktop environments and network managers.&lt;/p&gt;

&lt;p&gt;There is a superior way to achieve the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VMs can communicate with each other&lt;/li&gt;
&lt;li&gt;Host can communicate with VMs and vice versa&lt;/li&gt;
&lt;li&gt;VMs can access the internet&lt;/li&gt;
&lt;li&gt;Host does not need to be converted to a router&lt;/li&gt;
&lt;li&gt;2 or more adapters do not need to be attached to every single VM&lt;/li&gt;
&lt;li&gt;Nothing is permanently changed on the host that requires manual intervention to revert, everything stays in Virtualbox and goes away when you delete them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The solution is to run pfSense in a VM that acts as the router between network adapters, keeping everything contained inside VirtualBox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqs0k5po82pqyo7fde2hl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqs0k5po82pqyo7fde2hl.png" alt="Network diagram showing pfSense VM with WAN adapter (Bridged/NAT/NAT Network) connected to the internet, and LAN adapter (VBHOEA) connecting to a Host Only Network shared with other VMs" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The setup is simple: install pfSense inside a VM and attach two adapters to it.&lt;/p&gt;

&lt;p&gt;The first adapter connects pfSense to the internet. You can use Bridged, NAT, or NAT Network - all three work as long as the host has internet access. (Bridged gives pfSense a real IP on your physical LAN; NAT and NAT Network are simpler and work without touching your router).&lt;/p&gt;

&lt;p&gt;For more information on VirtualBox virtual networking, see the &lt;a href="https://www.virtualbox.org/manual/ch06.html" rel="noopener noreferrer"&gt;manual&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The second adapter is a VirtualBox Host Only Ethernet Adapter attached to a specific Host Only Network. This becomes the LAN side.&lt;/p&gt;

&lt;p&gt;For every other VM in your lab, attach a single VirtualBox Host Only Ethernet Adapter and select the same Host Only Network you assigned to pfSense's LAN adapter.&lt;/p&gt;

&lt;p&gt;In pfSense, assign the first adapter as WAN and the Host Only Ethernet Adapter as LAN.&lt;/p&gt;

&lt;p&gt;Disable the VirtualBox built-in DHCP server for that Host Only Network, and configure DHCP inside pfSense instead.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Always boot pfSense before your lab VMs&lt;/strong&gt; - it handles DHCP and routing for the entire network.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install pfSense on VirtualBox
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Download the pfSense ISO file from &lt;a href="https://shop.netgate.com/products/netgate-installer" rel="noopener noreferrer"&gt;Netgate Shop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click "New" on Virtual Box and fill in the name of the OS, select the ISO file.&lt;/li&gt;
&lt;li&gt;Select OS: "BSD"; Distro: "FreeBSD"; OS Version: "FreeBSD 64 bit";&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj62d73brlqerza5dsl5l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj62d73brlqerza5dsl5l.png" alt="001" width="799" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select 2GB RAM and 4GB Hard Disk&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdx7c2nc29z45vslbfnf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdx7c2nc29z45vslbfnf.png" alt="002" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click on "Start" and then install the OS. By default, Virtualbox assigns the "NAT" adapter as the only interface. That is fine for now. Select that as the WAN interface in the installation process. For installing the OS, accept defaults for everything else.&lt;/li&gt;
&lt;li&gt;After installation is over, shut the VM down instead of rebooting. &lt;/li&gt;
&lt;li&gt;Go to VM's settings -&amp;gt; Storage and remove the ISO file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxcjxwcx7s0xwt166e1ia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxcjxwcx7s0xwt166e1ia.png" alt="003" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click "Ok" and then boot the VM up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw8n1h4c6j8330nxh62t0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw8n1h4c6j8330nxh62t0.png" alt="004" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If it boots up to this state then it means that it is successful. Now you can reset the admin account and password, then shut it down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring interfaces on pfSense
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to Virtualbox Networks and select "Host Only Networks". Create a new Host Only Network if you do not want to use the default. Note the subnet/CIDR block.&lt;/li&gt;
&lt;li&gt;Go to Machines -&amp;gt; pfSense VM settings -&amp;gt; Network and enable a second adapter, and set it to VirtualBox Host Only Ethernet Adapter and then select the Host Only Network you wish to use it. Make sure to select "Allow All" in promiscuous mode. This allows pfSense to forward traffic on behalf of other VMs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc0oeapt8n8o1zdqai1v1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc0oeapt8n8o1zdqai1v1.png" alt="005" width="799" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click on "OK" and boot it up again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6zvp4koebxrkzwa6b8g3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6zvp4koebxrkzwa6b8g3.png" alt="006" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select "1" to configure interfaces. Then select no for VLANs, assign &lt;code&gt;em0&lt;/code&gt; as WAN interface and then &lt;code&gt;em1&lt;/code&gt; as LAN interface. Then confirm it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhza7silbxj54otaesxvt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhza7silbxj54otaesxvt.png" alt="007" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select 2 to set interface IP addresses. We need a static IP on the LAN interface because pfSense will be our DHCP server. Make sure you are setting up an IP that does not clash with the host, and lies in the same subnet as the VirtualBox Host only network, then choose to enable DHCP server on LAN. (IPv6 is optional).
&amp;gt; Note: To find the host's IP on that network, go to VirtualBox Manager -&amp;gt; Networks -&amp;gt; Host Only Networks and select your network.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0b0kxl1elkt6glyyk7za.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0b0kxl1elkt6glyyk7za.png" alt="008" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fouxyeya8i8bo0uglvznt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fouxyeya8i8bo0uglvznt.png" alt="009" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;After it is done, access the static IP you set via a web browser from the host. Enter the username &lt;code&gt;admin&lt;/code&gt; and the password you set up earlier.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  pfSense web configuration
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Click next on the welcome messages and on the initial configuration, enter the appropriate values as shown in the image. Make sure to set up a valid upstream DNS resolver.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpdc97xk1o4m3lrnhjzvy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpdc97xk1o4m3lrnhjzvy.png" alt="010" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Leave the timeserver as is, and choose your timezone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8g3ycs66xe6e8jvsmd53.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8g3ycs66xe6e8jvsmd53.png" alt="011" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click next and you will be asked to configure WAN and LAN interfaces. Change nothing as you already set this one up.&lt;/li&gt;
&lt;li&gt;Set the admin account password again when it prompts you to and then reload and login.
At this point pfSense is fully operational. Any additional configuration such as firewall rules or DNS overrides can be done from this interface.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  VM Intercommunication and Verification
&lt;/h2&gt;

&lt;p&gt;Create VMs as needed. Make sure to attach a VirtualBox Host Only Ethernet Adapter and select the Host Only Network you set up pfSense in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5tbajvadkqliify1mls7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5tbajvadkqliify1mls7.png" alt="012" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Set up a web server VM with a static IP and a test page to verify host-to-VM communication.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv0igonau8e4kxf9x4jn6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv0igonau8e4kxf9x4jn6.png" alt="013" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Set up a client VM in the same adapter and network, configured as a DHCP client. Verify that it receives an IP from pfSense.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj0rpm5sv4aknxefz19bz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj0rpm5sv4aknxefz19bz.png" alt="014" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the client VM, access the web server using its IP. Verify that VM-to-VM communication works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6lsb7j5nd00yapxeminf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6lsb7j5nd00yapxeminf.png" alt="015" width="799" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Verify internet access from the client VM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fixcfomx2datysip63i35.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fixcfomx2datysip63i35.png" alt="016" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that the client VM has only one interface attached - the Host Only Ethernet Adapter with the relevant Host Only Network selected. Through this single adapter, the VM can reach other VMs on the same network, the host, and the internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The result is a clean, self-contained lab network: VMs can communicate with each other and the host, have full internet access, and the host requires no permanent configuration changes. pfSense handles routing, DHCP, and DNS entirely within VirtualBox.&lt;/p&gt;

&lt;p&gt;This setup works as a solid foundation for any multi-VM lab that needs real network services, making it particularly useful for cybersecurity home labs.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>pfsense</category>
      <category>virtualbox</category>
      <category>network</category>
    </item>
    <item>
      <title>The Superior way to share files over a LAN</title>
      <dc:creator>Shoban Chiddarth</dc:creator>
      <pubDate>Fri, 06 Mar 2026 13:18:02 +0000</pubDate>
      <link>https://dev.to/shobanchiddarth/the-superior-way-to-share-files-over-a-lan-28nb</link>
      <guid>https://dev.to/shobanchiddarth/the-superior-way-to-share-files-over-a-lan-28nb</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/ShobanChiddarth/openssh-server" rel="noopener noreferrer"&gt;GitHub: @ShobanChiddarth/openssh-server&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;You need to send a file to another machine on your network. It should be a solved problem. It is not.&lt;/p&gt;

&lt;p&gt;Every method people actually reach for is either insecure, annoying to set up, or leaves something running on your machine long after you needed it. This post is about the one approach that gets all three things right - and it takes about two minutes to set up.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You're Probably Doing (And Why It's Bad)
&lt;/h2&gt;

&lt;p&gt;Let's go through the common options honestly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Python HTTP server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everyone knows this one. It works, and that's roughly where the positives end. Traffic is completely unencrypted. Anyone on the same network can see every byte in transit. There's no authentication - anyone who can reach the port can download anything you're serving.&lt;/p&gt;

&lt;h3&gt;
  
  
  Netcat
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# sender&lt;/span&gt;
nc &lt;span class="nt"&gt;-l&lt;/span&gt; 9999 &amp;lt; file.txt

&lt;span class="c"&gt;# receiver&lt;/span&gt;
nc 192.168.1.x 9999 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; file.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same problem. Cleartext, no auth, one shot if you're lucky. It's basically a raw pipe over TCP dressed up as a tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing openssh-server on your machine
&lt;/h3&gt;

&lt;p&gt;This one at least solves the encryption problem. But now you've installed a permanent service on your host OS. &lt;code&gt;sshd&lt;/code&gt; is running all the time, listening on port 22, attached to your actual system - its users, its filesystem, its everything. You wanted to share one folder for five minutes. You now have a permanently expanded attack surface.&lt;/p&gt;

&lt;p&gt;And if you give the other person SSH credentials? They have a shell into your real machine. Full access. That's a lot of trust for a quick file transfer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Samba / NFS
&lt;/h3&gt;

&lt;p&gt;Heavy to configure, plenty of surface area, and definitely not something you spin up for a quick transfer and tear down cleanly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Actual Problem
&lt;/h2&gt;

&lt;p&gt;The core issue with all of these is that they either skip encryption entirely, or they go too far in the other direction and give the other party more access than a file transfer requires.&lt;/p&gt;

&lt;p&gt;What you actually want is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Encrypted transfer&lt;/li&gt;
&lt;li&gt;Auth (so not just anyone on the LAN can connect)&lt;/li&gt;
&lt;li&gt;Access scoped to exactly one directory, nothing else&lt;/li&gt;
&lt;li&gt;Zero footprint when you're done&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Solution: OpenSSH Inside a Docker Container
&lt;/h2&gt;

&lt;p&gt;Here's the idea: run an SSH server inside a Docker container. Mount only the specific directory you want to share into that container. The other party connects over SSH, lands in that directory, and that's all they can see - not your home folder, not your system files, not anything else on your machine. When the transfer is done, you stop the container and it's completely gone.&lt;/p&gt;

&lt;p&gt;The host OS never has &lt;code&gt;sshd&lt;/code&gt; installed. No permanent service. No lingering open port. The container is the entire blast radius.&lt;/p&gt;

&lt;p&gt;I built and published this as a ready-to-use Docker image with two variants: one based on Alpine Linux (smaller, recommended) and one based on Ubuntu Noble. The image handles user creation, password setup, and public key injection entirely through environment variables.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;When the container starts, the entrypoint script does the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads &lt;code&gt;SSH_USER&lt;/code&gt;, &lt;code&gt;SSH_PASSWORD&lt;/code&gt;, and &lt;code&gt;SSH_PUBLIC_KEY&lt;/code&gt; from environment variables&lt;/li&gt;
&lt;li&gt;Creates a low-privilege user inside the container with those credentials&lt;/li&gt;
&lt;li&gt;Writes the public key to that user's &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Starts &lt;code&gt;sshd&lt;/code&gt; in the foreground&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The user's home directory inside the container is whatever you bind-mount from your host. They can read and write files there. That's the full extent of what they can touch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-Step: Running It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Docker installed and running. That's it.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Pull the image
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull shobanchiddarth/openssh-server:alpine-1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you prefer the Ubuntu variant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull shobanchiddarth/openssh-server:ubuntu-noble-1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Create the directory you want to share
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/myshare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put whatever files you want to transfer in here, or leave it empty for the other party to upload into.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Set up your environment variables
&lt;/h3&gt;

&lt;p&gt;Grab the &lt;code&gt;sample.env&lt;/code&gt; from the repo and copy it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;sample.env .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SSH_USER=USERNAME
SSH_PASSWORD=PASSWORD
SSH_PUBLIC_KEY=ssh-ed25519 AAAA... you@yourmachine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SSH_PUBLIC_KEY&lt;/code&gt; is optional in practice - if the other party doesn't have a key pair, they'll just use the password. But public key is always preferable.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Run the container
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; myshare &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 0.0.0.0:2222:22 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env-file&lt;/span&gt; .env &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /tmp/myshare:/home/USERNAME &lt;span class="se"&gt;\&lt;/span&gt;
  shobanchiddarth/openssh-server:alpine-1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-v&lt;/code&gt; flag is the key part. &lt;code&gt;/tmp/myshare&lt;/code&gt; is the folder on your machine. &lt;code&gt;/home/USERNAME&lt;/code&gt; is where the SSH user lands inside the container. Those two are linked - anything written to one appears in the other. The username in the path should match &lt;code&gt;SSH_USER&lt;/code&gt; in your &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Your LAN IP is whatever &lt;code&gt;ip a&lt;/code&gt; (Linux) or &lt;code&gt;ipconfig&lt;/code&gt; (Windows) shows for your network interface - typically something like &lt;code&gt;192.168.1.x&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Transfer files from the other machine
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Upload a file&lt;/span&gt;
scp &lt;span class="nt"&gt;-P&lt;/span&gt; 2222 /path/to/file.txt USERNAME@192.168.1.x:~/

&lt;span class="c"&gt;# Download a file&lt;/span&gt;
scp &lt;span class="nt"&gt;-P&lt;/span&gt; 2222 USERNAME@192.168.1.x:~/file.txt ./

&lt;span class="c"&gt;# Interactive session&lt;/span&gt;
sftp &lt;span class="nt"&gt;-P&lt;/span&gt; 2222 USERNAME@192.168.1.x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works from Linux, macOS, and Windows (via PowerShell with OpenSSH, WSL, or Git Bash - all ship with &lt;code&gt;scp&lt;/code&gt; and &lt;code&gt;sftp&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Tear it down
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stop myshare
docker &lt;span class="nb"&gt;rm &lt;/span&gt;myshare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port closed. No service running. Nothing left behind.&lt;/p&gt;




&lt;h2&gt;
  
  
  Windows Host Note
&lt;/h2&gt;

&lt;p&gt;If you're running Docker Desktop on Windows, the volume mount path uses Windows-style paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;myshare&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0.0.0.0:2222:22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\Users\YourName\myshare:/home/USERNAME&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;shobanchiddarth/openssh-server:alpine-1.0.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else - the &lt;code&gt;scp&lt;/code&gt;/&lt;code&gt;sftp&lt;/code&gt; commands from the client side - is identical.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security Properties at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Encryption&lt;/td&gt;
&lt;td&gt;SSH - all traffic encrypted in transit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;Password + public key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host filesystem&lt;/td&gt;
&lt;td&gt;Completely inaccessible from inside the container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shell access&lt;/td&gt;
&lt;td&gt;Scoped to the container only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host SSH daemon&lt;/td&gt;
&lt;td&gt;Never installed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistence after use&lt;/td&gt;
&lt;td&gt;Zero - stop the container, it's gone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One thing worth being explicit about: bind the port to &lt;code&gt;0.0.0.0&lt;/code&gt; only on trusted networks. That makes the container reachable on all your local interfaces. If you want to lock it down to a specific interface, replace &lt;code&gt;0.0.0.0&lt;/code&gt; with that interface's IP (e.g., &lt;code&gt;192.168.1.5:2222:22&lt;/code&gt;). Either way, don't forward port 2222 through your router - this is a LAN tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Is Better
&lt;/h2&gt;

&lt;p&gt;The Python HTTP server is convenient. This is only marginally less convenient - one &lt;code&gt;docker run&lt;/code&gt; command instead of one &lt;code&gt;python3&lt;/code&gt; command - and in exchange you get encryption, authentication, proper access scoping, and a completely clean teardown.&lt;/p&gt;

&lt;p&gt;If you have Docker, there's no good reason to reach for the HTTP server anymore.&lt;/p&gt;

&lt;p&gt;The repo has both Alpine and Ubuntu variants, a &lt;code&gt;sample.env&lt;/code&gt; template, and full usage documentation. Pull it, try it, and stop sharing files over cleartext.&lt;/p&gt;




&lt;h2&gt;
  
  
  The &lt;code&gt;known_hosts&lt;/code&gt; problem
&lt;/h2&gt;

&lt;p&gt;Let's say you want your friend to share a file to you. So you ask him his public ssh key and spin up a container with port and volume mapped. And then he tries to ssh into your container. Then he gets hit with this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh test@192.168.1.2
The authenticity of host '192.168.1.2 (192.168.1.2)' can't be established.
ED25519 key fingerprint is SHA256:9d8pzTNQ1vLi2laKIqHnzOG+QdgNv8bDQVz67sNJi1E.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the unknown host warning, it is what ssh usually does to make sure you are accessing the machine you think you are accessing by showing the host key of the machine. So since your friend knows he is accessing your machine and trusts is, he types yes and then the host key gets added to &lt;code&gt;~/.ssh/known_hosts&lt;/code&gt; file. He then finishes the file sharing work and then closes the connection so you stop the container and delete it. Some time later if he wants to do another file sharing work, you spin up another container and if you are in the same LAN with the same private IP, ssh client on his machine sees your host with a different ssh host key when he is trying to access it. So it prints a message like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:s9QoffY4cHer0u66A9s57WzN3/ufOfCBGTq/v9fhr5w.
Please contact your system administrator.
Add correct host key in /home/mint/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /home/mint/.ssh/known_hosts:3
  remove with:
  ssh-keygen -f '/home/mint/.ssh/known_hosts' -R '[192.168.56.1]:2222'
Host key for [192.168.56.1]:2222 has changed and you have requested strict checking.
Host key verification failed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this can be ignored since it is true that the host key has changed and he has to remove the old known hosts entry with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh-keygen -f '/home/mint/.ssh/known_hosts' -R '[192.168.56.1]:2222'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as said in that message. If it is unsure whether or not &lt;strong&gt;SOMEONE&lt;/strong&gt; is doing &lt;strong&gt;SOMETHING NASTY&lt;/strong&gt; you should send your host key to your friend and ask him to compare. You can do it by&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec -it myshare /bin/sh # or /bin/bash if ubuntu
cd /etc/ssh
ls *host*.pub # to see the host public key names
ssh-keygen -lf ssh_host_ecdsa_key.pub 
ssh-keygen -lf ssh_host_ed25519_key.pub 
ssh-keygen -lf ssh_host_rsa_key.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now send the output of the last 3 commands to him and tell him to enter the fingerprint of the algorithm (&lt;code&gt;ecdsa&lt;/code&gt; or &lt;code&gt;ed25519&lt;/code&gt; or &lt;code&gt;rsa&lt;/code&gt;) he is seeing in his screen and if it matches the host will be added to &lt;code&gt;known_hosts&lt;/code&gt; (with the current host public key, so he will have to remove it later by following the above instructions) and the connection will happen.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/ShobanChiddarth/openssh-server" rel="noopener noreferrer"&gt;GitHub: @ShobanChiddarth/openssh-server&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>network</category>
      <category>security</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Setting Up Pi-hole as a Custom DNS Server on my Home Lab</title>
      <dc:creator>Shoban Chiddarth</dc:creator>
      <pubDate>Wed, 04 Mar 2026 14:14:02 +0000</pubDate>
      <link>https://dev.to/shobanchiddarth/setting-up-pi-hole-as-a-custom-dns-server-on-my-home-lab-4jd7</link>
      <guid>https://dev.to/shobanchiddarth/setting-up-pi-hole-as-a-custom-dns-server-on-my-home-lab-4jd7</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I'm building out a home lab and this post covers setting up Pi-hole as a custom DNS server so that internal hosts are reachable by name rather than IP address.&lt;/p&gt;

&lt;p&gt;This is part of my &lt;a href="https://dev.to/shobanchiddarth/series/36267"&gt;Physical Network Engineering Home Lab series&lt;/a&gt; where I document building and configuring a physical home lab from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason for Choosing Pi-Hole as a custom DNS server
&lt;/h2&gt;

&lt;p&gt;I needed a local DNS server where I could register internal hostnames and have them resolve across the network — so my server is reachable as &lt;code&gt;server.local&lt;/code&gt; rather than &lt;code&gt;192.168.1.2&lt;/code&gt;. This becomes especially important as I add more services to the lab.&lt;/p&gt;

&lt;p&gt;Pi-hole fits this requirement well. It is primarily a network-level DNS server that supports custom local DNS records, and it comes with a clean web interface for managing them. The fact that it also blocks ads and trackers at the DNS level for every device on the network is a useful side effect — the &lt;a href="https://web.archive.org/web/20221225170743/https://www.ic3.gov/Media/Y2022/PSA221221" rel="noopener noreferrer"&gt;FBI has even recommended ad blockers as protection against malvertising scams&lt;/a&gt;, so having this enforced network-wide without configuring each device individually is a reasonable security baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Pi-Hole
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pre-requisites
&lt;/h3&gt;

&lt;p&gt;Refer my &lt;a href="https://dev.to/shobanchiddarth/my-network-engineering-home-lab-setup-2j8g"&gt;Physical Network Engineering Home Lab&lt;/a&gt; to understand what I already have and working with.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing Pi-Hole software on my server
&lt;/h3&gt;

&lt;p&gt;My server runs headless Linux Mint, which is Ubuntu-based and &lt;a href="https://docs.pi-hole.net/main/prerequisites/#supported-operating-systems" rel="noopener noreferrer"&gt;officially supported&lt;/a&gt; for Pi-hole.&lt;/p&gt;

&lt;p&gt;So I am going to &lt;a href="https://docs.pi-hole.net/main/basic-install/#alternative-2-manually-download-the-installer-and-run" rel="noopener noreferrer"&gt;manually download the installer and run&lt;/a&gt; it on my server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget &lt;span class="nt"&gt;-O&lt;/span&gt; basic-install.sh https://install.pi-hole.net
&lt;span class="nb"&gt;sudo &lt;/span&gt;bash basic-install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The usual install process begins, and I get asked about choosing upstream DNS server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzufb2gc5orb8e61ed5c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzufb2gc5orb8e61ed5c.png" alt="001" width="563" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I went with cloudflare, selected the default ad blocklist, enabled query logging, selected "show everything" because I am the only one using this home lab so there are no privacy issues, and waited for it to finish installing. &lt;/p&gt;

&lt;p&gt;The installation is now complete and I get this popup&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7p49wl0uljpexffcjzm0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7p49wl0uljpexffcjzm0.png" alt="002" width="579" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Post-installation
&lt;/h3&gt;

&lt;p&gt;Pi-hole installs a web interface under &lt;code&gt;/var/www/html/admin&lt;/code&gt;. I confirmed it was there after installation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftpgquzjt5593mqrf8834.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftpgquzjt5593mqrf8834.png" alt="003" width="544" height="57"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I visited my server's IP &lt;code&gt;192.168.1.2&lt;/code&gt; on the browser and it worked fine, like usually.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsiaeljiqn6pmwkgdzmsh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsiaeljiqn6pmwkgdzmsh.png" alt="004" width="787" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I tried to access the admin panel at &lt;code&gt;192.168.1.2/admin&lt;/code&gt; and got a 403 error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foep21njn1b8xlzib0kxz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foep21njn1b8xlzib0kxz.png" alt="005" width="800" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I remembered the admin panel works only over https. So I tried visiting &lt;code&gt;https://192.168.1.2/admin&lt;/code&gt; and it never worked. So I came back to the terminal where I have ssh-ed into the server and ran &lt;code&gt;ss -tulnp | grep :443&lt;/code&gt; to check if the service was running, and the service was indeed running on &lt;code&gt;0.0.0.0&lt;/code&gt; which means it is supposed to be accessible from the other hosts. That is when I remembered I enabled ufw and had to allow certain ports in the firewall.&lt;/p&gt;

&lt;p&gt;I referred the &lt;a href="https://docs.pi-hole.net/main/prerequisites/#ufw" rel="noopener noreferrer"&gt;pi-hole docs for the ufw commands&lt;/a&gt; and ran the following snippet in the terminal&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 53/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 53/udp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 67/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 67/udp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 123/udp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 546:547/udp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Future me here, I just realised I don't need to allow port 67 as I did not setup DHCP server on the pi hole).&lt;/p&gt;

&lt;p&gt;Now that I have allowed the ports, I tried visiting &lt;code&gt;https://192.168.1.2/admin/&lt;/code&gt; and got this warning, which I will be taking care of in the next part of my home lab series where I properly configure SSL and get HTTPS on browsers with no warning.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fepls5eootl99v4y1zpss.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fepls5eootl99v4y1zpss.png" alt="006" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I clicked on "Proceed" and got the pi-hole admin login page, then I entered my admin password I got from the "Installation over" popup and logged in. Now I get this beautiful dashboard.&lt;/p&gt;

&lt;p&gt;Note: You can change the default password by running &lt;code&gt;pihole setpassword&lt;/code&gt; on the pi hole server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqy0ygtps11ymtfe68ks1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqy0ygtps11ymtfe68ks1.png" alt="007" width="800" height="689"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pointing Router's DHCP to use pi-hole as DNS
&lt;/h2&gt;

&lt;p&gt;I opened my Tenda router's login page via its IP, went to "Internet Settings" and set the DHCP server to &lt;code&gt;192.168.1.2&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqlpiskbrpyibdtchyps.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqlpiskbrpyibdtchyps.png" alt="008" width="799" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I went into System Settings -&amp;gt; LAN settings and did the same&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkvt9v9e7ytuoh11fg687.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkvt9v9e7ytuoh11fg687.png" alt="009" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To refresh the DNS server config from the DHCP configuration, I manually turned off the wifi from my laptop and turned it on again.&lt;/p&gt;

&lt;p&gt;The DNS server shows up as &lt;code&gt;192.168.1.2&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faagu9blai2in9u9eiqab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faagu9blai2in9u9eiqab.png" alt="010" width="800" height="719"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying Ad blocking at Network level
&lt;/h2&gt;

&lt;p&gt;I checked if the DNS service was working, for both allowed and blocked hosts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gray@OMEN-Slim-Gaming-Laptop-16:~&lt;span class="nv"&gt;$ &lt;/span&gt;nslookup google.com
Server:     127.0.0.53
Address:    127.0.0.53#53

Non-authoritative answer:
Name:   google.com
Address: 142.251.222.78
Name:   google.com
Address: 2404:6800:4009:80b::200e

gray@OMEN-Slim-Gaming-Laptop-16:~&lt;span class="nv"&gt;$ &lt;/span&gt;nslookup ad-assets.futurecdn.net 
Server:     127.0.0.53
Address:    127.0.0.53#53

Non-authoritative answer:
Name:   ad-assets.futurecdn.net
Address: 0.0.0.0
Name:   ad-assets.futurecdn.net
Address: ::

gray@OMEN-Slim-Gaming-Laptop-16:~&lt;span class="nv"&gt;$ &lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works perfectly fine. Ads are blocked in the network level.&lt;/p&gt;

&lt;p&gt;Side note: If you are using Cloudflare WARP for privacy, all your internet traffic, including DNS queries are routed through Cloudflare's servers which means the DNS lookup never reaches your Pi-hole and the advertisements domains will be rendered fine. So if you want to access your home lab's hosts using DNS resolution from pi hole or any other DNS server or network level ad blocking for devices where cloudflare warp is not available, you must temporarily turn off Cloudflare WARP. &lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Custom DNS records
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pi.hole&lt;/code&gt; is a custom DNS entry in pi-hole by default and it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2404ii5yz0xs2ouskds.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2404ii5yz0xs2ouskds.png" alt="011" width="800" height="683"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I want to add my own DNS records and check them, which is the entire purpose of this project. So I wanted another host with static IP on the same LAN. &lt;/p&gt;

&lt;h3&gt;
  
  
  Virtualbox VM with static IP in the LAN
&lt;/h3&gt;

&lt;p&gt;I cloned an existing Mint VM, selected to create random MAC addresses for each interface and set it's network adapter to "Bridged" and bridged it with the real interface I use to connect to the LAN, and set "Promiscuous Mode" to allow all. For more information on Virtualbox networking see &lt;a href="https://www.virtualbox.org/manual/ch06.html" rel="noopener noreferrer"&gt;Virtualbox  Manual - Chapter 6 : Virtual Networking&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When I booted it up, it had DHCP by default.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F886uohc03fgu92fls5t2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F886uohc03fgu92fls5t2.png" alt="012" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I went to settings and changed it to static IP &lt;code&gt;192.168.1.23&lt;/code&gt; and turned the interface off and turned it on again and the static IP is available and accessible from the host as well as the server. I also turned off automatic DNS as DHCP will be disabled and manually added &lt;code&gt;192.168.1.2&lt;/code&gt; as the DNS server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feocebz683appo5ngytf4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feocebz683appo5ngytf4.png" alt="013" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsop2co5gy1ijx1da34u5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsop2co5gy1ijx1da34u5.png" alt="014" width="654" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a custom DNS record in pi hole web GUI
&lt;/h3&gt;

&lt;p&gt;I went to Pi hole admin page -&amp;gt; settings -&amp;gt; Local DNS records and added this entry&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft7o2p7tx44bsw1ooqqi2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft7o2p7tx44bsw1ooqqi2.png" alt="015" width="800" height="675"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Side Note: I want this to be temporary that is why I didn't bind DHCP reservation to MAC address in the router.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Custom DNS record
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffhnf7vo3140b4to3kqyf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffhnf7vo3140b4to3kqyf.png" alt="016" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DNS query works from both my laptop and the server. So I set up a nginx web server in the VM like usually&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt-get update
sudo apt-get install nginx -y
sudo systemctl enable nginx --now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And edited the default &lt;code&gt;index.html&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Nginx web page as well as pinging is working from my laptop&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xoptnn1o1edbpcphvxt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xoptnn1o1edbpcphvxt.png" alt="017" width="799" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;as well as the server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxidvv8hqp8101syq9sr5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxidvv8hqp8101syq9sr5.png" alt="018" width="778" height="761"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Changing default website on the pi-hole DNS server (not required)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0grzdjwa7sox7po37h1s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0grzdjwa7sox7po37h1s.png" alt="019" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the new website hosted on the root folder of the DNS server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minor Issues encountered
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Cloudflare WARP conflict (as mentioned above)&lt;/li&gt;
&lt;li&gt;Do not use a custom domain with TLD as &lt;code&gt;.local&lt;/code&gt; because all &lt;code&gt;.local&lt;/code&gt; domains are first processed through the host before sending it anywhere else and it is used for &lt;a href="https://en.wikipedia.org/wiki/Multicast_DNS" rel="noopener noreferrer"&gt;mDNS&lt;/a&gt; (multicast DNS, for when hosts need to discover each other without a central DNS server)&lt;/li&gt;
&lt;li&gt;Make sure to allow the required ports in your firewall as mentioned above&lt;/li&gt;
&lt;li&gt;Every client that is connected to the LAN and is using DHCP has to refresh by disconnecting and reconnecting for this update to work.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  CONCLUSION
&lt;/h2&gt;

&lt;p&gt;I have successfully setup pi-hole on an Ubuntu-based server for network wide ad blocking as well as local DNS resolution. &lt;/p&gt;

&lt;p&gt;After this, I am going to set up fully local SSL for HTTPS (without browser warnings) on my physical network engineering home lab. I will also document setting up pi-hole in docker as well as a Virtualbox VM as a portable alternative when this pi-hole server is inaccessible.&lt;/p&gt;

</description>
      <category>pihole</category>
      <category>privacy</category>
      <category>linux</category>
      <category>network</category>
    </item>
    <item>
      <title>How to clone all GitHub repos of multiple users at once?</title>
      <dc:creator>Shoban Chiddarth</dc:creator>
      <pubDate>Thu, 26 Feb 2026 15:58:52 +0000</pubDate>
      <link>https://dev.to/shobanchiddarth/how-to-clone-all-github-repos-of-multiple-users-at-once-36bn</link>
      <guid>https://dev.to/shobanchiddarth/how-to-clone-all-github-repos-of-multiple-users-at-once-36bn</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/ShobanChiddarth/panic-clone" rel="noopener noreferrer"&gt;Repo Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This script helps you clone all repositories of a given list of GitHub users (including organizations) at once, instead of cloning them manually one by one.&lt;/p&gt;

&lt;p&gt;I built it after running into the need to archive and collect repositories from multiple accounts efficiently. The goal was simple: automate bulk cloning in a clean, repeatable way.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Accepts multiple GitHub usernames&lt;/li&gt;
&lt;li&gt;Fetches all accessible repositories via the GitHub API&lt;/li&gt;
&lt;li&gt;Clones them into a structured local directory&lt;/li&gt;
&lt;li&gt;Supports shallow clone (&lt;code&gt;--depth 1&lt;/code&gt;) or full history&lt;/li&gt;
&lt;li&gt;Skips repositories that are inaccessible or already cloned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each user gets their own folder inside the target directory.&lt;/p&gt;




&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.12+&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git&lt;/code&gt; installed and available in PATH&lt;/li&gt;
&lt;li&gt;A GitHub Personal Access Token with &lt;strong&gt;read access to repository contents&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Generate a Personal Access Token:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to GitHub → Settings → Developer settings → Personal access tokens&lt;/li&gt;
&lt;li&gt;Create a token with repository read access&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copy &lt;code&gt;sample.env&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt;  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Replace the placeholder token value with your actual token  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install dependencies:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python github-main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll be prompted for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A space-separated list of GitHub usernames&lt;/li&gt;
&lt;li&gt;A target directory (default: &lt;code&gt;mass_clone_output&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Clone depth (&lt;code&gt;None&lt;/code&gt; for full history or &lt;code&gt;1&lt;/code&gt; for latest commit only)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The script validates usernames, fetches repositories, and clones them sequentially with live &lt;code&gt;git clone&lt;/code&gt; output shown in the terminal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repository
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ShobanChiddarth/panic-clone" rel="noopener noreferrer"&gt;GitHub Link&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://archive.softwareheritage.org/browse/origin/directory/?origin_url=https://github.com/ShobanChiddarth/panic-clone&amp;amp;visit_type=git" rel="noopener noreferrer"&gt;SoftwareHeritage Archive&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>git</category>
      <category>github</category>
    </item>
    <item>
      <title>How to Enable MAC Address Randomisation in Linux Desktop</title>
      <dc:creator>Shoban Chiddarth</dc:creator>
      <pubDate>Sat, 14 Feb 2026 17:01:13 +0000</pubDate>
      <link>https://dev.to/shobanchiddarth/how-to-enable-mac-address-randomisation-in-linux-desktop-4g2k</link>
      <guid>https://dev.to/shobanchiddarth/how-to-enable-mac-address-randomisation-in-linux-desktop-4g2k</guid>
      <description>&lt;p&gt;Link to Project: &lt;a href="https://github.com/ShobanChiddarth/randomised_mac_linux" rel="noopener noreferrer"&gt;https://github.com/ShobanChiddarth/randomised_mac_linux&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Persistent MAC Address Is a Problem
&lt;/h2&gt;

&lt;p&gt;A persistent hardware address allows networks to consistently identify the same device across sessions. On:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public Wi-Fi&lt;/li&gt;
&lt;li&gt;Campus networks&lt;/li&gt;
&lt;li&gt;Enterprise environments&lt;/li&gt;
&lt;li&gt;Captive portals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;this makes long-term tracking trivial, even if your IP address changes.&lt;/p&gt;

&lt;p&gt;Many Linux desktops still use the permanent hardware address by default, which means your device can be passively identified every time it reconnects to a network.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This Project Solves It
&lt;/h2&gt;

&lt;p&gt;This project automates MAC address randomisation at boot.&lt;/p&gt;

&lt;p&gt;On every system startup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The interface is brought down&lt;/li&gt;
&lt;li&gt;A new locally administered MAC address is generated&lt;/li&gt;
&lt;li&gt;The interface is brought back up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;systemd&lt;/code&gt; service ensures this runs automatically, requiring no manual action after setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;When you install &lt;code&gt;macchanger&lt;/code&gt;, you may get a prompt to enable regular changing of MAC addresses. You should select "no" because we wan't complete control over when and how the MAC address randomisation is scheduled.&lt;/p&gt;

&lt;h4&gt;
  
  
  Debian based
&lt;/h4&gt;

&lt;p&gt;Save this script and run it as &lt;code&gt;sudo&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;macchanger git &lt;span class="nt"&gt;-y&lt;/span&gt;
git clone https://github.com/ShobanChiddarth/randomised_mac_linux.git
&lt;span class="nb"&gt;cd &lt;/span&gt;randomised_mac_linux
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ./randomise_MACs.sh
&lt;span class="nb"&gt;sudo cp &lt;/span&gt;randomise_MACs.sh /usr/bin/
&lt;span class="nb"&gt;sudo cp &lt;/span&gt;randomise_MACs.service /etc/systemd/system/
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;randomise_MACs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your MAC addresses of real network interfaces will automatically change to a random value on each boot. To verify it, you can run &lt;code&gt;ifconfig&lt;/code&gt; to show MAC addresses before and after running &lt;code&gt;sudo systemctl start randomise_MACs&lt;/code&gt; to compare values.&lt;/p&gt;

&lt;h4&gt;
  
  
  Other distros
&lt;/h4&gt;

&lt;p&gt;Visit &lt;a href="https://github.com/ShobanChiddarth/randomised_mac_linux" rel="noopener noreferrer"&gt;the GitHub repo&lt;/a&gt; and google your distro specific command for each command listed.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>cybersecurity</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
