<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[HomelabForge: Production DevOps for Homelabs]]></title><description><![CDATA[Production-grade DevOps for homelabs and small teams: Terraform, Ansible, Proxmox, Kubernetes, CI/CD and security hardening. Reproducible guides with code & diagrams.]]></description><link>https://blog.homelabforge.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1762114517773/9da11927-5653-4e45-9593-85730f3d6998.png</url><title>HomelabForge: Production DevOps for Homelabs</title><link>https://blog.homelabforge.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 24 Apr 2026 19:38:15 GMT</lastBuildDate><atom:link href="https://blog.homelabforge.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Harden PAM on Linux: pwquality, faillock and MFA (with Ansible + Molecule)]]></title><description><![CDATA[A practical, reproducible guide for RHEL/Rocky/Alma and Debian/Ubuntu, with Ansible + Molecule examples.

TL;DR: Secure PAM with pam_pwquality (strong passwords), pam_faillock (brute-force protection)]]></description><link>https://blog.homelabforge.dev/harden-pam-on-linux-pwquality-faillock-and-mfa-with-ansible-molecule</link><guid isPermaLink="true">https://blog.homelabforge.dev/harden-pam-on-linux-pwquality-faillock-and-mfa-with-ansible-molecule</guid><category><![CDATA[Linux]]></category><category><![CDATA[Security]]></category><category><![CDATA[ansible]]></category><category><![CDATA[RHEL]]></category><category><![CDATA[debian]]></category><category><![CDATA[Ubuntu]]></category><category><![CDATA[pam]]></category><category><![CDATA[DevSecOps]]></category><dc:creator><![CDATA[Miguel Alpañez Alcalde]]></dc:creator><pubDate>Thu, 05 Mar 2026 09:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/61055c0f33a2a43971a2d1d3/3a6f6149-cdcb-4f8a-a4b3-b9092ec9dc35.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>A practical, reproducible guide for RHEL/Rocky/Alma and Debian/Ubuntu, with Ansible + Molecule examples.</strong></p>
<blockquote>
<p><strong>TL;DR:</strong> Secure PAM with <code>pam_pwquality</code> (strong passwords), <code>pam_faillock</code> (brute-force protection), U2F/YubiKey MFA where it makes sense, and — most importantly — <strong>put everything in code</strong> with Ansible + Molecule. Validate in CI/CD and sleep better at night.</p>
</blockquote>
<hr />
<h2>Why does PAM still matter?</h2>
<p>PAM is Linux's <strong>authentication layer</strong>. A wrong order in the stack can break SSH/<code>sudo</code> or allow weak passwords. With <strong>a few well-placed pieces</strong> and <strong>automation</strong>, you gain security, predictability, and the ability to audit every change.</p>
<hr />
<h2>What you will implement</h2>
<ol>
<li><p><strong>Password complexity</strong> with <code>pam_pwquality</code> using <code>/etc/security/pwquality.conf</code>.</p>
</li>
<li><p><strong>Account lockout on failed attempts</strong> with <code>pam_faillock</code> and <code>/etc/security/faillock.conf</code>.</p>
</li>
<li><p><strong>MFA with YubiKey (U2F/FIDO2)</strong> via <code>pam_u2f</code> (recommended for <code>sudo</code>).</p>
</li>
<li><p><strong>Traceability</strong> with <code>auditd</code> monitoring PAM/SSH/sudo.</p>
</li>
</ol>
<blockquote>
<p>🔐 <strong>Principles</strong>: least privilege, declarative configuration, test before prod, and documented <strong>break-glass</strong> procedure.</p>
</blockquote>
<hr />
<h2>Multi-distro matrix (avoid manual editing)</h2>
<ul>
<li><p><strong>RHEL/derivatives (Rocky/Alma):</strong> do not edit <code>system-auth</code>/<code>password-auth</code> directly. Use <code>authselect</code> to enable <code>with-faillock</code> and <code>with-pwquality</code> and apply changes.</p>
</li>
<li><p><strong>Debian/Ubuntu:</strong> do not edit <code>common-*</code> directly. Use <code>pam-auth-update</code> with profiles in <code>/usr/share/pam-configs/</code> (enable/disable via the tool).</p>
</li>
</ul>
<hr />
<h2>Flow diagram (Mermaid)</h2>
<img src="https://cdn.hashnode.com/uploads/covers/61055c0f33a2a43971a2d1d3/c9a5b839-7115-41e0-be82-26bcc5cbc0b0.png" alt="PAM Sequence Diagram" style="display:block;margin:0 auto" />

