<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Michał Lis</title>
    <link>https://nutcracker.one.pl/</link>
    <description>yet another technical blog...</description>
    <pubDate>Sun, 08 Nov 2026 04:27:05 +0100</pubDate>
    <item>
      <title>How to Manage Jenkins Configuration as Code on Kubernetes</title>
      <link>https://nutcracker.one.pl/how-to-manage-jenkins-configuration-as-code-on-kubernetes</link>
      <description>&lt;![CDATA[Running Jenkins on Kubernetes is relatively straightforward, but maintaining its configuration across deployments can quickly become challenging. Manually configuring plugins, authentication, permissions, credentials, and pipelines through the Jenkins UI makes rebuilding or migrating an instance unnecessarily complicated.&#xA;&#xA;To solve this problem, I implemented a Jenkins Configuration as Code (JCasC) approach, allowing me to deploy a fully configured Jenkins instance on Kubernetes using a custom Docker image, ConfigMaps, Kubernetes Secrets, and Job DSL.&#xA;&#xA;The goal was simple: make Jenkins configuration reproducible, version-controlled, and easy to restore without manual intervention. &#xA;&#xA;!--more--&#xA;&#xA;Building a custom Jenkins image with preinstalled plugins&#xA;&#xA;The first step was to create a custom Jenkins Docker image based on the official Jenkins distribution with JDK 21.&#xA;&#xA;Instead of installing plugins manually through the Jenkins UI, I embedded all required plugins directly into the image during the build process.&#xA;&#xA;FROM docker.io/jenkins/jenkins:jdk21&#xA;USER root&#xA;ENV CASCJENKINSCONFIG=/var/jenkins/cascconfig&#xA;RUN apt-get update &amp;&amp; apt-get install -y \&#xA;    lsb-release curl ca-certificates&#xA;RUN curl -fsSLo /usr/share/keyrings/docker-archive-keyring.asc \&#xA;    https://download.docker.com/linux/debian/gpg&#xA;RUN echo &#34;deb [arch=$(dpkg --print-architecture) \&#xA;    signed-by=/usr/share/keyrings/docker-archive-keyring.asc] \&#xA;    https://download.docker.com/linux/debian \&#xA;    $(lsbrelease -cs) stable&#34; \&#xA;      /etc/apt/sources.list.d/docker.list&#xA;RUN apt-get update &amp;&amp; apt-get install -y docker-ce-cli&#xA;USER jenkins&#xA;RUN jenkins-plugin-cli --plugins&#xA;    &#34;configuration-as-code job-dsl git workflow-job workflow-cps saml oic-auth matrix-auth gitea docker-plugin docker-commons docker-java-api&#34;&#xA;&#xA;The most important plugins in this setup are:&#xA;Configuration as Code: Automatically configures Jenkins using YAML files.&#xA;Job DSL: Defines and creates Jenkins jobs programmatically.&#xA;OpenID Connect Authentication: Integrates Jenkins with an external identity provider such as Keycloak.&#xA;Matrix Authorization Strategy: Controls permissions for users and groups.&#xA;Docker Plugin: Provides dynamically provisioned Jenkins build agents.&#xA;I also installed the Docker CLI to support Docker-related operations. The Docker plugin itself communicates with a compatible Docker API endpoint.&#xA;&#xA;By building plugins into the image, I can ensure that every deployment starts with a consistent set of dependencies. For stronger reproducibility, I would pin the Jenkins base image and plugin versions rather than relying on moving tags and latest compatible versions.&#xA;&#xA;Configuring Jenkins with Configuration as Code&#xA;&#xA;After preparing the image, I used the Configuration as Code plugin to manage Jenkins settings through YAML.&#xA;&#xA;The environment variable:&#xA;CASCJENKINSCONFIG=/var/jenkins/cascconfig&#xA;&#xA;tells Jenkins where to find its configuration files.&#xA;&#xA;Here is a simplified example based on my implementation:&#xA;&#xA;jenkins:&#xA;  numExecutors: 2&#xA;  mode: NORMAL&#xA;&#xA;  authorizationStrategy:&#xA;    globalMatrix:&#xA;      entries:&#xA;        group:&#xA;            name: &#34;admin&#34;&#xA;            permissions:&#xA;              &#34;Overall/Administer&#34;&#xA;        group:&#xA;            name: &#34;authenticated&#34;&#xA;            permissions:&#xA;              &#34;Overall/Read&#34;&#xA;              &#34;Job/Read&#34;&#xA;              &#34;Job/Build&#34;&#xA;&#xA;  securityRealm:&#xA;    oic:&#xA;      clientId: &#34;jenkins-oic-auth&#34;&#xA;      clientSecret: &#34;${OIDCCLIENTSECRET}&#34;&#xA;      userNameField: &#34;preferredusername&#34;&#xA;      groupsFieldName: &#34;groups&#34;&#xA;      serverConfiguration:&#xA;        manual:&#xA;          issuer: &#34;https://keycloak.example.com/realms/master&#34;&#xA;          scopes: &#34;openid email profile&#34;&#xA;&#xA;  clouds:&#xA;    docker:&#xA;        name: &#34;podman&#34;&#xA;        containerCap: 3&#xA;        dockerApi:&#xA;          dockerHost:&#xA;            uri: &#34;unix:///var/run/docker.sock&#34;&#xA;        templates:&#xA;          labelString: &#34;alpine-jdk21&#34;&#xA;            name: &#34;alpine-jdk21&#34;&#xA;            dockerTemplateBase:&#xA;              image: &#34;jenkins/inbound-agent:latest-alpine-jdk21&#34;&#xA;            remoteFs: &#34;/home/jenkins/agent&#34;&#xA;            mode: EXCLUSIVE&#xA;            connector:&#xA;              jnlp:&#xA;                jenkinsUrl: &#34;https://jenkins.example.com/&#34;&#xA;                user: &#34;1000&#34;&#xA;jobs:&#xA;  script:   pipelineJob(&#39;TerraformAWSaccount&#39;) {&#xA;        definition {&#xA;          cpsScm {&#xA;            lightweight(true)&#xA;            scm {&#xA;              git {&#xA;                remote {&#xA;                  url(&#39;ssh://git@git.example.com:7999/infra/project.git&#39;)&#xA;                  credentials(&#39;nutcracker-rsa-key&#39;)&#xA;                }&#xA;                branches(&#39;/main&#39;)&#xA;              }&#xA;            }&#xA;            scriptPath(&#39;path/to/Jenkinsfile&#39;)&#xA;          }&#xA;        }&#xA;      }&#xA;In my setup, I integrated Jenkins with Keycloak using OpenID Connect, configured role-based permissions through matrix authorisation, and defined Docker-based build agents.&#xA;&#xA;The agent configuration uses a Docker-compatible API endpoint, which can also be provided by Podman. The socket must be available to the Jenkins controller and accessible with appropriate permissions. On Kubernetes, I would avoid mounting a privileged host Docker socket where possible and prefer a dedicated agent provisioning mechanism.&#xA;&#xA;Managing credentials securely with Kubernetes Secrets&#xA;&#xA;One important consideration is handling sensitive information.&#xA;&#xA;Although JCasC supports defining credentials directly in YAML, storing private keys or authentication secrets in a ConfigMap or Git repository is not a secure solution.&#xA;&#xA;Instead, I can store sensitive data in Kubernetes Secrets and reference them from the JCasC configuration.&#xA;&#xA;For example, the SSH credential definition becomes:&#xA;&#xA;credentials:&#xA;  system:&#xA;    domainCredentials:&#xA;      credentials:&#xA;          basicSSHUserPrivateKey:&#xA;              scope: GLOBAL&#xA;              id: &#34;nutcracker-rsa-key&#34;&#xA;              username: &#34;rsa-key&#34;&#xA;              description: &#34;rsa key for Git access&#34;&#xA;              privateKeySource:&#xA;                directEntry:&#xA;                  privateKey: &#34;${JENKINSRSAPRIVATEKEY}&#34;&#xA;&#xA;The ${JENKINSRSAPRIVATEKEY} expression is resolved by the Configuration as Code plugin from an environment variable.&#xA;&#xA;Now can create the Kubernetes Secrets using an existing SSH private key and OIDC taken secret taken from Keycloak:&#xA;&#xA;kubectl create secret generic jenkins-secrets --namespace jenkins --from-file=rsa-private-key=./idrsa --from-literal=oidc-client-secret=&#39;YOUROIDCSECRET&#39;&#xA;&#xA;The Job DSL configuration is included in the same JCasC YAML file:&#xA;&#xA;jobs:&#xA;  script:   pipelineJob(&#39;TerraformAWSaccount&#39;) {&#xA;        definition {&#xA;          cpsScm {&#xA;            lightweight(true)&#xA;            scm {&#xA;              git {&#xA;                remote {&#xA;                  url(&#39;ssh://git@git.example.com:7999/infra/project.git&#39;)&#xA;                  credentials(&#39;nutcracker-rsa-key&#39;)&#xA;                }&#xA;                branches(&#39;/main&#39;)&#xA;              }&#xA;            }&#xA;            scriptPath(&#39;path/to/Jenkinsfile&#39;)&#xA;          }&#xA;        }&#xA;      }&#xA;This configuration automatically creates a pipeline named TerraformAWSaccount and connects it to a Git repository containing the Jenkinsfile.&#xA;&#xA;The SSH credential is referenced by its Jenkins credential ID, so the private key does not need to appear in the Job DSL script.&#xA;&#xA;This approach allows me to version-control not only Jenkins system settings but also the pipeline definitions themselves.&#xA;&#xA;Finally, I defined a Kubernetes Deployment that mounts the ConfigMap into the path specified by CASCJENKINSCONFIG&#xA;&#xA;apiVersion: apps/v1&#xA;kind: Deployment&#xA;metadata:&#xA;  name: jenkins&#xA;  namespace: jenkins&#xA;spec:&#xA;  replicas: 1&#xA;  strategy:&#xA;    type: Recreate&#xA;  selector:&#xA;    matchLabels:&#xA;      app: jenkins&#xA;  template:&#xA;    metadata:&#xA;      labels:&#xA;        app: jenkins&#xA;    spec:&#xA;      containers:&#xA;        name: jenkins&#xA;          image: registry.example.com/jenkins:1.0.0&#xA;&#xA;          ports:&#xA;            containerPort: 8080&#xA;            containerPort: 50000&#xA;&#xA;          env:&#xA;            name: CASCJENKINSCONFIG&#xA;              value: /var/jenkins/cascconfig&#xA;&#xA;            name: JENKINSRSAPRIVATEKEY&#xA;              valueFrom:&#xA;                secretKeyRef:&#xA;                  name: jenkins-secrets&#xA;                  key: rsa-private-key&#xA;&#xA;            name: OIDCCLIENTSECRET&#xA;              valueFrom:&#xA;                secretKeyRef:&#xA;                  name: jenkins-secrets&#xA;                  key: oidc-client-secret&#xA;&#xA;          volumeMounts:&#xA;            name: jenkins-casc&#xA;              mountPath: /var/jenkins/cascconfig&#xA;              readOnly: true&#xA;&#xA;            name: jenkins-home&#xA;              mountPath: /var/jenkins_home&#xA;&#xA;      volumes:&#xA;        name: jenkins-casc&#xA;          configMap:&#xA;            name: jenkins-casc&#xA;&#xA;        name: jenkins-home&#xA;          persistentVolumeClaim:&#xA;            claimName: jenkins-home&#xA;&#xA;In this deployment:&#xA;The custom image contains all required Jenkins plugins.&#xA;The ConfigMap provides the JCasC YAML configuration.&#xA;Kubernetes Secrets provide sensitive credentials through environment variables.&#xA;A PersistentVolumeClaim stores Jenkins runtime data, build history, and other persistent state.&#xA;The PVC must be created separately. This example also assumes that Jenkins agents are configured independently or that the Docker-compatible endpoint defined in JCasC is available to the controller.&#xA;&#xA;An important detail is that updating the ConfigMap does not automatically restart Jenkins or guarantee that the new configuration is applied. In my deployment process, I can trigger a rollout restart:&#xA;kubectl rollout restart deployment/jenkins -n jenkins&#xA;&#xA;Jenkins then loads the configuration from the mounted YAML file during startup.&#xA;&#xA;Benefits of this approach&#xA;By combining Kubernetes, a custom Jenkins image, JCasC, and Job DSL, I achieved a deployment model where most of the Jenkins configuration is defined declarativelyt.&#xA;&#xA;The main benefits are reproducibility, simplified disaster recovery, version-controlled configuration, and reduced manual administration.&#xA;&#xA;It also makes upgrading Jenkins easier: I can build a new image with updated plugins, validate the configuration in a test environment, and deploy the updated version through Kubernetes.&#xA;&#xA;The most important improvement is that Jenkins is no longer configured primarily through its UI. Instead, its configuration becomes part of the infrastructure code, making changes easier to review, track, and reproduce.&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>Running Jenkins on Kubernetes is relatively straightforward, but maintaining its configuration across deployments can quickly become challenging. Manually configuring plugins, authentication, permissions, credentials, and pipelines through the Jenkins UI makes rebuilding or migrating an instance unnecessarily complicated.</p>

<p>To solve this problem, I implemented a Jenkins Configuration as Code (JCasC) approach, allowing me to deploy a fully configured Jenkins instance on Kubernetes using a custom Docker image, ConfigMaps, Kubernetes Secrets, and Job DSL.</p>

<p>The goal was simple: make Jenkins configuration reproducible, version-controlled, and easy to restore without manual intervention.</p>



<h1 id="building-a-custom-jenkins-image-with-preinstalled-plugins">Building a custom Jenkins image with preinstalled plugins</h1>

<p>The first step was to create a custom Jenkins Docker image based on the official Jenkins distribution with JDK 21.</p>

<p>Instead of installing plugins manually through the Jenkins UI, I embedded all required plugins directly into the image during the build process.</p>

<pre><code>FROM docker.io/jenkins/jenkins:jdk21
USER root
ENV CASC_JENKINS_CONFIG=/var/jenkins/casc_config
RUN apt-get update &amp;&amp; apt-get install -y \
    lsb-release curl ca-certificates
RUN curl -fsSLo /usr/share/keyrings/docker-archive-keyring.asc \
    https://download.docker.com/linux/debian/gpg
RUN echo &#34;deb [arch=$(dpkg --print-architecture) \
    signed-by=/usr/share/keyrings/docker-archive-keyring.asc] \
    https://download.docker.com/linux/debian \
    $(lsb_release -cs) stable&#34; \
    &gt; /etc/apt/sources.list.d/docker.list
RUN apt-get update &amp;&amp; apt-get install -y docker-ce-cli
USER jenkins
RUN jenkins-plugin-cli --plugins
    &#34;configuration-as-code job-dsl git workflow-job workflow-cps saml oic-auth matrix-auth gitea docker-plugin docker-commons docker-java-api&#34;
</code></pre>

<p>The most important plugins in this setup are:
* Configuration as Code: Automatically configures Jenkins using YAML files.
* Job DSL: Defines and creates Jenkins jobs programmatically.
* OpenID Connect Authentication: Integrates Jenkins with an external identity provider such as Keycloak.
* Matrix Authorization Strategy: Controls permissions for users and groups.
* Docker Plugin: Provides dynamically provisioned Jenkins build agents.
I also installed the Docker CLI to support Docker-related operations. The Docker plugin itself communicates with a compatible Docker API endpoint.</p>

<p>By building plugins into the image, I can ensure that every deployment starts with a consistent set of dependencies. For stronger reproducibility, I would pin the Jenkins base image and plugin versions rather than relying on moving tags and latest compatible versions.</p>

<h2 id="configuring-jenkins-with-configuration-as-code">Configuring Jenkins with Configuration as Code</h2>

<p>After preparing the image, I used the Configuration as Code plugin to manage Jenkins settings through YAML.</p>

<p>The environment variable:</p>

<pre><code>CASC_JENKINS_CONFIG=/var/jenkins/casc_config
</code></pre>

<p>tells Jenkins where to find its configuration files.</p>

<p>Here is a simplified example based on my implementation:</p>

<pre><code>jenkins:
  numExecutors: 2
  mode: NORMAL

  authorizationStrategy:
    globalMatrix:
      entries:
        - group:
            name: &#34;admin&#34;
            permissions:
              - &#34;Overall/Administer&#34;
        - group:
            name: &#34;authenticated&#34;
            permissions:
              - &#34;Overall/Read&#34;
              - &#34;Job/Read&#34;
              - &#34;Job/Build&#34;

  securityRealm:
    oic:
      clientId: &#34;jenkins-oic-auth&#34;
      clientSecret: &#34;${OIDC_CLIENT_SECRET}&#34;
      userNameField: &#34;preferred_username&#34;
      groupsFieldName: &#34;groups&#34;
      serverConfiguration:
        manual:
          issuer: &#34;https://keycloak.example.com/realms/master&#34;
          scopes: &#34;openid email profile&#34;

  clouds:
    - docker:
        name: &#34;podman&#34;
        containerCap: 3
        dockerApi:
          dockerHost:
            uri: &#34;unix:///var/run/docker.sock&#34;
        templates:
          - labelString: &#34;alpine-jdk21&#34;
            name: &#34;alpine-jdk21&#34;
            dockerTemplateBase:
              image: &#34;jenkins/inbound-agent:latest-alpine-jdk21&#34;
            remoteFs: &#34;/home/jenkins/agent&#34;
            mode: EXCLUSIVE
            connector:
              jnlp:
                jenkinsUrl: &#34;https://jenkins.example.com/&#34;
                user: &#34;1000&#34;
jobs:
  - script: &gt;
      pipelineJob(&#39;Terraform_AWS_account&#39;) {
        definition {
          cpsScm {
            lightweight(true)
            scm {
              git {
                remote {
                  url(&#39;ssh://git@git.example.com:7999/infra/project.git&#39;)
                  credentials(&#39;nutcracker-rsa-key&#39;)
                }
                branches(&#39;*/main&#39;)
              }
            }
            scriptPath(&#39;path/to/Jenkinsfile&#39;)
          }
        }
      }
</code></pre>

<p>In my setup, I integrated Jenkins with Keycloak using OpenID Connect, configured role-based permissions through matrix authorisation, and defined Docker-based build agents.</p>

<p>The agent configuration uses a Docker-compatible API endpoint, which can also be provided by Podman. The socket must be available to the Jenkins controller and accessible with appropriate permissions. On Kubernetes, I would avoid mounting a privileged host Docker socket where possible and prefer a dedicated agent provisioning mechanism.</p>

<h2 id="managing-credentials-securely-with-kubernetes-secrets">Managing credentials securely with Kubernetes Secrets</h2>

<p>One important consideration is handling sensitive information.</p>

<p>Although JCasC supports defining credentials directly in YAML, storing private keys or authentication secrets in a ConfigMap or Git repository is not a secure solution.</p>

<p>Instead, I can store sensitive data in Kubernetes Secrets and reference them from the JCasC configuration.</p>

<p>For example, the SSH credential definition becomes:</p>

<pre><code>credentials:
  system:
    domainCredentials:
      - credentials:
          - basicSSHUserPrivateKey:
              scope: GLOBAL
              id: &#34;nutcracker-rsa-key&#34;
              username: &#34;rsa-key&#34;
              description: &#34;rsa key for Git access&#34;
              privateKeySource:
                directEntry:
                  privateKey: &#34;${JENKINS_RSA_PRIVATE_KEY}&#34;
</code></pre>

<p>The ${JENKINS<em>RSA</em>PRIVATE_KEY} expression is resolved by the Configuration as Code plugin from an environment variable.</p>

<p>Now can create the Kubernetes Secrets using an existing SSH private key and OIDC taken secret taken from Keycloak:</p>

<pre><code>kubectl create secret generic jenkins-secrets --namespace jenkins --from-file=rsa-private-key=./id_rsa --from-literal=oidc-client-secret=&#39;YOUR_OIDC_SECRET&#39;
</code></pre>

<p>The Job DSL configuration is included in the same JCasC YAML file:</p>

<pre><code>jobs:
  - script: &gt;
      pipelineJob(&#39;Terraform_AWS_account&#39;) {
        definition {
          cpsScm {
            lightweight(true)
            scm {
              git {
                remote {
                  url(&#39;ssh://git@git.example.com:7999/infra/project.git&#39;)
                  credentials(&#39;nutcracker-rsa-key&#39;)
                }
                branches(&#39;*/main&#39;)
              }
            }
            scriptPath(&#39;path/to/Jenkinsfile&#39;)
          }
        }
      }
</code></pre>

<p>This configuration automatically creates a pipeline named Terraform<em>AWS</em>account and connects it to a Git repository containing the Jenkinsfile.</p>

<p>The SSH credential is referenced by its Jenkins credential ID, so the private key does not need to appear in the Job DSL script.</p>

<p>This approach allows me to version-control not only Jenkins system settings but also the pipeline definitions themselves.</p>

<p>Finally, I defined a Kubernetes Deployment that mounts the ConfigMap into the path specified by CASC<em>JENKINS</em>CONFIG</p>

<pre><code>
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins
  namespace: jenkins
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      containers:
        - name: jenkins
          image: registry.example.com/jenkins:1.0.0

          ports:
            - containerPort: 8080
            - containerPort: 50000

          env:
            - name: CASC_JENKINS_CONFIG
              value: /var/jenkins/casc_config

            - name: JENKINS_RSA_PRIVATE_KEY
              valueFrom:
                secretKeyRef:
                  name: jenkins-secrets
                  key: rsa-private-key

            - name: OIDC_CLIENT_SECRET
              valueFrom:
                secretKeyRef:
                  name: jenkins-secrets
                  key: oidc-client-secret

          volumeMounts:
            - name: jenkins-casc
              mountPath: /var/jenkins/casc_config
              readOnly: true

            - name: jenkins-home
              mountPath: /var/jenkins_home

      volumes:
        - name: jenkins-casc
          configMap:
            name: jenkins-casc

        - name: jenkins-home
          persistentVolumeClaim:
            claimName: jenkins-home
</code></pre>

<p>In this deployment:
* The custom image contains all required Jenkins plugins.
* The ConfigMap provides the JCasC YAML configuration.
* Kubernetes Secrets provide sensitive credentials through environment variables.
* A PersistentVolumeClaim stores Jenkins runtime data, build history, and other persistent state.
The PVC must be created separately. This example also assumes that Jenkins agents are configured independently or that the Docker-compatible endpoint defined in JCasC is available to the controller.</p>

<p>An important detail is that updating the ConfigMap does not automatically restart Jenkins or guarantee that the new configuration is applied. In my deployment process, I can trigger a rollout restart:</p>

<pre><code>kubectl rollout restart deployment/jenkins -n jenkins
</code></pre>

<p>Jenkins then loads the configuration from the mounted YAML file during startup.</p>

<h2 id="benefits-of-this-approach">Benefits of this approach</h2>

<p>By combining Kubernetes, a custom Jenkins image, JCasC, and Job DSL, I achieved a deployment model where most of the Jenkins configuration is defined declarativelyt.</p>

<p>The main benefits are reproducibility, simplified disaster recovery, version-controlled configuration, and reduced manual administration.</p>

<p>It also makes upgrading Jenkins easier: I can build a new image with updated plugins, validate the configuration in a test environment, and deploy the updated version through Kubernetes.</p>

<p>The most important improvement is that Jenkins is no longer configured primarily through its UI. Instead, its configuration becomes part of the infrastructure code, making changes easier to review, track, and reproduce.</p>
]]></content:encoded>
      <guid>https://nutcracker.one.pl/how-to-manage-jenkins-configuration-as-code-on-kubernetes</guid>
      <pubDate>Mon, 26 Oct 2026 19:37:26 +0100</pubDate>
    </item>
    <item>
      <title>NodeMCU - how to automate your home cheaply</title>
      <link>https://nutcracker.one.pl/nodemcu-how-to-automate-your-home-cheaply</link>
      <description>&lt;![CDATA[I like electronic smart home gadgets, but the problem is that popular hardware platforms like Arduino and Raspberry Pi can be relatively expensive. If you want to control all your power outlets via Wi-Fi, for example, the total cost can increase dramatically, as you need a separate device for each outlet you want to control. Fortunately, there are cheaper alternatives, such as NodeMCU.&#xA;!--more--&#xA;&#xA;NodeMCUschema&#xA;&#xA;This is really cheap and easy to work with &#xA;For around $3, you can buy an ESP board with a built-in relay, such as the ESP Relay 1.0 shown in the picture below.&#xA;&#xA;relay1&#xA;relay2&#xA;&#xA;The relay allows you to open or close an electrical circuit, which essentially means you can turn on, turn off, or power-cycle almost any AC-powered device .I power the ESP Relay board using a small 3V power adapter from an old Nokia phone. It&#39;s a simple and inexpensive way to reuse an old charger instead of buying a dedicated power supply.&#xA;&#xA;I probably don&#39;t need to explain how many things we can control using just our smartphones. :)&#xA;&#xA;The code below, uploaded to the ESP controller using Arduino IDE, starts a simple HTTP server that allows you to switch the relay on or off directly from your web browser.&#xA;&#xA;In my case, I&#39;ve found two particularly useful applications:&#xA;&#xA;Remotely rebooting my ISP&#39;s router, which is located in a hard-to-reach spot in the attic.&#xA;Automatically turning on my drip coffee maker at 6 AM using a simple curl command scheduled via cron impressed my wife. :D&#xA;&#xA;includeESP8266WiFi.h &#xA;&#xA;const char ssid = &#34;MyHomeWiFiSSID&#34;; //your WiFi Name&#xA;const char password = &#34;WiFiPassword&#34;;  //Your Wifi Password&#xA;int ledPin = 03; &#xA;WiFiServer server(80);&#xA;void setup() {&#xA;  Serial.begin(115200);&#xA;  delay(10); &#xA;  pinMode(ledPin, OUTPUT);&#xA;  digitalWrite(ledPin, LOW); &#xA;  Serial.println();&#xA;  Serial.println();&#xA;  Serial.print(&#34;Connecting to &#34;);&#xA;  Serial.println(ssid); &#xA;  WiFi.begin(ssid, password);&#xA;  while (WiFi.status() != WLCONNECTED) {&#xA;    delay(500);&#xA;    Serial.print(&#34;.&#34;);&#xA;  }&#xA;  Serial.println(&#34;&#34;);&#xA;  Serial.println(&#34;WiFi connected&#34;); &#xA;  server.begin();&#xA;  Serial.println(&#34;Server started&#34;);&#xA;  Serial.print(&#34;Use this URL to connect: &#34;);&#xA;  Serial.print(&#34;http://&#34;);&#xA;  Serial.print(WiFi.localIP());&#xA;  Serial.println(&#34;/&#34;); &#xA;}&#xA;&#xA;void loop() {&#xA;  WiFiClient client = server.available();&#xA;  if (!client) {&#xA;    return;&#xA;  }&#xA;  Serial.println(&#34;new client&#34;);&#xA;  while(!client.available()){&#xA;    delay(1);&#xA;  } &#xA;  String request = client.readStringUntil(&#39;\r&#39;);&#xA;  Serial.println(request);&#xA;  client.flush(); &#xA;  int value = LOW;&#xA;  if (request.indexOf(&#34;/LED=ON&#34;) != -1)  {&#xA;    digitalWrite(ledPin, HIGH);&#xA;    value = HIGH;&#xA;  }&#xA;  if (request.indexOf(&#34;/LED=OFF&#34;) != -1)  {&#xA;    digitalWrite(ledPin, LOW);&#xA;    value = LOW;&#xA;  }&#xA;  client.println(&#34;HTTP/1.1 200 OK&#34;);&#xA;  client.println(&#34;Content-Type: text/html&#34;);&#xA;  client.println(&#34;&#34;); &#xA;  client.println(&#34;&#34;);&#xA;  client.println(&#34;&#34;);&#xA;  client.print(&#34;Led is : &#34;); &#xA;  if(value == HIGH) {&#xA;    client.print(&#34;On&#34;);&#xA;  } else {&#xA;    client.print(&#34;Off&#34;);&#xA;  }  client.println(&#34;&#34;);  &#xA;client.println(&#34; On &#34;);&#xA;   client.println(&#34; Off &#34;);  &#xA;  client.println(&#34; &#34;);&#xA;     delay(1);&#xA;   Serial.println(&#34;Client disonnected&#34;);&#xA;  Serial.println(&#34;&#34;);   }&#xA;//code copied from link&#xA;&#xA;Keep in mind:&#xA;Exposing an unauthenticated HTTP endpoint that controls electrical equipment to the public internet is a bad idea. Keep it on a trusted local network or access it through a VPN. And second thing&#xA;&#xA; A relay ESP-01S is marked 10A/250V AC is not necessarily suitable for every 10A load. Motors, transformers, and other inductive loads can generate high inrush currents and voltage spikes.&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>I like electronic smart home gadgets, but the problem is that popular hardware platforms like Arduino and Raspberry Pi can be relatively expensive. If you want to control all your power outlets via Wi-Fi, for example, the total cost can increase dramatically, as you need a separate device for each outlet you want to control. Fortunately, there are cheaper alternatives, such as NodeMCU.
</p>

<p><img src="https://nutcracker.one.pl/nc_shared_files/nodemcu.jpeg" alt="NodeMCU_schema"></p>

<p>This is really cheap and easy to work with
For around $3, you can buy an ESP board with a built-in relay, such as the ESP Relay 1.0 shown in the picture below.</p>

<p><img src="https://nutcracker.one.pl/nc_shared_files/relay1.jpg" alt="relay1">
<img src="https://nutcracker.one.pl/nc_shared_files/relay2.jpg" alt="relay2"></p>

<p>The relay allows you to open or close an electrical circuit, which essentially means you can turn on, turn off, or power-cycle almost any AC-powered device .I power the ESP Relay board using a small 3V power adapter from an old Nokia phone. It&#39;s a simple and inexpensive way to reuse an old charger instead of buying a dedicated power supply.</p>

<p>I probably don&#39;t need to explain how many things we can control using just our smartphones. :)</p>

<p>The code below, uploaded to the ESP controller using Arduino IDE, starts a simple HTTP server that allows you to switch the relay on or off directly from your web browser.</p>

<p>In my case, I&#39;ve found two particularly useful applications:</p>
<ul><li>Remotely rebooting my ISP&#39;s router, which is located in a hard-to-reach spot in the attic.</li>
<li>Automatically turning on my drip coffee maker at 6 AM using a simple curl command scheduled via cron impressed my wife. :D</li></ul>

<pre><code>#include&lt;ESP8266WiFi.h&gt; 

const char* ssid = &#34;MyHomeWiFiSSID&#34;; //your WiFi Name
const char* password = &#34;WiFiPassword&#34;;  //Your Wifi Password
int ledPin = 03; 
WiFiServer server(80);
void setup() {
  Serial.begin(115200);
  delay(10); 
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW); 
  Serial.println();
  Serial.println();
  Serial.print(&#34;Connecting to &#34;);
  Serial.println(ssid); 
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(&#34;.&#34;);
  }
  Serial.println(&#34;&#34;);
  Serial.println(&#34;WiFi connected&#34;); 
  server.begin();
  Serial.println(&#34;Server started&#34;);
  Serial.print(&#34;Use this URL to connect: &#34;);
  Serial.print(&#34;http://&#34;);
  Serial.print(WiFi.localIP());
  Serial.println(&#34;/&#34;); 
}

void loop() {
  WiFiClient client = server.available();
  if (!client) {
    return;
  }
  Serial.println(&#34;new client&#34;);
  while(!client.available()){
    delay(1);
  } 
  String request = client.readStringUntil(&#39;\r&#39;);
  Serial.println(request);
  client.flush(); 
  int value = LOW;
  if (request.indexOf(&#34;/LED=ON&#34;) != -1)  {
    digitalWrite(ledPin, HIGH);
    value = HIGH;
  }
  if (request.indexOf(&#34;/LED=OFF&#34;) != -1)  {
    digitalWrite(ledPin, LOW);
    value = LOW;
  }
  client.println(&#34;HTTP/1.1 200 OK&#34;);
  client.println(&#34;Content-Type: text/html&#34;);
  client.println(&#34;&#34;); 
  client.println(&#34;&#34;);
  client.println(&#34;&#34;);
  client.print(&#34;Led is : &#34;); 
  if(value == HIGH) {
    client.print(&#34;On&#34;);
  } else {
    client.print(&#34;Off&#34;);
  }  client.println(&#34;&#34;);  
client.println(&#34; On &#34;);
   client.println(&#34; Off &#34;);  
  client.println(&#34; &#34;);
     delay(1);
   Serial.println(&#34;Client disonnected&#34;);
  Serial.println(&#34;&#34;);   }
//code copied from link
</code></pre>

<p>Keep in mind:
Exposing an unauthenticated HTTP endpoint that controls electrical equipment to the public internet is a bad idea. Keep it on a trusted local network or access it through a VPN. And second thing</p>

<p> A relay ESP-01S is marked 10A/250V AC is not necessarily suitable for every 10A load. Motors, transformers, and other inductive loads can generate high inrush currents and voltage spikes.</p>
]]></content:encoded>
      <guid>https://nutcracker.one.pl/nodemcu-how-to-automate-your-home-cheaply</guid>
      <pubDate>Thu, 15 Oct 2026 09:49:51 +0200</pubDate>
    </item>
    <item>
      <title>Hangman game in Rust: a story about an API you can use for free</title>
      <link>https://nutcracker.one.pl/hangman-game-in-rust-a-story-about-an-api-you-can-use</link>
      <description>&lt;![CDATA[Recently, I took part in a Rust programming course, and one of the tasks we had to do as homework was a &#34;hangman&#34; game. The funny thing is that to create a game with real words, not just &#34;random&#34; interpolated letters, I had to find a library that can take a random word from a dictionary. !--more--&#xA; You can find the library commented at the top of the code: use randomword::Lang;&#xA;The problem was that the library I found did not support the Polish language, and my kids did not want to play. Then I started thinking, maybe there is an API publicly available where I could get a random Polish word. And of course there are some; take a look at the example https://api.poocoo.pl. Below, you can find the snippet of code I wrote:&#xA;&#xA;//use randomword::Lang;&#xA;use std::io::{self, Write};&#xA;use std::process::ExitCode;&#xA;use serde::Deserialize;&#xA;&#xA;fn guessword(s: &amp;str) {&#xA;    let mut chance : i8 = 6;&#xA;    let mut ch : char = &#39; &#39;;&#xA;    println!(&#34;The word has {} letters.&#34;, s.chars().count());&#xA;    let mut chararray: Vecchar = vec![&#39;&#39;; s.chars().count()];&#xA;    loop {&#xA;            println!(&#34;--- Hangman game ---&#34;);&#xA;            println!(&#34;Guess the word! You have 6 tries to win the game.&#34;);&#xA;            println!(&#34;Enter your letter that might me in the word (A-Z): &#34;);&#xA;&#xA;            println!(&#34;Currently word looks like this:&#34;);&#xA;            chararray.iter().foreach(|c| print!(&#34; {} &#34;, c));&#xA;&#xA;            println!(&#34;\n&#34;);&#xA;            let mut input = String::new();&#xA;            io::stdin().readline(&amp;mut input);&#xA;&#xA;            let trimmed = input.trim();&#xA;            if trimmed.chars().count() != 1 {&#xA;                println!(&#34;Please enter exactly one letter.&#34;);&#xA;            }else {&#xA;                ch = trimmed.chars().next().unwrap();&#xA;            }&#xA;&#xA;            println!(&#34;Processing letter: {:?}&#34;, ch);&#xA;            if s.contains(ch) {&#xA;                println!(&#34;Letter {:?} is already guessed.&#34;, ch);&#xA;                chararray.itermut().enumerate().foreach(|(i, c)| {&#xA;                    if s.chars().nth(i).unwrap() == ch {&#xA;                        *c = ch;&#xA;                    }&#xA;                });&#xA;            } else {&#xA;                println!(&#34;Letter {:?} is not guessed yet.&#34;, ch);&#xA;                chance -= 1;&#xA;            }&#xA;&#xA;            if !chararray.contains(&amp;&#39;&#39;) {&#xA;                println!(&#34;You won the game!&#34;);&#xA;                println!(&#34;The word was: &#34;,);&#xA;                chararray.iter().foreach(|c| print!(&#34; {} &#34;, c));&#xA;                println!(&#34;\n&#34;);&#xA;                std::process::exit(0);&#xA;            }&#xA;            hungman(chance, &amp;s);&#xA;    }   &#xA;}&#xA;&#xA;fn hungman(i : i8, s : &amp;str) {&#xA;    match i &#xA;    {&#xA;        6 =  {&#xA;            println!(&#xA;                r#&#34;&#xA;        ============================================&#xA;&#xA;                ^&#xA;        ============================================&#xA;        &#34;#&#xA;            );&#xA;        }&#xA;        5 =  {&#xA;        println!(&#xA;                r#&#34;&#xA;        ============================================&#xA;&#xA;                |&#xA;                ^&#xA;        ============================================&#xA;        &#34;#&#xA;            );&#xA;    }&#xA;    4 =  {&#xA;        println!(&#xA;                r#&#34;&#xA;        ============================================&#xA;&#xA;                |&#xA;                |&#xA;                ^&#xA;        ============================================&#xA;        &#34;#&#xA;            );&#xA;    }&#xA;    3 =  {&#xA;        println!(&#xA;            r#&#34;&#xA;        ============================================&#xA;                &#xA;                |&#xA;                |&#xA;                ^&#xA;        ============================================&#xA;        &#34;#&#xA;            );&#xA;    }&#xA;    2 =  {&#xA;        println!(&#xA;            r#&#34;&#xA;        ============================================&#xA;                &#xA;                |   O&#xA;                |&#xA;                ^&#xA;        ============================================&#xA;        &#34;#&#xA;            );&#xA;    }&#xA;    1 =  {&#xA;        println!(&#xA;            r#&#34;&#xA;        ============================================&#xA;                &#xA;                |   O&#xA;                |  /|\&#xA;                ^   &#xA;        ============================================&#xA;        &#34;#&#xA;            );&#xA;    }&#xA;    0 =  {&#xA;        println!(&#xA;            r#&#34;&#xA;        ============================================&#xA;                &#xA;                |   O&#xA;                |  /|\&#xA;                ^  / \&#xA;        ============================================&#xA;        &#34;#&#xA;            );&#xA;        println!(&#34;You lost the game!&#34;);&#xA;        println!(&#34;The word was: {:?}&#34;, s);&#xA;        println!(&#34;\n&#34;);&#xA;        std::process::exit(1);&#xA;        }&#xA;     =  {&#xA;        println!( &#34;Something went wrong, please try again!&#34;  );&#xA;        }&#xA;    }&#xA;}&#xA;&#xA;[derive(Deserialize)]&#xA;struct ApiResponse {&#xA;    data: WordsPage,&#xA;}&#xA;&#xA;[derive(Deserialize)]&#xA;[serde(renameall = &#34;camelCase&#34;)]&#xA;struct WordsPage {&#xA;    words: VecString,&#xA;    totalpages: u32,&#xA;}&#xA;&#xA;fn fetchwordspage(wordlength: u32, page: u32) -  ResultWordsPage, reqwest::Error {&#xA;    let url = format!(&#34;https://api.poocoo.pl/api/v1/words?length={wordlength}&amp;page={page}&#34;);&#xA;    let response: ApiResponse = reqwest::blocking::get(&amp;url)?.json()?;&#xA;    Ok(response.data)&#xA;}&#xA;&#xA;fn getrandomwordfromrestapi(wordlength: u32) -  ResultString, reqwest::Error {&#xA;    let totalpages = fetchwordspage(wordlength, 1)?.totalpages;&#xA;    println!(&#34;totalPages = {totalpages}&#34;);&#xA;&#xA;    let page = fetchwordspage(wordlength, rand::randomrange(1..=totalpages))?;&#xA;    let index = rand::randomrange(0..page.words.len());&#xA;    Ok(page.words[index].clone())&#xA;}&#xA;&#xA;fn main() {&#xA;    let wordlength = rand::randomrange(3..=10);&#xA;    println!(&#34;Random search for a word in a dictionary&#34;);&#xA;    //let word = randomword::getlen(wordlength, Lang::En);&#xA;    let word = getrandomwordfromrestapi(wordlength).expect(&#34;failed to fetch word&#34;);&#xA;    //println!(&#34;Found word: {:?}&#34;, word);&#xA;    guessword(&amp;word);&#xA;}&#xA;&#xA;This is a publicly available REST API you can use for your projects and tests. Be careful about the number of calls you send, as sometimes it can return an HTTP 429 Error, this means you exceeded the rate limit. Don&#39;t overdo it; they accept 120 requests per minute per IP address.]]&gt;</description>
      <content:encoded><![CDATA[<p>Recently, I took part in a Rust programming course, and one of the tasks we had to do as homework was a “hangman” game. The funny thing is that to create a game with real words, not just “random” interpolated letters, I had to find a library that can take a random word from a dictionary. 
 You can find the library commented at the top of the code: <code>use random_word::Lang;</code>
The problem was that the library I found did not support the Polish language, and my kids did not want to play. Then I started thinking, maybe there is an API publicly available where I could get a random Polish word. And of course there are some; take a look at the example <a href="https://api.poocoo.pl">https://api.poocoo.pl</a>. Below, you can find the snippet of code I wrote:</p>

<pre><code class="language-rust">//use random_word::Lang;
use std::io::{self, Write};
use std::process::ExitCode;
use serde::Deserialize;

fn guess_word(s: &amp;str) {
    let mut chance : i8 = 6;
    let mut ch : char = &#39; &#39;;
    println!(&#34;The word has {} letters.&#34;, s.chars().count());
    let mut char_array: Vec&lt;char&gt; = vec![&#39;_&#39;; s.chars().count()];
    loop {
            println!(&#34;--- Hangman game ---&#34;);
            println!(&#34;Guess the word! You have 6 tries to win the game.&#34;);
            println!(&#34;Enter your letter that might me in the word (A-Z): &#34;);

            println!(&#34;Currently word looks like this:&#34;);
            char_array.iter().for_each(|c| print!(&#34; {} &#34;, c));

            println!(&#34;\n&#34;);
            let mut input = String::new();
            io::stdin().read_line(&amp;mut input);

            let trimmed = input.trim();
            if trimmed.chars().count() != 1 {
                println!(&#34;Please enter exactly one letter.&#34;);
            }else {
                ch = trimmed.chars().next().unwrap();
            }

            println!(&#34;Processing letter: {:?}&#34;, ch);
            if s.contains(ch) {
                println!(&#34;Letter {:?} is already guessed.&#34;, ch);
                char_array.iter_mut().enumerate().for_each(|(i, c)| {
                    if s.chars().nth(i).unwrap() == ch {
                        *c = ch;
                    }
                });
            } else {
                println!(&#34;Letter {:?} is not guessed yet.&#34;, ch);
                chance -= 1;
            }

            if !char_array.contains(&amp;&#39;_&#39;) {
                println!(&#34;You won the game!&#34;);
                println!(&#34;The word was: &#34;,);
                char_array.iter().for_each(|c| print!(&#34; {} &#34;, c));
                println!(&#34;\n&#34;);
                std::process::exit(0);
            }
            hungman(chance, &amp;s);
    }   
}

fn hungman(i : i8, s : &amp;str) {
    match i 
    {
        6 =&gt; {
            println!(
                r#&#34;
        ============================================



                ^
        ============================================
        &#34;#
            );
        }
        5 =&gt; {
        println!(
                r#&#34;
        ============================================


                |
                ^
        ============================================
        &#34;#
            );
    }
    4 =&gt; {
        println!(
                r#&#34;
        ============================================

                |
                |
                ^
        ============================================
        &#34;#
            );
    }
    3 =&gt; {
        println!(
            r#&#34;
        ============================================
                ____
                |
                |
                ^
        ============================================
        &#34;#
            );
    }
    2 =&gt; {
        println!(
            r#&#34;
        ============================================
                ____
                |   O
                |
                ^
        ============================================
        &#34;#
            );
    }
    1 =&gt; {
        println!(
            r#&#34;
        ============================================
                ____
                |   O
                |  /|\
                ^   
        ============================================
        &#34;#
            );
    }
    0 =&gt; {
        println!(
            r#&#34;
        ============================================
                ____
                |   O
                |  /|\
                ^  / \
        ============================================
        &#34;#
            );
        println!(&#34;You lost the game!&#34;);
        println!(&#34;The word was: {:?}&#34;, s);
        println!(&#34;\n&#34;);
        std::process::exit(1);
        }
    _ =&gt; {
        println!( &#34;Something went wrong, please try again!&#34;  );
        }
    }
}

#[derive(Deserialize)]
struct ApiResponse {
    data: WordsPage,
}

#[derive(Deserialize)]
#[serde(rename_all = &#34;camelCase&#34;)]
struct WordsPage {
    words: Vec&lt;String&gt;,
    total_pages: u32,
}

fn fetch_words_page(word_length: u32, page: u32) -&gt; Result&lt;WordsPage, reqwest::Error&gt; {
    let url = format!(&#34;https://api.poocoo.pl/api/v1/words?length={word_length}&amp;page={page}&#34;);
    let response: ApiResponse = reqwest::blocking::get(&amp;url)?.json()?;
    Ok(response.data)
}

fn get_random_word_from_rest_api(word_length: u32) -&gt; Result&lt;String, reqwest::Error&gt; {
    let total_pages = fetch_words_page(word_length, 1)?.total_pages;
    println!(&#34;totalPages = {total_pages}&#34;);

    let page = fetch_words_page(word_length, rand::random_range(1..=total_pages))?;
    let index = rand::random_range(0..page.words.len());
    Ok(page.words[index].clone())
}

fn main() {
    let word_length = rand::random_range(3..=10);
    println!(&#34;Random search for a word in a dictionary&#34;);
    //let word = random_word::get_len(word_length, Lang::En);
    let word = get_random_word_from_rest_api(word_length).expect(&#34;failed to fetch word&#34;);
    //println!(&#34;Found word: {:?}&#34;, word);
    guess_word(&amp;word);
}


</code></pre>

<p>This is a publicly available REST API you can use for your projects and tests. Be careful about the number of calls you send, as sometimes it can return an HTTP 429 Error, this means you exceeded the rate limit. Don&#39;t overdo it; they accept 120 requests per minute per IP address.</p>
]]></content:encoded>
      <guid>https://nutcracker.one.pl/hangman-game-in-rust-a-story-about-an-api-you-can-use</guid>
      <pubDate>Thu, 15 Oct 2026 05:43:51 +0200</pubDate>
    </item>
    <item>
      <title>mod_security setup and  domestication on your Apache server</title>
      <link>https://nutcracker.one.pl/mod_security-setup-and-domestication-on-your-apache-server</link>
      <description>&lt;![CDATA[If you’ve ever worked with a Web Application Firewall (WAF), you probably know how challenging it can be to deploy one in a real-world production environment. This is especially true when running it in blocking mode. Once enabled, a WAF can unexpectedly interfere with legitimate requests, causing certain features of the underlying application to stop working because requests are blocked and users receive 4xx HTTP status codes.&#xA;&#xA;!--more--&#xA;&#xA;ModSecurity is a WAF module which integrates with Apache HTTP Server and analyses incoming traffic using predefined security rules. It is commonly deployed with the OWASP Core Rule Set (CRS), which provides protection against common web application attacks, this is how I installed it on Fedora Linux Server for my Nextcloud&#xA;&#xA;Instalation and basic config&#xA;&#xA; dnf install httpd modsecurity modsecuritycrs&#xA;Restart Apache to load the module :&#xA;systemctl restart httpd&#xA;Check if is loaded:&#xA; httpd -M | grep security&#xA;Expected:&#xA;security2module (shared)&#xA;&#xA;The main ModSecurity configuration file is typically located at:&#xA;/etc/httpd/conf.d/modsecurity.conf&#xA;Edit /etc/httpd/conf.d/modsecurity.conf and set:&#xA;SecRuleEngine DetectionOnly&#xA;&#xA;In this mode, ModSecurity evaluates incoming HTTP requests and logs potential security violations, but does not execute disruptive actions defined by WAF rules.&#xA;&#xA;SecAuditEngine RelevantOnly&#xA;SecAuditLog /var/log/httpd/modsecaudit.log&#xA;SecAuditLogParts ABIJDEFHZ&#xA;&#xA;And here is the important thing: DetectionOnly does not disable ModSecurity. Security rules are still evaluated, and suspicious requests are logged. This makes it possible to understand how the WAF behaves with real production traffic before enforcing blocking rules.&#xA;&#xA;Monitoring:&#xA;Look at files like this:&#xA;&#xA;/var/log/httpd/modsecaudit.log&#x9;(Detailed WAF security events)&#xA;/var/log/httpd/errorlog&#x9;(ModSecurity warnings and Apache errors)&#xA;/var/log/httpd/accesslog&#x9;(HTTP requests and response status codes)&#xA;&#xA;or monitor all events i you have more files like me:&#xA;tail -f /var/log/httpd/&#xA;&#xA;This is really helpful grep which you can use to find the Rule Ids that were triggered:&#xA;&#xA;grep -oE &#39;\[id &#34;[0-9]+&#34;\]&#39; /var/log/httpd/ | sort | uniq -c | sort -nr&#xA;&#xA;For Fedora installations using OWASP CRS 4, exclusions can typically be placed in:&#xA;/etc/httpd/modsecurity.d/activatedrules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf&#xA;&#xA;For example, suppose the /api/editor/save endpoint accepts HTML content and triggers false positives for rule 941100.&#xA;Instead of disabling the rule globally, exclude a specific parameter for that endpoint:&#xA;&#xA;SecRule REQUESTURI &#34;@streq /api/editor/save&#34; &#34;id:10001,phase:1,pass,nolog,t:none,ctl:ruleRemoveTargetById=941100;ARGS:content&#34;&#xA;&#xA;It is also possible to disable an entire rule for a particular endpoint:&#xA;SecRule REQUESTURI &#34;@streq /api/legacy&#34;  &#34;id:10002,phase:1,pass,nolog,t:none,ctl:ruleRemoveById=942100&#34;&#xA;&#xA;Validate and reload Apache:&#xA;apachectl configtest&#xA;systemctl reload httpd&#xA;&#xA;Best practices for exclusions:&#xA;&#xA;Prefer parameter-level exclusions over disabling entire rules.&#xA;Restrict exclusions to specific endpoints whenever possible.&#xA;Assign unique IDs to custom rules.&#xA;Document the reason for each exclusion.&#xA;&#xA;Change the mode &#xA;&#xA;Finally, you can switch to active blocking mode; it is simply as simple as changing:&#xA;&#xA;SecRuleEngine DetectionOnly&#xA;to &#xA;SecRuleEngine On&#xA;&#xA;modsecurity can work on a side server in proxy mode with modproxy on Apache.&#xA;&#xA;A long time ago, I implemented this for the website szkla.com. The architecture diagram:&#xA;&#xA;+----------+           +--------------------+       +--------------+&#xA;|                    |        | Apache WAF Server  |       |                            |&#xA;|  Client        | --  | ModSecurity             | --  | Backend Server |&#xA;|                    |        | modproxy               |       |                            |&#xA;+----------+             +------------------+       +---------------+&#xA;                              |&#xA;                              | Blocked&#xA;                              v&#xA;                         HTTP 403&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>If you’ve ever worked with a Web Application Firewall (WAF), you probably know how challenging it can be to deploy one in a real-world production environment. This is especially true when running it in blocking mode. Once enabled, a WAF can unexpectedly interfere with legitimate requests, causing certain features of the underlying application to stop working because requests are blocked and users receive 4xx HTTP status codes.</p>



<p>ModSecurity is a WAF module which integrates with Apache HTTP Server and analyses incoming traffic using predefined security rules. It is commonly deployed with the OWASP Core Rule Set (CRS), which provides protection against common web application attacks, this is how I installed it on Fedora Linux Server for my Nextcloud</p>

<h2 id="instalation-and-basic-config">Instalation and basic config</h2>

<pre><code> dnf install httpd mod_security mod_security_crs
</code></pre>

<p>Restart Apache to load the module :</p>

<pre><code>systemctl restart httpd
</code></pre>

<p>Check if is loaded:</p>

<pre><code> httpd -M | grep security
</code></pre>

<p>Expected:</p>

<pre><code>security2_module (shared)
</code></pre>

<p>The main ModSecurity configuration file is typically located at:
<code>/etc/httpd/conf.d/mod_security.conf</code>
Edit /etc/httpd/conf.d/mod_security.conf and set:</p>

<pre><code>SecRuleEngine DetectionOnly
</code></pre>

<p>In this mode, ModSecurity evaluates incoming HTTP requests and logs potential security violations, but does not execute disruptive actions defined by WAF rules.</p>

<pre><code>SecAuditEngine RelevantOnly
SecAuditLog /var/log/httpd/modsec_audit.log
SecAuditLogParts ABIJDEFHZ
</code></pre>

<p>And here is the important thing: DetectionOnly does not disable ModSecurity. Security rules are still evaluated, and suspicious requests are logged. This makes it possible to understand how the WAF behaves with real production traffic before enforcing blocking rules.</p>

<h2 id="monitoring">Monitoring:</h2>

<p>Look at files like this:</p>
<ul><li>/var/log/httpd/modsec_audit.log   (Detailed WAF security events)</li>
<li>/var/log/httpd/error_log  (ModSecurity warnings and Apache errors)</li>
<li>/var/log/httpd/access_log (HTTP requests and response status codes)</li></ul>

<p>or monitor all events i you have more files like me:</p>

<pre><code>tail -f /var/log/httpd/*
</code></pre>

<p>This is really helpful grep which you can use to find the Rule Ids that were triggered:</p>

<pre><code>grep -oE &#39;\[id &#34;[0-9]+&#34;\]&#39; /var/log/httpd/* | sort | uniq -c | sort -nr
</code></pre>

<p>For Fedora installations using OWASP CRS 4, exclusions can typically be placed in:</p>

<pre><code>/etc/httpd/modsecurity.d/activated_rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
</code></pre>

<p>For example, suppose the /api/editor/save endpoint accepts HTML content and triggers false positives for rule 941100.
Instead of disabling the rule globally, exclude a specific parameter for that endpoint:</p>

<pre><code>SecRule REQUEST_URI &#34;@streq /api/editor/save&#34; &#34;id:10001,phase:1,pass,nolog,t:none,ctl:ruleRemoveTargetById=941100;ARGS:content&#34;
</code></pre>

<p>It is also possible to disable an entire rule for a particular endpoint:</p>

<pre><code>SecRule REQUEST_URI &#34;@streq /api/legacy&#34;  &#34;id:10002,phase:1,pass,nolog,t:none,ctl:ruleRemoveById=942100&#34;
</code></pre>

<p>Validate and reload Apache:</p>

<pre><code>apachectl configtest
systemctl reload httpd
</code></pre>

<h2 id="best-practices-for-exclusions">Best practices for exclusions:</h2>
<ul><li>Prefer parameter-level exclusions over disabling entire rules.</li>
<li>Restrict exclusions to specific endpoints whenever possible.</li>
<li>Assign unique IDs to custom rules.</li>
<li>Document the reason for each exclusion.</li></ul>

<h2 id="change-the-mode">Change the mode</h2>

<p>Finally, you can switch to active blocking mode; it is simply as simple as changing:</p>

<pre><code>SecRuleEngine DetectionOnly
</code></pre>

<p>to</p>

<pre><code>SecRuleEngine On
</code></pre>

<p>mod<em>security can work on a side server in proxy mode with mod</em>proxy on Apache.</p>

<p>A long time ago, I implemented this for the website szkla.com. The architecture diagram:</p>

<p>+—————+           +——————————+       +———————+
|                    |        | Apache WAF Server  |       |                            |
|  Client        | —&gt; | ModSecurity             | —&gt; | Backend Server |
|                    |        | mod_proxy               |       |                            |
+—————+             +—————————+       +———————–+
                              |
                              | Blocked
                              v
                         HTTP 403</p>
]]></content:encoded>
      <guid>https://nutcracker.one.pl/mod_security-setup-and-domestication-on-your-apache-server</guid>
      <pubDate>Mon, 10 Nov 2025 20:16:42 +0100</pubDate>
    </item>
  </channel>
</rss>