<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>arturz</title><description>Software Engineer</description><link>https://arturz.com</link><item><title>Getting the client&apos;s IP in the cloud: an example in Elixir</title><link>https://arturz.com/blog/getting-the-clients-ip-in-the-cloud-elixir</link><guid isPermaLink="true">https://arturz.com/blog/getting-the-clients-ip-in-the-cloud-elixir</guid><description>How to correctly identify client IPs when your Elixir application is running behind a load balancer.</description><pubDate>Thu, 21 May 2026 08:08:09 GMT</pubDate><content:encoded>&lt;p&gt;A misconfigured rate limiter can be worse than no rate limiter. It gives you a false sense of confidence, while letting malicious users silently spoof their own IP address.&lt;/p&gt;
&lt;p&gt;This is surprisingly easy to get wrong in a cloud environment. Your server sees the IP of the last network node, which is the load balancer, not the real user. The proxy (AWS ALB, Nginx, Cloudflare) forwards the original client IP in the &lt;code&gt;X-Forwarded-For&lt;/code&gt; header, but &lt;a href=&quot;https://github.com/elixir-plug/plug/issues/319&quot;&gt;you have to parse it yourself&lt;/a&gt;, and getting it wrong makes IP spoofing possible.&lt;/p&gt;
&lt;p&gt;None of the techniques below work if your origin is reachable from the public internet without a load balancer or reverse proxy, because an attacker can just pass their own header.&lt;/p&gt;
&lt;h2&gt;Where is the client&apos;s IP&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;X-Forwarded-For&lt;/code&gt; (XFF) header typically has the following form: let&apos;s say the real client&apos;s IP is &lt;code&gt;5.5.5.5&lt;/code&gt; and the request passes through a load balancer (LB) with internal address &lt;code&gt;10.0.0.1&lt;/code&gt;. The user can add two fake IPs in their own XFF header before sending the request: &lt;code&gt;1.1.1.1&lt;/code&gt; and &lt;code&gt;2.2.2.2&lt;/code&gt;, for example with a &lt;code&gt;curl&lt;/code&gt; command like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl https://yourapp.com/login \
  -H &quot;X-Forwarded-For: 1.1.1.1&quot; \
  -H &quot;X-Forwarded-For: 2.2.2.2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9110#section-5.2&quot;&gt;HTTP specification&lt;/a&gt; allows repeated headers to be merged into a single comma-separated value, and most LBs do this in practice. The final header received by the server is&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;X-Forwarded-For: 1.1.1.1, 2.2.2.2, 5.5.5.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first two IPs come from the user&apos;s spoofed headers, followed by the real client&apos;s IP.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/blog/load-balancer-diagram-evil.svg&quot; alt=&quot;Diagram showing a user sending request with spoofed X-Forwarded-For headers through a load balancer to backend server instances&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Parsing from right to left&lt;/h2&gt;