<hr />
<h2>Step by step (fast and safe)</h2>
<h3>1) Base configuration: <code>pwquality</code> and <code>faillock</code></h3>
<p><strong>Sensible values (both families):</strong></p>
<pre><code class="language-ini"># /etc/security/pwquality.conf
minlen = 12
minclass = 3
ucredit = -1
lcredit = -1
dcredit = -1
ocredit = -1
maxrepeat = 2
</code></pre>
<pre><code class="language-ini"># /etc/security/faillock.conf
deny = 5
unlock_time = 600
fail_interval = 900
# Note: the tally directory is usually /run/faillock on most systems
</code></pre>
<h4>RHEL/Rocky/Alma</h4>
<pre><code class="language-shell">sudo dnf install -y libpwquality pam_u2f audit
# Select base profile (adjust to your environment: sssd/winbind/local)
sudo authselect select sssd --force
# Enable features
sudo authselect enable-feature with-pwquality
sudo authselect enable-feature with-faillock
sudo authselect apply-changes
# Deploy policy files
sudo cp pwquality.conf /etc/security/pwquality.conf
sudo cp faillock.conf  /etc/security/faillock.conf
# Verify
authselect check
</code></pre>
<h4>Debian/Ubuntu</h4>
<pre><code class="language-plaintext">sudo apt update
sudo apt install -y libpam-pwquality libpam-modules auditd
# Profile for pam-auth-update (faillock)
cat &lt;&lt;'EOF' | sudo tee /usr/share/pam-configs/faillock
Name: Faillock (lock after failed logins)
Default: yes
Priority: 900
Auth-Type: Primary
Auth:
    requisite pam_faillock.so preauth
    [default=die] pam_faillock.so authfail
    sufficient pam_faillock.so authsucc
Account-Type: Primary
Account:
    required pam_faillock.so
EOF
sudo pam-auth-update --enable faillock
# Deploy policy files
sudo cp pwquality.conf /etc/security/pwquality.conf
sudo cp faillock.conf  /etc/security/faillock.conf
</code></pre>
<blockquote>
<p>✅ <strong>Quick validation:</strong></p>
<ul>
<li><p>Use <code>pwscore</code> or try setting a weak password to see it rejected.</p>
</li>
<li><p>Make 5 failed attempts and check <code>faillock --user &lt;username&gt;</code>.</p>
</li>
</ul>
</blockquote>
<hr />
<h3>2) MFA with YubiKey (U2F/FIDO2) for <code>sudo</code> (optional but recommended)</h3>
<p><strong>Installation</strong></p>
<ul>
<li><p>RHEL/Rocky/Alma: <code>sudo dnf install -y pam_u2f</code> (may require EPEL).</p>
</li>
<li><p>Debian/Ubuntu: <code>sudo apt install -y libpam-u2f</code>.</p>
</li>
</ul>
<p><strong>Key enrollment (centralised mapping)</strong></p>
<pre><code class="language-bash"># Enroll YubiKey for the user (repeat with backup key)
mkdir -p ~/.config/Yubico
pamu2fcfg -u "$(whoami)" &gt; ~/.config/Yubico/u2f_keys
# Central mapping for PAM
sudo cp ~/.config/Yubico/u2f_keys /etc/u2f_mappings
sudo chmod 600 /etc/u2f_mappings
</code></pre>
<p><strong>Protect</strong> <code>sudo</code> <strong>with MFA + break-glass</strong></p>
<pre><code class="language-plaintext"># /etc/pam.d/sudo (add near the top)
# Allow emergency group to bypass MFA (break-glass)
auth  [success=1 default=ignore] pam_succeed_if.so user ingroup admins-nomfa
# Require U2F MFA for everyone else
auth  required pam_u2f.so authfile=/etc/u2f_mappings cue=1
</code></pre>
<blockquote>
<p>🔁 Enroll 2 keys per user and document your <strong>emergency procedure</strong> (console/iLO/Out-of-Band + break-glass user with rotation and audit trail).</p>
</blockquote>
<hr />
<h3>3) Auditing with <code>auditd</code></h3>
<pre><code class="language-bash"># /etc/audit/rules.d/99-hardening.rules
-w /etc/pam.d/          -p wa -k pam_changes
-w /etc/security/       -p wa -k pam_changes
-w /etc/ssh/sshd_config -p wa -k ssh_config
-w /etc/sudoers         -p wa -k sudo_config
-w /etc/sudoers.d/      -p wa -k sudo_config
</code></pre>
<pre><code class="language-bash">sudo augenrules --load
sudo systemctl restart auditd
# Queries
sudo ausearch -k pam_changes --start recent
sudo aureport  -k --summary
</code></pre>
<hr />
<h2>Put it in code: Ansible + Molecule</h2>
<p><strong>Role structure</strong></p>
<pre><code class="language-plaintext">roles/pam_hardening/
├─ defaults/main.yml
├─ tasks/main.yml
├─ templates/pwquality.conf.j2
├─ templates/faillock.conf.j2
├─ files/pam-configs/faillock   # for Debian/Ubuntu
└─ molecule/default/
   ├─ molecule.yml
   ├─ converge.yml
   └─ tests/test_pam.py
</code></pre>
<p><code>defaults/main.yml</code></p>
<pre><code class="language-yaml">pwquality:
  minlen: 12
  minclass: 3
  ucredit: -1
  lcredit: -1
  dcredit: -1
  ocredit: -1
  maxrepeat: 2
faillock:
  deny: 5
  unlock_time: 600
  fail_interval: 900
</code></pre>
<p><code>tasks/main.yml</code> (summary)</p>
<pre><code class="language-yaml">- name: Install base packages
  package:
    name: "{{ item }}"
    state: present
  loop: &gt;-
    {{ (ansible_facts.os_family == 'RedHat') | ternary(
         ['libpwquality', 'pam', 'audit'],
         ['libpam-pwquality', 'libpam-modules', 'auditd']
       ) }}

- name: RHEL | Enable authselect features
  command: &gt;-
    authselect enable-feature {{ item }}
  loop:
    - with-pwquality
    - with-faillock
  when: ansible_facts.os_family == 'RedHat'
  notify: Apply authselect

- name: RHEL | Select sssd profile if not set
  command: authselect select sssd --force
  when: ansible_facts.os_family == 'RedHat'

- name: Debian | Deploy faillock profile for pam-auth-update
  copy:
    src: files/pam-configs/faillock
    dest: /usr/share/pam-configs/faillock
    owner: root
    group: root
    mode: '0644'
  when: ansible_facts.os_family == 'Debian'

- name: Debian | Enable faillock
  command: pam-auth-update --enable faillock
  changed_when: "'No changes' not in result.stdout"
  register: result
  when: ansible_facts.os_family == 'Debian'

- name: Template pwquality.conf
  template:
    src: pwquality.conf.j2
    dest: /etc/security/pwquality.conf
    owner: root
    group: root
    mode: '0644'

- name: Template faillock.conf
  template:
    src: faillock.conf.j2
    dest: /etc/security/faillock.conf
    owner: root
    group: root
    mode: '0644'
</code></pre>
<p><code>handlers/main.yml</code></p>
<pre><code class="language-yaml">- name: Apply authselect
  command: authselect apply-changes
  when: ansible_facts.os_family == 'RedHat'
</code></pre>
<p><code>molecule/default/molecule.yml</code></p>
<pre><code class="language-yaml">platforms:
  - name: rocky9
    image: rockylinux:9
    privileged: true
  - name: ubuntu-24
    image: ubuntu:24.04
provisioner:
  name: ansible
verifier:
  name: testinfra
</code></pre>
<p><code>molecule/default/tests/test_pam.py</code> (excerpt)</p>
<pre><code class="language-python">def test_pwquality_file(host):
    f = host.file('/etc/security/pwquality.conf')
    assert f.exists
    assert f.contains('minlen = 12')

def test_pam_chain(host):
    distro = host.system_info.distribution.lower()
    if 'rocky' in distro or 'alma' in distro or 'redhat' in distro:
        pam = host.file('/etc/pam.d/password-auth')
        assert pam.contains('pam_pwquality.so')
    else:
        pam = host.file('/etc/pam.d/common-password')
        assert pam.contains('pam_pwquality.so')