&lt;p&gt;Each proxy appends the network layer IP &lt;a href=&quot;https://adam-p.ca/blog/2022/03/x-forwarded-for/#it-cant-be-that-hard-to-get-the-real-client-ip-right&quot;&gt;it saw the request come from&lt;/a&gt; to the end of XFF. The strategy is simple: parse from right to left, ignore all proxies&apos; CIDRs, and the first non-proxy IP is the real client&apos;s address.&lt;/p&gt;
&lt;p&gt;Fortunately, in single load balancer setups, the last element in &lt;code&gt;X-Forwarded-For&lt;/code&gt; is usually the client&apos;s IP.&lt;/p&gt;
&lt;p&gt;| Provider       | Client&apos;s IP position in XFF | Note                             |
| -------------- | --------------------------- | -------------------------------- |
| &lt;strong&gt;AWS&lt;/strong&gt;        | Last                        | -                                |
| &lt;strong&gt;GCP&lt;/strong&gt;        | Second-to-last              | Exception: last is load balancer&apos;s IP |
| &lt;strong&gt;Azure&lt;/strong&gt;      | Last                        | App Gateway appends &lt;code&gt;IP:port&lt;/code&gt;    |
| &lt;strong&gt;Cloudflare&lt;/strong&gt; | Last                        | Prefer &lt;code&gt;CF-Connecting-IP&lt;/code&gt; header |&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(checked in May 2026)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Multiple proxies complicate this, as covered below.&lt;/p&gt;
&lt;h2&gt;Configuration in Elixir&lt;/h2&gt;
&lt;h3&gt;The simple approach&lt;/h3&gt;
&lt;p&gt;Assuming the user&apos;s IP is the last XFF element, let&apos;s create a simple function to be used in a Phoenix plug or WebSocket connection handler for extracting the right address.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;def get_remote_ip(x_forwarded_for_header) do
  x_forwarded_for_header
  |&gt; String.split(&quot;,&quot;)
  |&gt; Enum.map(&amp;#x26;String.trim/1)
  |&gt; Enum.map(&amp;#x26;to_charlist/1)
  |&gt; Enum.map(&amp;#x26;:inet.parse_strict_address/1)
  |&gt; Enum.filter(fn result -&gt; match?({:ok, _}, result) end)
  |&gt; Enum.map(fn {:ok, ip} -&gt; ip end)
  |&gt; List.last()
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This shows the mechanics, but it has a few caveats:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;returns the load balancer&apos;s IP on GCP, because it appends the client&apos;s IP, then its own&lt;/li&gt;
&lt;li&gt;can return the attacker&apos;s IP on Azure with App Gateway, because it appends &lt;code&gt;IP:port&lt;/code&gt;, &lt;code&gt;parse_strict_address&lt;/code&gt; fails, and those entries get filtered out&lt;/li&gt;
&lt;li&gt;breaks the moment you have more proxies&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Advanced approach with remote_ip library&lt;/h3&gt;
&lt;p&gt;There is a library named &lt;a href=&quot;https://hex.pm/packages/remote_ip&quot;&gt;remote_ip&lt;/a&gt;, which has a familiar API and allows more advanced configuration, such as specifying known proxy CIDRs to ignore when parsing the XFF header. This makes both simple environments and multiple proxies cases easy to set up.&lt;/p&gt;
&lt;p&gt;By default it looks for common headers such as &lt;code&gt;X-Forwarded-For&lt;/code&gt;, &lt;code&gt;X-Real-Ip&lt;/code&gt;, and &lt;code&gt;X-Client-Ip&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defmodule MyApp.Router do
  use Plug.Router

  plug RemoteIp

  # ...
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the plug runs, &lt;code&gt;conn.remote_ip&lt;/code&gt; is overwritten, so rate limiters, logs, and audit trails see the real client&apos;s IP address.&lt;/p&gt;
&lt;p&gt;Private IP ranges are recognised as proxies by default. For more control, you can pass options:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;plug RemoteIp,
  headers: [&quot;x-forwarded-for&quot;],
  proxies: [&quot;192.0.2.0/24&quot;], # optional proxy CIDRs
  clients: [] # optional trusted clients (even if in proxies range)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this config, the two &lt;code&gt;192.0.2.*&lt;/code&gt; additional proxies&apos; addresses are stripped and &lt;code&gt;5.5.5.5&lt;/code&gt; is set as &lt;code&gt;remote_ip&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;X-Forwarded-For: 1.1.1.1, 2.2.2.2, 5.5.5.5, 192.0.2.1, 192.0.2.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On GCP, configure it the same way:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;plug RemoteIp,
  headers: [&quot;x-forwarded-for&quot;],
  proxies: [
    # GCP load balancers
    &quot;35.191.0.0/16&quot;,
    &quot;130.211.0.0/22&quot;
  ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It handles XFF header with GCP load balancer&apos;s address, e.g.:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;X-Forwarded-For: 1.1.1.1, 2.2.2.2, 5.5.5.5, 35.191.5.10
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Using it in a controller&lt;/h3&gt;
&lt;p&gt;With the plug in place, a rate limiter like &lt;a href=&quot;https://hex.pm/packages/hammer&quot;&gt;Hammer&lt;/a&gt; can use the updated &lt;code&gt;remote_ip&lt;/code&gt; connection&apos;s field directly as a bucket key.&lt;/p&gt;
&lt;p&gt;Define a rate limiter module and start it in your supervision tree:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defmodule MyApp.RateLimit do
  use Hammer, backend: :ets
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you can use it in your controller:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defmodule MyAppWeb.SessionController do
  use MyAppWeb, :controller

  def create(conn, _params) do
    ip = conn.remote_ip |&gt; :inet.ntoa() |&gt; to_string()

    case MyApp.RateLimit.hit(&quot;login:#{ip}&quot;, :timer.minutes(1), 5) do
      {:allow, _count} -&gt;
        # ... authenticate the user
        conn |&gt; put_status(:ok) |&gt; json(%{ok: true})

      {:deny, _retry_after_ms} -&gt;
        conn |&gt; put_status(:too_many_requests) |&gt; json(%{error: &quot;rate limited&quot;})
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows 5 login attempts per minute per client IP before returning &lt;code&gt;429 Too Many Requests&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Lock down the origin&lt;/h2&gt;
&lt;p&gt;One thing the &lt;code&gt;remote_ip&lt;/code&gt; library and our custom function cannot solve: it&apos;s only safe if your app is really behind the load balancer. If the origin is reachable from the public internet without a LB, an attacker can spoof their IP by passing their own &lt;code&gt;X-Forwarded-For&lt;/code&gt;. You need to lock the origin down at the network level (security group, VPC, firewall) so that the load balancer is the only way in.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The two main takeaways are to parse &lt;code&gt;X-Forwarded-For&lt;/code&gt; from right to left, and to trust only what came from a load balancer or reverse proxy you control, which only works if your app is actually behind it.&lt;/p&gt;</content:encoded><h:img src="/_astro/img-guard.BeTjCr06.jpg"/><enclosure url="/_astro/img-guard.BeTjCr06.jpg"/></item><item><title>Why opening a PR turns your source branch red in GitHub Actions</title><link>https://arturz.com/blog/why-opening-a-pr-turns-your-source-branch-red-in-github-actions</link><guid isPermaLink="true">https://arturz.com/blog/why-opening-a-pr-turns-your-source-branch-red-in-github-actions</guid><description>How a shared commit hash between your branch and a pull request can turn your passing CI red.</description><pubDate>Tue, 17 Feb 2026 07:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Imagine the following scenario: you merge the latest changes to the &lt;em&gt;main&lt;/em&gt; branch, and CI passes ✅. Everything&apos;s looking good!&lt;/p&gt;
&lt;p&gt;You have a standard GitHub Actions workflow that runs CI on every push and pull request on both &lt;em&gt;main&lt;/em&gt; and &lt;em&gt;staging&lt;/em&gt; branches:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;on:
  pull_request:
    branches: [ main, staging ]
  push:
    branches: [ main, staging ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You also have a CD configured that automatically deploys the latest changes from &lt;em&gt;staging&lt;/em&gt; branch, so you create a PR to merge changes from &lt;em&gt;main&lt;/em&gt; to &lt;em&gt;staging&lt;/em&gt; to live test new features in the staging environment.&lt;/p&gt;
&lt;p&gt;Suddenly, CI status on the &lt;em&gt;main&lt;/em&gt; branch changes from ✅ to ❌️.&lt;/p&gt;
&lt;p&gt;What happened?&lt;/p&gt;
&lt;p&gt;You see that the PR failed. GitHub Actions attaches check results to &lt;strong&gt;commit hashes&lt;/strong&gt;, not branches. The status you see on a branch is the status of its latest commit, and since your &lt;em&gt;main&lt;/em&gt; branch and new PR both point to the same commit, the PR&apos;s check failure shows up on &lt;em&gt;main&lt;/em&gt; too.&lt;/p&gt;
&lt;p&gt;If you go into failure details on your &lt;em&gt;main&lt;/em&gt; branch, you&apos;ll see both checks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;❌️ CI / build (pull_request)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;✅ CI / build (push)&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And because at least one of them is failing, the whole check is red.&lt;/p&gt;
&lt;h2&gt;What can be done about it&lt;/h2&gt;
&lt;p&gt;A modern DevOps industry standard is to use the &lt;strong&gt;Build Once, Deploy Anywhere&lt;/strong&gt; pattern instead of &lt;strong&gt;environment branching&lt;/strong&gt; to deploy to different environments.&lt;/p&gt;
&lt;p&gt;But there are a lot of legacy codebases and simpler setups out there, and there&apos;s no simple solution that does not have its trade-off.&lt;/p&gt;
&lt;p&gt;You can create a GitHub branch protection rule to only require a &lt;code&gt;push&lt;/code&gt; check on &lt;em&gt;main&lt;/em&gt; for passing ✅. But by the time someone creates a new check, let&apos;s say for E2E tests, you will likely have forgotten about this rule, and it won&apos;t show on &lt;em&gt;main&lt;/em&gt; as a failure ❌️ until someone notices. Right now, it&apos;s not possible to create a rule that requires &lt;strong&gt;all checks except&lt;/strong&gt; &lt;code&gt;pull_request&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can also configure your GitHub Actions workflow not to run checks on PRs to &lt;em&gt;staging&lt;/em&gt;, but then you&apos;ll find out about errors only after they are merged into &lt;em&gt;staging&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;Resolution&lt;/h2&gt;
&lt;p&gt;After fixing failing CI on new PR and rerunning checks (or after merging it and pushing new commits to &lt;em&gt;main&lt;/em&gt; branch), the old &lt;code&gt;pull_request&lt;/code&gt; check is overwritten and &lt;em&gt;main&lt;/em&gt; turns green again ✅.&lt;/p&gt;</content:encoded><h:img src="/_astro/transition.CPkBWkCa.png"/><enclosure url="/_astro/transition.CPkBWkCa.png"/></item><item><title>Introducing Kanta - Translations for Elixir &amp; Phoenix</title><link>https://curiosum.com/blog/introducing-kanta-elixir-phoenix-open-source-translation-tool</link><guid isPermaLink="true">https://curiosum.com/blog/introducing-kanta-elixir-phoenix-open-source-translation-tool</guid><description>An open source solution for handling translations in Elixir and Phoenix applications</description><pubDate>Tue, 05 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Handling translations in web applications can be a challenging and time-consuming task. The process entails managing various files, tracking modifications, and ensuring that translations remain current as the project progresses. These complexities can slow down development and introduce errors. That&apos;s why we decided to create an open source solution: Kanta.&lt;/p&gt;
&lt;p&gt;In this blog post, we&apos;ll explore the problem of managing translations in web apps, and how Kanta simplifies the process. We&apos;ll also compare it with other translation management tools and delve into its powerful plugins.&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>What does AI dream about? Stable Diffusion in Elixir</title><link>https://curiosum.com/blog/what-does-ai-dream-about-stable-diffusion-in-elixir</link><guid isPermaLink="true">https://curiosum.com/blog/what-does-ai-dream-about-stable-diffusion-in-elixir</guid><description>Exploring AI image generation with Stable Diffusion using Elixir</description><pubDate>Mon, 06 Mar 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Tools like ChatGPT and DALL·E2 brought immense interest to AI. The go-to language to work with machine learning and artificial intelligence is Python, but market share may shift thanks to some tools created recently in Elixir programming language.&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>How to program an IoT device in Elixir using Nerves</title><link>https://curiosum.com/blog/how-program-iot-device-elixir-using-nerves</link><guid isPermaLink="true">https://curiosum.com/blog/how-program-iot-device-elixir-using-nerves</guid><description>Building IoT applications with Elixir and the Nerves framework</description><pubDate>Sat, 30 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Elixir is a great choice language for IoT devices due to its fault-tolerant nature, but also because of the ability to handle a lot of concurrent I/O operations thanks to the BEAM scheduler running under the hood.&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>