</code></pre>
<p><strong>CI/CD (quick idea):</strong></p>
<ul>
<li><p><code>ansible-lint</code> + <code>yamllint</code>.</p>
</li>
<li><p><code>molecule test</code> on Rocky 9 and Ubuntu 24.04.</p>
</li>
<li><p>Publish artefacts (guide PDF and changelog).</p>
</li>
</ul>
<hr />
<h2>Production checklist</h2>
<ul>
<li><p>[ ] <code>UsePAM yes</code> in <code>sshd_config</code> if you need PAM in SSH.</p>
</li>
<li><p>[ ] <code>pam_pwquality</code> with policy aligned to business requirements.</p>
</li>
<li><p>[ ] <code>pam_faillock</code> with sensible <code>deny</code>/<code>unlock_time</code>/<code>fail_interval</code>; decide whether tally persists across reboots.</p>
</li>
<li><p>[ ] U2F/YubiKey MFA on <code>sudo</code>, with <strong>second key</strong> and <strong>break-glass group</strong>.</p>
</li>
<li><p>[ ] <code>auditd</code> monitoring PAM/SSH/sudo with periodic reports.</p>
</li>
<li><p>[ ] Ansible role with <strong>green Molecule</strong> in CI.</p>
</li>
</ul>
<hr />
<h2>Common mistakes (and how to avoid them)</h2>
<ul>
<li><p><strong>Directly editing</strong> <code>system-auth</code>/<code>password-auth</code> (RHEL) or <code>common-*</code> (Debian): use <code>authselect</code>/<code>pam-auth-update</code>.</p>
</li>
<li><p><strong>Locking out service accounts</strong>: exclude them or apply rules per TTY/service.</p>
</li>
<li><p><strong>No recovery plan</strong>: document OOB/console access and rotated emergency users.</p>
</li>
<li><p><strong>Changes outside version control</strong>: everything in Git, with PRs and review.</p>
</li>
</ul>
<hr />
<h2>Appendix: reference values</h2>
<ul>
<li><p><code>deny = 5</code>, <code>unlock_time = 600</code>, <code>fail_interval = 900</code> typically balance security and usability.</p>
</li>
<li><p><code>minlen = 12</code>, <code>minclass = 3</code> with negative credits to enforce character variety.</p>
</li>
<li><p>On workstations, consider <code>pam_u2f</code> for <strong>GDM</strong> as well; on servers, focus on <code>sudo</code>.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Surviving Cloudflare Terraform Provider v5: Pain, Breaking Changes, and a Free Template for GitHub Pages]]></title><description><![CDATA[There’s a special moment in every engineer’s life where you sit down, open your laptop, crack your knuckles, and say:

“Today I’m going to set up a simple website.GitHub Pages + Cloudflare. How hard can it be?”

…and 20 minutes later you’re knee-deep...]]></description><link>https://blog.homelabforge.dev/surviving-cloudflare-terraform-provider-v5-pain-breaking-changes-and-a-free-template-for-github-pages</link><guid isPermaLink="true">https://blog.homelabforge.dev/surviving-cloudflare-terraform-provider-v5-pain-breaking-changes-and-a-free-template-for-github-pages</guid><category><![CDATA[Devops]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[cloudflare]]></category><category><![CDATA[Infrastructure as code]]></category><category><![CDATA[GitHubPages]]></category><category><![CDATA[Homelab]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Miguel Alpañez Alcalde]]></dc:creator><pubDate>Wed, 12 Nov 2025 09:00:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762887814212/95a7cdaf-1028-4cba-9137-02209dd94b94.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There’s a special moment in every engineer’s life where you sit down, open your laptop, crack your knuckles, and say:</p>
<blockquote>
<p>“Today I’m going to set up a simple website.<br />GitHub Pages + Cloudflare. How hard can it be?”</p>
</blockquote>
<p>…and 20 minutes later you’re knee-deep in manually creating DNS records, toggling SSL settings, configuring redirects, debugging Rulesets, and wondering whether Cloudflare’s KV namespaces require a sacrifice to the gods before Terraform accepts them.</p>
<p>If you’ve ever tried to maintain Cloudflare manually — DNS, HTTPS rewrites, redirects, Caching rules, and Pages settings — you already know the pain.</p>
<p>And if you’ve tried automating all of this with <strong>Terraform</strong>…<br />well, then you probably met the <em>beautiful chaos</em> that is <strong>Cloudflare Provider v5</strong>.</p>
<p>This is the story of how something “simple” turned into two weeks of deep-dive debugging — and the free GitHub template I built so you don’t have to suffer through the same journey.</p>
<hr />
<h1 id="heading-the-pain-begins-terraform-cloudflare-provider-v5">💥 The Pain Begins: Terraform Cloudflare Provider v5</h1>
<p>Cloudflare Provider v5 didn’t just make a few tweaks.<br />It brought a <strong>tsunami of breaking changes</strong>.</p>
<hr />
<h2 id="heading-1-resources-removed-or-replaced">❌ 1. Resources removed or replaced</h2>
<p>Some Terraform resources disappeared entirely. Others were renamed or split into multiple new ones.</p>
<pre><code class="lang-hcl"># v4: used everywhere
resource "cloudflare_page_rule" "redirect" { ... }

# v5: nope, now use rulesets
resource "cloudflare_ruleset" "redirect" { ... }
</code></pre>
<hr />
<h2 id="heading-2-the-great-rulesets-migration">❌ 2. The Great Rulesets Migration</h2>
<p>Rulesets are powerful.<br />Rulesets are flexible.<br />Rulesets are… not documented consistently.</p>
<p>Migrating from Page Rules to Rulesets requires new blocks, phases, expressions, actions, and sometimes zone-level instead of account-level.</p>
<hr />
<h2 id="heading-3-zone-settings-override-limitations">❌ 3. Zone Settings Override limitations</h2>
<p>Some settings are <em>only</em> allowed at account-level. Others <em>only</em> at zone-level. Terraform shows:</p>
<pre><code><span class="hljs-built_in">Error</span>: cannot modify setting at zone level
</code></pre><p>…without telling you where you <em>should</em> modify it.</p>
<hr />
<h2 id="heading-4-api-token-scopes-became-a-labyrinth">❌ 4. API Token scopes became a labyrinth</h2>
<p>You now need extremely precise permissions.</p>
<p>Miss <strong>one</strong> scope — even a read-only one — and Terraform explodes.</p>
<hr />
<h2 id="heading-5-account-level-vs-zone-level-divergence">❌ 5. Account-level vs Zone-level divergence</h2>
<ul>
<li>DNS → zone-level  </li>
<li>Cache rules → zone-level  </li>
<li>Redirects → account-level  </li>
<li>Pages → account-level  </li>
</ul>
<p>If you configure something on the wrong side, Terraform fails.</p>
<hr />
<h1 id="heading-why-this-matters-for-solo-engineers-homelab-builders-and-small-devops-teams">🙋‍♂️ Why This Matters for Solo Engineers, Homelab Builders, and Small DevOps Teams</h1>
<p>Most of us don’t want to architect a global CDN strategy.</p>
<p>We just want:</p>
<ul>
<li>a static site  </li>
<li>GitHub Pages  </li>
<li>Cloudflare  </li>
<li>DNS automated  </li>
<li>SSL working  </li>
<li>redirects configured  </li>
<li>caching optimized  </li>
</ul>
<p>Terraform v5 turned this simple workflow into:</p>
<blockquote>
<p>“Let me spend three hours debugging a zone-level setting just to redirect www → apex.”</p>
</blockquote>
<hr />
<h1 id="heading-introducing-my-free-template-terraform-cloudflare-github-pages">✅ Introducing My Free Template: Terraform + Cloudflare + GitHub Pages</h1>
<p>A clean, minimal, reproducible template that:</p>
<ul>
<li>Works with provider v4 (or v5 with small edits)  </li>
<li>Creates GitHub Pages DNS  </li>
<li>Configures SSL  </li>
<li>Creates redirects  </li>
<li>Adds cache rules  </li>
<li>Documents API token scopes  </li>
<li>Requires zero Cloudflare dashboard clicks  </li>
</ul>
<hr />
<h1 id="heading-what-the-template-solves">🛠️ What the Template Solves</h1>
<h3 id="heading-dns-records-for-github-pages">✔ DNS Records for GitHub Pages</h3>
<h3 id="heading-github-pages-cname-handling">✔ GitHub Pages CNAME Handling</h3>
<h3 id="heading-redirect-www-apex">✔ Redirect www → apex</h3>
<h3 id="heading-https-rewrite-amp-security-settings">✔ HTTPS rewrite &amp; security settings</h3>
<h3 id="heading-cache-rules-for-static-assets">✔ Cache rules for static assets</h3>
<h3 id="heading-clean-api-token-requirements">✔ Clean API Token requirements</h3>
<hr />
<h1 id="heading-example-terraform-code">🧩 Example Terraform Code</h1>
<h3 id="heading-dns-record">DNS Record</h3>
<pre><code class="lang-hcl">resource "cloudflare_record" "github_pages" {
  zone_id = var.zone_id
  name    = "@"
  type    = "CNAME"
  value   = "${var.github_username}.github.io"
  proxied = true
}
</code></pre>
<h3 id="heading-redirect">Redirect</h3>
<pre><code class="lang-hcl">resource "cloudflare_ruleset" "redirect_www_to_apex" {
  name    = "www-to-apex"
  zone_id = var.zone_id
  kind    = "zone"
  phase   = "http_request_redirect"

  rules {
    expression = "(http.host eq \"www.${var.domain}\")"
    action     = "redirect"

    action_parameters {
      from_value {
        status_code = 301
        target_url  = "https://${var.domain}/"
      }
    }
  }
}
</code></pre>
<h3 id="heading-cache-static">Cache Static</h3>
<pre><code class="lang-hcl">resource "cloudflare_ruleset" "cache_static" {
  name    = "cache-static"
  zone_id = var.zone_id
  kind    = "zone"
  phase   = "http_request_cache_settings"

  rules {
    expression = "(http.request.uri.path matches \".*\\.(css|js|png|jpg|svg|ico)\")"
    action     = "set_cache_settings"

    action_parameters {
      cache = true
      edge_ttl {
        mode         = "override_origin"
        default      = 2592000
      }
    }
  }
}
</code></pre>
<hr />
<h1 id="heading-architecture-diagram">🔎 Architecture Diagram</h1>
<pre><code class="lang-mermaid">flowchart LR
    A[Browser] --&gt; B[Cloudflare Edge]
    B --&gt; C[Rulesets: Redirect + Cache + HTTPS]
    C --&gt; D[GitHub Pages Hosting]
    D --&gt; B
    B --&gt; A
</code></pre>
<hr />
<h1 id="heading-lessons-learned-from-migrating-to-v5">🧠 Lessons Learned from Migrating to v5</h1>
<ul>
<li>Always use a sandbox zone  </li>
<li>Documentation lags behind API and TF provider  </li>
<li>Be explicit with API token scopes  </li>
<li>Don’t mix account/zone configs blindly  </li>
<li>Cache &amp; redirect rules are fragile  </li>
</ul>
<hr />
<h1 id="heading-final-thoughts">🚀 Final Thoughts</h1>
<p>If you want to avoid the suffering and deploy Cloudflare + GitHub Pages cleanly:</p>
<p>👉 <strong>GitHub Repository:</strong> <em>(https://github.com/malpanez/terraform-cloudflare-github-pages)</em><br />👉 <strong>Hashnode Blog:</strong> <em>(https://blog.homelabforge.dev/surviving-cloudflare-terraform-provider-v5-pain-breaking-changes-and-a-free-template-for-github-pages)</em>  </p>
<p>Happy automating!</p>
]]></content:encoded></item><item><title><![CDATA[Welcome to HomelabForge]]></title><description><![CDATA[Mission. Bring production-grade DevOps to homelabs and small teams with clear, reproducible guides you can copy, adapt, and ship.
What to expect

IaC with Terraform & Ansible  
Platform: Proxmox, Kubernetes, containers  
Pipelines: CI/CD, testing, pr...]]></description><link>https://blog.homelabforge.dev/welcome-to-homelabforge</link><guid isPermaLink="true">https://blog.homelabforge.dev/welcome-to-homelabforge</guid><dc:creator><![CDATA[Miguel Alpañez Alcalde]]></dc:creator><pubDate>Tue, 04 Nov 2025 09:30:59 GMT</pubDate><content:encoded><![CDATA[<p><strong>Mission.</strong> Bring production-grade DevOps to homelabs and small teams with clear, reproducible guides you can copy, adapt, and ship.</p>
<h2 id="heading-what-to-expect">What to expect</h2>
<ul>
<li><strong>IaC</strong> with Terraform &amp; Ansible  </li>
<li><strong>Platform</strong>: Proxmox, Kubernetes, containers  </li>
<li><strong>Pipelines</strong>: CI/CD, testing, pre-commit  </li>
<li><strong>Security</strong>: least privilege, secrets, hardening  </li>
<li><strong>Design</strong>: diagrams, templates, checklists</li>
</ul>
<h2 id="heading-principles">Principles</h2>
<ol>
<li><strong>Production first</strong> — patterns from enterprise, adapted to home.  </li>
<li><strong>Reproducible</strong> — everything as code, minimal clicks.  </li>
<li><strong>Measurable</strong> — benchmarks, before/after, numbers.</li>
</ol>
<h2 id="heading-stack-amp-repos">Stack &amp; repos</h2>
<ul>
<li>GitHub: https://github.com/malpanez  </li>
<li>Site: https://homelabforge.dev</li>
</ul>
<blockquote>
<p><strong>Subscribe</strong> to get new posts and templates (weekly cadence).<br />Prefer Spanish? La versión en español llegará pronto.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[A Cloud Guru Challenge - Improve Application Performance using ElastiCache Redis]]></title><description><![CDATA[Improve perfomance using redis
 https://acloudguru.com/blog/engineering/cloudguruchallenge-improve-application-performance-using-amazon-elasticache?utm_source=linkedin&utm_medium=social&utm_campaign=cloudguruchallengee 
For accomplish this issue I re...]]></description><link>https://blog.homelabforge.dev/acg-challenge-2021-elasticache-redis-terraform</link><guid isPermaLink="true">https://blog.homelabforge.dev/acg-challenge-2021-elasticache-redis-terraform</guid><category><![CDATA[AWS]]></category><category><![CDATA[Amazon Elasticache]]></category><category><![CDATA[Redis]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[performance]]></category><category><![CDATA[caching]]></category><category><![CDATA[vpc]]></category><category><![CDATA[Devops]]></category><category><![CDATA[#A Cloud Guru]]></category><category><![CDATA[challenge]]></category><dc:creator><![CDATA[Miguel Alpañez Alcalde]]></dc:creator><pubDate>Sat, 31 Jul 2021 15:13:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1627744355192/ImA_Dr2Lh.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-improve-perfomance-using-redis">Improve perfomance using redis</h3>
<p> <a target="_blank" href="Link">https://acloudguru.com/blog/engineering/cloudguruchallenge-improve-application-performance-using-amazon-elasticache?utm_source=linkedin&amp;utm_medium=social&amp;utm_campaign=cloudguruchallengee</a> </p>
<p>For accomplish this issue I required:</p>
<ul>
<li>a valid AWS profile</li>
<li>a valid KeyPair to be used in the EC2.</li>
<li>Terraform</li>
</ul>
<p>Steps
Implement RDS,VPC,EC2,REDIS using Terraform:</p>
<p>See code <a target="_blank" href="https://github.com/malpanez/acg-app-performance-challenge">here</a></p>
<p>Log by ssh
Inside the public ec2:</p>
<p>ssh ec2-user@xxx.xxx.xxx.xxx -i key</p>
<p>Ensure that everything from the user-data.bash it's installed and start the configuration around the:</p>
<ol>
<li>nginx (install and apply the configuration given for redirection when using /app to the port 5000</li>
<li>ensure that all the pre-requisites regarding python are in place (install python modules: psycopg2-binary, postgres, flask, parserconfig, redis)</li>
<li>configure the database.ini file:</li>
</ol>
<p>Note: there's no problem in show full code as it's a Cloud Playground and will be deleted by when it's published.</p>
<pre><code>[postgresql]
host=terraform<span class="hljs-number">-20210731120731938300000001.</span>cyb7h7wacdzq.us-east<span class="hljs-number">-1.</span>rds.amazonaws.com
database=postgres
user=postgres
password=postgres

[redis]
redis_url=redis:<span class="hljs-comment">//redis-cache.x2mygh.0001.use1.cache.amazonaws.com:6379</span>
</code></pre><ol start="4">
<li>Modify the app.py given once you include the ElastiCache, here's the final code:</li>
</ol>
<pre><code># /usr/bin/python2<span class="hljs-number">.7</span>
<span class="hljs-keyword">import</span> psycopg2
<span class="hljs-keyword">from</span> configparser <span class="hljs-keyword">import</span> ConfigParser
<span class="hljs-keyword">from</span> flask <span class="hljs-keyword">import</span> Flask, request, render_template, g, abort
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">import</span> redis

def config(filename=<span class="hljs-string">'config/database.ini'</span>, section=<span class="hljs-string">'postgresql'</span>):
    # create a parser
    parser = ConfigParser()
    # read config file
    parser.read(filename)
  # get section, <span class="hljs-keyword">default</span> to postgresql
    db = {}
    <span class="hljs-keyword">if</span> parser.has_section(section):
        params = parser.items(section)
        <span class="hljs-keyword">for</span> param <span class="hljs-keyword">in</span> params:
            db[param[<span class="hljs-number">0</span>]] = param[<span class="hljs-number">1</span>]
    <span class="hljs-attr">else</span>:
        raise Exception(<span class="hljs-string">'Section {0} not found in the {1} file'</span>.format(section, filename))

    <span class="hljs-keyword">return</span> db

def fetch(sql):
    # connect to database listed <span class="hljs-keyword">in</span> database.ini
    ttl = <span class="hljs-number">10</span> # Time to live <span class="hljs-keyword">in</span> seconds
    <span class="hljs-attr">try</span>:
       params = config(filename=<span class="hljs-string">'config/database.ini'</span>,section=<span class="hljs-string">'redis'</span>)
       cache = redis.Redis.from_url(params[<span class="hljs-string">'redis_url'</span>])
       result = cache.get(sql)

       <span class="hljs-keyword">if</span> result:
         <span class="hljs-keyword">return</span> result
       <span class="hljs-attr">else</span>:
         # connect to database listed <span class="hljs-keyword">in</span> database.ini
         conn = connect()
         cur = conn.cursor()
         cur.execute(sql)
         # fetch one row
         result = cur.fetchone()
         print(<span class="hljs-string">'Closing connection to database...'</span>)
         cur.close()
         conn.close()

         # cache result
         cache.setex(sql, ttl, <span class="hljs-string">''</span>.join(result))
         <span class="hljs-keyword">return</span> result

    except (Exception, psycopg2.DatabaseError) <span class="hljs-keyword">as</span> error:
        print(error)

def connect():
    <span class="hljs-string">""</span><span class="hljs-string">" Connect to the PostgreSQL database server and return a cursor "</span><span class="hljs-string">""</span>
    conn = None
    <span class="hljs-attr">try</span>:
        # read connection parameters
        params = config()

        # connect to the PostgreSQL server
        print(<span class="hljs-string">'Connecting to the PostgreSQL database...'</span>)
        conn = psycopg2.connect(**params)

    except (Exception, psycopg2.DatabaseError) <span class="hljs-keyword">as</span> error:
        print(<span class="hljs-string">"Error:"</span>, error)
        conn = None

    <span class="hljs-attr">else</span>:
        # <span class="hljs-keyword">return</span> a conn
        <span class="hljs-keyword">return</span> conn

app = Flask(__name__)

@app.before_request
def before_request():
   g.request_start_time = time.time()
   g.request_time = lambda: <span class="hljs-string">"%.5fs"</span> % (time.time() - g.request_start_time)

@app.route(<span class="hljs-string">"/"</span>)
def index():
    sql = <span class="hljs-string">'SELECT slow_version();'</span>
    db_result = fetch(sql)

    <span class="hljs-keyword">if</span>(db_result):
        db_version = <span class="hljs-string">""</span>.join([str(i) <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> db_result])
    <span class="hljs-attr">else</span>:
        abort(<span class="hljs-number">500</span>)
    params = config()
    <span class="hljs-keyword">return</span> render_template(<span class="hljs-string">'index.html'</span>, db_version = db_version, db_host = params[<span class="hljs-string">'host'</span>])


<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:        # on running python app.py
    app.run()                     # run the flask app
</code></pre><p>Here's the first attempt using the slow code given to the postgresql:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627743213440/WTNVya44K.png" alt="image.png" /></p>
<p>Second time:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627743285723/XlgFW_pds.png" alt="image.png" /></p>
<p>Third time:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627743299841/zOIHBF6rx.png" alt="image.png" /></p>
<p>This is using Redis Cache:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627743317836/jAU6jUcZUS.png" alt="image.png" /></p>
<p>Example of using the Application with connection to DB and from Redis:</p>
<pre><code>(venv) [ec2-user@ip<span class="hljs-number">-10</span><span class="hljs-number">-0</span><span class="hljs-number">-3</span><span class="hljs-number">-203</span> bin]$ python app.py 
 * Serving Flask app <span class="hljs-string">'app'</span> (lazy loading)
 * Environment: production
   <span class="hljs-attr">WARNING</span>: This is a development server. Do not use it <span class="hljs-keyword">in</span> a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http:<span class="hljs-comment">//127.0.0.1:5000/ (Press CTRL+C to quit)</span>
Connecting to the PostgreSQL database...
Closing connection to database...
<span class="hljs-number">127.0</span><span class="hljs-number">.0</span><span class="hljs-number">.1</span> - - [<span class="hljs-number">31</span>/Jul/<span class="hljs-number">2021</span> <span class="hljs-number">14</span>:<span class="hljs-number">56</span>:<span class="hljs-number">26</span>] <span class="hljs-string">"GET / HTTP/1.0"</span> <span class="hljs-number">200</span> -
Connecting to the PostgreSQL database...
Closing connection to database...
<span class="hljs-number">127.0</span><span class="hljs-number">.0</span><span class="hljs-number">.1</span> - - [<span class="hljs-number">31</span>/Jul/<span class="hljs-number">2021</span> <span class="hljs-number">14</span>:<span class="hljs-number">56</span>:<span class="hljs-number">43</span>] <span class="hljs-string">"GET / HTTP/1.0"</span> <span class="hljs-number">200</span> -
Connecting to the PostgreSQL database...
Closing connection to database...
<span class="hljs-number">127.0</span><span class="hljs-number">.0</span><span class="hljs-number">.1</span> - - [<span class="hljs-number">31</span>/Jul/<span class="hljs-number">2021</span> <span class="hljs-number">14</span>:<span class="hljs-number">56</span>:<span class="hljs-number">58</span>] <span class="hljs-string">"GET / HTTP/1.0"</span> <span class="hljs-number">200</span> -
<span class="hljs-number">127.0</span><span class="hljs-number">.0</span><span class="hljs-number">.1</span> - - [<span class="hljs-number">31</span>/Jul/<span class="hljs-number">2021</span> <span class="hljs-number">14</span>:<span class="hljs-number">57</span>:<span class="hljs-number">01</span>] <span class="hljs-string">"GET / HTTP/1.0"</span> <span class="hljs-number">200</span> -
</code></pre>]]></content:encoded></item></channel></rss>