<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Networking on alikhil</title>
    <link>https://alikhil.dev/categories/networking/</link>
    <description>Recent content in Networking on alikhil</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <managingEditor>Alik Khilazhev</managingEditor>
    <lastBuildDate>Tue, 05 Aug 2025 21:14:12 +0300</lastBuildDate><atom:link href="https://alikhil.dev/categories/networking/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Protect Kubernetes Services with OAuth2 Proxy, ingress-nginx, and Pocket ID</title>
      <link>https://alikhil.dev/posts/oauth2-proxy-protect-services-in-k8s/</link>
      <pubDate>Tue, 05 Aug 2025 21:14:12 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/oauth2-proxy-protect-services-in-k8s/</guid>
      <description>Step-by-step guide to protecting internal Kubernetes services using OAuth2 Proxy, ingress-nginx auth annotations, and Pocket ID as the OIDC provider. Covers Helm values, cookie config, ingress annotations, and troubleshooting.</description>
      <content:encoded><![CDATA[<p>The original post wrote <a href="https://alikhil.dev/posts/oauth2-proxy-for-kubernetes-services/">about oauth2-proxy</a> over seven years ago was quite popular at the time and attracted a lot of organic traffic to my blog, which still benefits my SEO today. Since the tutorial had become outdated, I decided to rewrite it.</p>
<h2 id="what-we-have">What we have</h2>
<p>We have a Kubernetes cluster with several web services deployed for internal use.</p>
<h2 id="what-we-want-to-achieve">What we want to achieve</h2>
<p>We want to expose our internal web services to the Internet, but restrict access by requiring authorization. Access should be granted only to users authenticated through our Identity Provider (such as Google, GitHub, Keycloak, etc.).</p>
<h2 id="assumptions">Assumptions</h2>
<p>For simplicity, let&rsquo;s assume that both <a href="https://github.com/kubernetes/ingress-nginx">ingress-nginx</a> and <a href="https://cert-manager.io/">cert-manager</a> are already deployed in the cluster.</p>
<p>I will use <a href="https://github.com/pocket-id/pocket-id">Pocket ID</a> as Identity Provider in this tutorial. Configuration slightly differs for different providers. Check the <a href="https://oauth2-proxy.github.io/oauth2-proxy/configuration/providers/">official documentation</a> for your provider.</p>
<p>For the examples in this guide, I’ll use my <code>alikhil.dev</code> domain:</p>
<p>– <code>pocket-id.k8s.alikhil.dev</code> - will be used for Pocket ID</p>
<ul>
<li>
<p><code>k8s.alikhil.dev</code> - will be used for oauth2-proxy. I recommend to have higher domain for oauth2-proxy service for easier cookie setup.</p>
</li>
<li>
<p><code>*.k8s.alikhil.dev</code> - reserved for services deployed for internal usage</p>
</li>
</ul>
<h2 id="preparation">Preparation</h2>
<h3 id="dns">DNS</h3>
<p>I have added two DNS records:</p>
<ol>
<li><code>A</code> record for k8s.alikhil.dev pointing to ingress-nginx <code>LoadBalancer</code> IP address in the cluster (<code>kubectl get svc -n ingress-nginx | grep LoadBalancer</code>)</li>
<li><code>CNAME</code> record for <code>*.k8s.alikhil.dev</code> pointing to <code>k8s.alikhil.dev</code></li>
</ol>
<h3 id="install-pocket-id">Install Pocket ID</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">helm repo add anza-labs https://anza-labs.github.io/charts
</span></span><span class="line"><span class="cl">helm upgrade --install pocket-id anza-labs/pocket-id -f ./values/pocket-id.yaml
</span></span></code></pre></div><details>
    <summary>Values for pocket-id</summary>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">persistence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- Specifies whether ingress should be enabled.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- Ingress class name.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">className</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;nginx&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- Annotations to add to the ingress.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/ssl-redirect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.allow-http</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/tls-acme</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- Ingress host configuration.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">pocket-id.k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">ImplementationSpecific</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># -- List of TLS configurations for the ingress.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">   </span>- <span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">pocket-id-tls</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">     </span><span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span>- <span class="l">pocket-id.k8s.alikhil.dev</span><span class="w">
</span></span></span></code></pre></div></details>
<h3 id="configure-pocketid">Configure PocketID</h3>
<p>Go to <a href="https://pocket-id.k8s.alikhil.dev/signup/setup">https://pocket-id.k8s.alikhil.dev/signup/setup</a> and set initial configuration for Pocket ID.</p>
<p><img loading="lazy" src="/images/posts/oauth2-proxy/pocket-id-setup.png" type="" alt="Initial setup page"  /></p>
<p>Then add your passkey.</p>
<p><img loading="lazy" src="/images/posts/oauth2-proxy/pocket-id-pass.png" type="" alt="Add passkey"  /></p>
<p>Create <strong>developers</strong> group and add yourself to the list of members.
<img loading="lazy" src="/images/posts/oauth2-proxy/pocket-id-groups.png" type="" alt="Create group"  /></p>
<p>After that, go to <strong>OIDC clients</strong> page and create one for oauth2-proxy. Set proper callback url.</p>
<p><img loading="lazy" src="/images/posts/oauth2-proxy/pocket-id-client.png" type="" alt="PocketID client creation"  /></p>
<p>Save generated <strong>Client ID</strong> and <strong>Client Secret</strong> for later use.</p>
<h2 id="installing-oauth2-proxy">Installing oauth2-proxy</h2>
<p>I am using raw k8s secrets in this tutorial, but I highly recommend storing secrets in Vault or similar services and use <a href="https://external-secrets.io/latest/">External Secretes Operator</a> to deliver them to kubernetes.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl create secret generic oauth2-proxy-secrets --from-literal<span class="o">=</span>client-id<span class="o">=</span><span class="nv">$CLIENT_ID</span> --from-literal<span class="o">=</span>client-secret<span class="o">=</span><span class="nv">$CLIENT_SECRET</span> --from-literal<span class="o">=</span>cookie-secret<span class="o">=</span><span class="k">$(</span>openssl rand -base64 <span class="m">32</span> <span class="p">|</span> head -c <span class="m">32</span> <span class="p">|</span> base64<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">helm repo add oauth2-proxy https://oauth2-proxy.github.io/manifests
</span></span><span class="line"><span class="cl">helm install oauth2-proxy oauth2-proxy/oauth2-proxy -f values/oauth2-proxy.yaml
</span></span></code></pre></div><details>
    <summary>Adjust domains in values for oauth2-proxy</summary>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Oauth client configuration specifics</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">existingSecret</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy-secrets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cookieName</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;general-oauth2&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Default configuration, to be overridden</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">configFile</span><span class="p">:</span><span class="w"> </span><span class="p">|-</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">    email_domains = [ &#34;*&#34; ]
</span></span></span><span class="line"><span class="cl"><span class="sd">    upstreams = [ &#34;file:///dev/null&#34; ]
</span></span></span><span class="line"><span class="cl"><span class="sd">    skip_provider_button = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    allowed_groups = [ &#34;developers&#34;, &#34;admins&#34; ]
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_secure = false
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_domains = [&#34;.k8s.alikhil.dev&#34;, &#34;k8s.alikhil.dev&#34;]
</span></span></span><span class="line"><span class="cl"><span class="sd">    whitelist_domains = [ &#34;*.k8s.alikhil.dev&#34;, &#34;k8s.alikhil.dev&#34; ]
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_samesite = &#34;lax&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_csrf_per_request = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    cookie_csrf_expire = &#34;15m&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    pass_access_token = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    pass_authorization_header = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    provider = &#34;oidc&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    provider_display_name = &#34;PocketID&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    reverse_proxy = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    scope = &#34;openid profile email groups&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    session_store_type = &#34;redis&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    set_xauthrequest = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    set_authorization_header = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    silence_ping_logging = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    skip_auth_preflight = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    ssl_insecure_skip_verify = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    ssl_upstream_insecure_skip_verify = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    insecure_oidc_allow_unverified_email = true
</span></span></span><span class="line"><span class="cl"><span class="sd">    oidc_issuer_url = &#34;https://pocket-id.k8s.alikhil.dev&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    redirect_url = &#34;https://k8s.alikhil.dev/oauth2/callback&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">    # to reduce log amount
</span></span></span><span class="line"><span class="cl"><span class="sd">    request_logging = false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">className</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Only used if API capabilities (networking.k8s.io/v1) allow it</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">ImplementationSpecific</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Used to create an Ingress record.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">labels</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/tls-acme</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/cors-allow-origin</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;*&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/enable-cors</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;true&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.allow-http</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/ssl-redirect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Secrets must be manually created in the namespace.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">oauth2-proxy-tls</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Configure the session storage type, between cookie and redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">sessionStorage</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Can be one of the supported session storage cookie|redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">existingSecret</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">passwordKey</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;redis-password&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">clientType</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;standalone&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Enables and configure the automatic deployment of the redis subchart</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">redis</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># provision an instance of the redis sub-chart</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">architecture</span><span class="p">:</span><span class="w"> </span><span class="l">standalone</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">auth</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">master</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">persistence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">requests</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">100m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">128Mi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">1Gi</span><span class="w">
</span></span></span></code></pre></div></details>
<h2 id="testing">Testing</h2>
<h3 id="install-whoami">Install whoami</h3>
<p>To check oauth2-proxy we need a dummy service. I will use <a href="https://artifacthub.io/packages/helm/cowboysysop/whoami">whoami</a> helm chart for this.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm repo add cowboysysop https://cowboysysop.github.io/charts/
</span></span><span class="line"><span class="cl">helm install whoami cowboysysop/whoami
</span></span></code></pre></div><details>
    <summary>Values for whoami helm chart</summary>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ingressClassName</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/ssl-redirect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/ingress.allow-http</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kubernetes.io/tls-acme</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># put oauth2-proxy domain here</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/auth-signin</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://k8s.alikhil.dev/oauth2/start?rd=https://$host$request_uri$is_args$args&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># service-name.namespace-name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/auth-url</span><span class="p">:</span><span class="w"> </span><span class="l">http://oauth2-proxy.oauth-example.svc.cluster.local:80/oauth2/auth</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">whoami.k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">whoami.k8s.alikhil.dev</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">whoami-cert</span><span class="w">
</span></span></span></code></pre></div></details>
<h3 id="perform-test">Perform test</h3>
<p>Go to whoami url and check if oauth2-proxy redirects you to Pocket ID like in the demo:</p>
<p><img loading="lazy" src="/images/posts/oauth2-proxy/demo.gif" type="" alt="Demo"  /></p>
<h2 id="troubleshooting">Troubleshooting</h2>
<table>
	<thead>
			<tr>
					<th>Symptom</th>
					<th>Likely cause</th>
					<th>Fix</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Redirect loops back to login after successful auth</td>
					<td><code>redirect_url</code> in oauth2-proxy config doesn&rsquo;t match the callback URL registered in Pocket ID</td>
					<td>Set both to exactly <code>https://k8s.alikhil.dev/oauth2/callback</code></td>
			</tr>
			<tr>
					<td><code>cookie_secret</code> error on startup</td>
					<td>Cookie secret is not exactly 16, 24, or 32 bytes after base64 decode</td>
					<td>Regenerate: <code>openssl rand -base64 32 | head -c 32 | base64</code></td>
			</tr>
			<tr>
					<td>401 on every request despite logging in</td>
					<td><code>cookie_domains</code> / <code>whitelist_domains</code> doesn&rsquo;t cover the protected service domain</td>
					<td>Add <code>.k8s.alikhil.dev</code> to both <code>cookie_domains</code> and <code>whitelist_domains</code> in the config</td>
			</tr>
			<tr>
					<td>Protected service returns 401, not redirecting</td>
					<td><code>auth-url</code> annotation points to wrong service name or namespace</td>
					<td>Use the in-cluster DNS form: <code>http://oauth2-proxy.&lt;namespace&gt;.svc.cluster.local:80/oauth2/auth</code></td>
			</tr>
			<tr>
					<td>Auth works but Pocket ID group check fails</td>
					<td><code>allowed_groups</code> set but <code>scope</code> doesn&rsquo;t include <code>groups</code></td>
					<td>Add <code>groups</code> to the <code>scope</code> config option and ensure the Pocket ID client returns group claims</td>
			</tr>
	</tbody>
</table>
<h2 id="takeaways">Takeaways</h2>
<p>Later, when you need to protect any service in Kubernetes with oauth2-proxy, you simply need to add two annotations to your Ingress resource:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nginx.ingress.kubernetes.io/auth-signin</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;https://k8s.alikhil.dev/oauth2/start?rd=https://$host$request_uri$is_args$args&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nginx.ingress.kubernetes.io/auth-url</span><span class="p">:</span><span class="w"> </span><span class="l">http://oauth2-proxy.oauth-example.svc.cluster.local:80/oauth2/auth</span><span class="w">
</span></span></span></code></pre></div>]]></content:encoded>
    </item>
    
    <item>
      <title>Remote LAN access with WireGuard and Mikrotik</title>
      <link>https://alikhil.dev/posts/remote-lan-access-with-wireguard-and-mikrotik/</link>
      <pubDate>Sun, 13 Apr 2025 14:11:43 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/remote-lan-access-with-wireguard-and-mikrotik/</guid>
      <description>Step-by-step guide to setting up remote LAN access using WireGuard on a MikroTik hEX S router and a cloud VM. Covers RouterOS CLI, firewall rules, routing, and client configuration.</description>
      <content:encoded><![CDATA[<p>Recently I have configured out how to access my home and cloud network remotely using WireGuard and Mikrotik Hex S router. With this step-by-step tutorial I will show you (and perhaps my future self) how to do it.</p>
<p><strong>Tested with</strong>: MikroTik hEX S (RB760iGS) · RouterOS 7.x · Ubuntu 22.04 LTS · WireGuard hub-and-spoke topology</p>
<h2 id="requirements">Requirements</h2>
<h3 id="what-i-have">What I have</h3>
<ul>
<li>
<p>A Mikrotik Hex S router with a dynamic public IP address.</p>
</li>
<li>
<p>Services in a cloud VM (Ubuntu 22) with a static public IP address.</p>
</li>
<li>
<p>Services in a VM on my home network.</p>
</li>
<li>
<p>Clients - laptops and phones - that need to access the services in my home and cloud network.</p>
</li>
</ul>
<h3 id="what-i-want">What I want</h3>
<ul>
<li>
<p>Clients outside of my home network should be able to access services both on my home and cloud network.</p>
</li>
<li>
<p>Only traffic to my home and cloud network should be routed through the VPN.</p>
</li>
<li>
<p>Clients inside my home network should be able to access services on my cloud network without additional configuration.</p>
</li>
<li>
<p>No external centralized service should be used.</p>
</li>
<li>
<p>No open ports on my home router.</p>
</li>
</ul>
<p><img loading="lazy" src="/images/posts/mikrotik-wg/final.png" type="" alt="Final configuration"  /></p>
<h2 id="implementation">Implementation</h2>
<blockquote>
<p>Nowadays, there are plenty of VPN solutions like zero-tier and tailscale. However, I think they are too bloated for my humble needs and WireGuard is more than enough for that.</p>
</blockquote>
<p>Because of last requirement, it&rsquo;s obvious that traffic to home network should be routed though my cloud server.<br>
So, I will use WireGuard to create a tunnel between mikrotik router and cloud server.<br>
This way, I can access my home network from anywhere without exposing any ports on my home router.</p>
<h3 id="i-initial-configuration-of-wireguard-on-cloud-vm">I. Initial configuration of WireGuard on cloud VM</h3>
<blockquote>
<p><em>For the sake of reproducibility and simplicity I will use <a href="https://www.wireguard.com/">vanilla wireguard</a> and configure it on OS level, not in docker container.</em></p>
</blockquote>
<h4 id="1-install-wireguard-on-ubuntu-2204">1. Install wireguard on Ubuntu 22.04</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install wireguard -y
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Edit the configuration file</span>
</span></span><span class="line"><span class="cl">sudo nano /etc/sysctl.conf
</span></span><span class="line"><span class="cl"><span class="c1"># Find and uncomment the line:</span>
</span></span><span class="line"><span class="cl">net.ipv4.ip_forward<span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Save and exit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Then apply the changes:</span>
</span></span><span class="line"><span class="cl">sudo sysctl -p
</span></span></code></pre></div><h4 id="2-create-public-and-private-key">2. Create public and private key</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo su -
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> /etc/wireguard
</span></span><span class="line"><span class="cl"><span class="c1"># Generate private and public keys</span>
</span></span><span class="line"><span class="cl">wg genkey <span class="p">|</span> tee privatekey <span class="p">|</span> wg pubkey &gt; publickey
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check the keys</span>
</span></span><span class="line"><span class="cl">cat privatekey
</span></span><span class="line"><span class="cl">cat publickey
</span></span></code></pre></div><h4 id="3-create-the-configuration-file">3. Create the configuration file</h4>
<p>I will use <code>172.16.10.0/24</code> subnet for Wireguard network.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/wireguard/wg0.conf
</span></span><span class="line"><span class="cl"><span class="c1"># Add the following lines:</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>Interface<span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="nv">Address</span> <span class="o">=</span> 172.16.10.1/24
</span></span><span class="line"><span class="cl"><span class="nv">ListenPort</span> <span class="o">=</span> <span class="m">27277</span> <span class="c1"># You can choose any port you want</span>
</span></span><span class="line"><span class="cl"><span class="nv">Privatekey</span> <span class="o">=</span> &lt;your_private_key&gt; <span class="c1"># from previous step</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># These are placeholder lines for next steps, keep them commented for now</span>
</span></span><span class="line"><span class="cl"><span class="c1"># # Mikrotik peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># [Peer]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># AllowedIPs = 172.16.10.0/24,192.168.0.0/16</span>
</span></span><span class="line"><span class="cl"><span class="c1"># PublicKey = mikrotik public key</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># # client peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># [Peer]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># AllowedIPs = 172.16.10.2/32</span>
</span></span><span class="line"><span class="cl"><span class="c1"># PublicKey = first client device public key</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># CRTL+X: save and exit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Change the permissions of the configuration file</span>
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> wg0.conf
</span></span></code></pre></div><h4 id="4-start-the-wireguard-interface">4. Start the WireGuard interface</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Enable service to start on boot</span>
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> wg-quick@wg0.service
</span></span><span class="line"><span class="cl">systemctl daemon-reload
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Start the service</span>
</span></span><span class="line"><span class="cl">systemctl start wg-quick@wg0
</span></span><span class="line"><span class="cl"><span class="c1"># Check wg status</span>
</span></span><span class="line"><span class="cl">wg
</span></span></code></pre></div><p>It should show the interface is up like this</p>
<p><img loading="lazy" src="/images/posts/mikrotik-wg/wg.png" type="" alt="WireGuard status"  /></p>
<p>More commands to check the status of the service:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check the status of the service</span>
</span></span><span class="line"><span class="cl">systemctl status wg-quick@wg0
</span></span><span class="line"><span class="cl"><span class="c1"># Restart the service if needed</span>
</span></span><span class="line"><span class="cl">systemctl restart wg-quick@wg0
</span></span></code></pre></div><h3 id="ii-mikrotik-hex-s-wireguard-setup">II. MikroTik hEX S WireGuard setup</h3>
<p>I will use Mikrotik command line interface (CLI) to configure the router. You can use Winbox or WebFig if you prefer.</p>
<h4 id="1-create-wireguard-interface">1. Create WireGuard interface</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Create WireGuard interface and it will automatically generate private and public keys</span>
</span></span><span class="line"><span class="cl">/interface wireguard add listen-port<span class="o">=</span><span class="m">13231</span> <span class="nv">mtu</span><span class="o">=</span><span class="m">1420</span> <span class="nv">name</span><span class="o">=</span>cloud-wg
</span></span><span class="line"><span class="cl"><span class="c1"># Add the IP address to the interface</span>
</span></span><span class="line"><span class="cl">/ip add <span class="nv">address</span><span class="o">=</span>172.16.10.3/24 <span class="nv">interface</span><span class="o">=</span>cloud-wg <span class="nv">network</span><span class="o">=</span>172.16.10.0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Print the keys, you will need only the public key later</span>
</span></span><span class="line"><span class="cl">/interface/wireguard/print
</span></span></code></pre></div><h4 id="2-create-the-peer">2. Create the peer</h4>
<p>Here we add cloud vm as a peer to the Mikrotik router&rsquo;s wireguard. The public key of the cloud server is needed here.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># In allowed-address we add IP ranges which will be routed through this peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 172.21.0.0/16 - subnet of docker containers on cloud server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 172.16.10.0/16 - subnet of wireguard network</span>
</span></span><span class="line"><span class="cl">/interface wireguard peers add allowed-address<span class="o">=</span>172.16.10.0/24,172.21.0.0/16 <span class="se">\
</span></span></span><span class="line"><span class="cl">    endpoint-address<span class="o">=</span>cloud.vm.ip.address <span class="se">\
</span></span></span><span class="line"><span class="cl">    endpoint-port<span class="o">=</span><span class="m">27277</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="nv">interface</span><span class="o">=</span>cloud-wg <span class="se">\
</span></span></span><span class="line"><span class="cl">    public-key<span class="o">=</span><span class="s2">&#34;PUT CLOUD SERVER PUBLIC KEY HERE&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="nv">name</span><span class="o">=</span>peer1 persistent-keepalive<span class="o">=</span>25s
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Here is a tricky part</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Routing rule for subnet of wireguard network is added automatically on adding IP address to the interface</span>
</span></span><span class="line"><span class="cl"><span class="c1"># But we need to add explicitly the route for docker containers subnet</span>
</span></span><span class="line"><span class="cl"><span class="c1"># You may need to adjust the distance and routing-table values</span>
</span></span><span class="line"><span class="cl">/ip route <span class="nv">disabled</span><span class="o">=</span>no <span class="nv">distance</span><span class="o">=</span><span class="m">1</span> dst-address<span class="o">=</span>172.21.0.0/16 <span class="nv">gateway</span><span class="o">=</span>cloud-wg routing-table<span class="o">=</span>main <span class="nv">scope</span><span class="o">=</span><span class="m">30</span> suppress-hw-offload<span class="o">=</span>no target-scope<span class="o">=</span><span class="m">10</span>
</span></span></code></pre></div><h3 id="iii-add-wireguard-peer-vm">III. Add WireGuard peer (VM)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/wireguard/wg0.conf
</span></span><span class="line"><span class="cl"><span class="c1"># Uncomment the lines for Mikrotik peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Mikrotik peer</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>Peer<span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="nv">AllowedIPs</span> <span class="o">=</span> 172.16.10.0/24,192.168.0.0/16
</span></span><span class="line"><span class="cl"><span class="nv">PublicKey</span> <span class="o">=</span> mikrotik public key
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save and exit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Restart the WireGuard service</span>
</span></span><span class="line"><span class="cl">sudo systemctl restart wg-quick@wg0
</span></span><span class="line"><span class="cl"><span class="c1"># Check if changes are applied</span>
</span></span><span class="line"><span class="cl">wg show
</span></span></code></pre></div><p>You should see the Mikrotik peer in the list of peers.
<img loading="lazy" src="/images/posts/mikrotik-wg/wg-show-2.png" type="" alt="WireGuard peers"  /></p>
<details>
<summary>More commands to tests connection between cloud vm and mikrotik wg</summary>
<h5 id="run-in-mikrotik-cli">Run in mikrotik CLI</h5>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ping wireguard IP of cloud server</span>
</span></span><span class="line"><span class="cl">ping 172.16.10.1
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/mikrotik-ping.png" type="" alt="WireGuard ping"  /></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># resolve google.com via adguard container on cloud vm</span>
</span></span><span class="line"><span class="cl">put <span class="o">[</span>resolve google.com server 172.21.0.114<span class="o">]</span>
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/mikrotik-resolve.png" type="" alt="Wireguard resolve"  /></p>
<h5 id="run-in-cloud-server">Run in cloud server</h5>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ping wireguard IP of Mikrotik router</span>
</span></span><span class="line"><span class="cl">ping 172.168.10.3
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/vm-ping.png" type="" alt="VM ping"  /></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># resolve google.com via adguard container on home adguard</span>
</span></span><span class="line"><span class="cl">dig google.com @192.168.11.2
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/vm-resolve.png" type="" alt="VM resolve"  /></p>
<h5 id="run-on-my-mac-connected-to-home-network">Run on my Mac connected to home network</h5>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># ping wireguard IP of cloud server</span>
</span></span><span class="line"><span class="cl">ping 172.16.10.1
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/mac-ping.png" type="" alt="mac ping"  /></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># resolve google.com via adguard container on cloud adguard</span>
</span></span><span class="line"><span class="cl">dig google.com @172.121.0.114
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mikrotik-wg/mac-resolve.png" type="" alt="mac resolve"  /></p>
</details>
<h3 id="iv-add-first-client-device-as-a-peer">IV. Add first client device as a peer</h3>
<p>I recommend you to use your smartphone as first client device, because it can work from both home WiFi and mobile data. This way you can test the connection from both networks.</p>
<p>Also, install on your smartphone:</p>
<ul>
<li>WireGuard app (<a href="https://apps.apple.com/us/app/wireguard/id1441195209">iOS</a>, <a href="https://play.google.com/store/apps/details?id=com.wireguard.android&amp;hl=en&amp;pli=1">Android</a>)</li>
<li>Network debugging app (<a href="https://apps.apple.com/bo/app/inettools-ping-dns-port-scan/id561659975">iOS</a>, <a href="https://play.google.com/store/apps/details?id=com.ulfdittmer.android.ping&amp;hl=en">Android</a>)</li>
</ul>
<h4 id="1-create-keys-for-the-client">1. Create keys for the client</h4>
<p>Install Wireguard app for your client OS.</p>
<p>Then, generate public and private keys on the device, for that create config from scratch in the app and then click on <strong>Generate keypair</strong> button.</p>
<p>Or you can generate keys on the cloud server and then copy them to the client device.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">wg genkey <span class="p">|</span> tee peer-privatekey <span class="p">|</span> wg pubkey &gt; peer-publickey
</span></span></code></pre></div><h4 id="2-add-the-client-as-a-peer-to-wireguard-config-on-cloud-server">2. Add the client as a peer to WireGuard config on cloud server</h4>
<p>On the cloud server, edit the WireGuard config file and add the client as a peer.</p>
<p>Each time increment previous peer address by 1.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/wireguard/wg0.conf
</span></span><span class="line"><span class="cl"><span class="c1"># Uncomment the lines for client peer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># client peer</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>Peer<span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="nv">AllowedIPs</span> <span class="o">=</span> 172.16.10.2/32
</span></span><span class="line"><span class="cl"><span class="nv">PublicKey</span> <span class="o">=</span> put public key
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># CRTL+X: save and exit</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Restart the WireGuard service</span>
</span></span><span class="line"><span class="cl">sudo systemctl restart wg-quick@wg0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check if changes are applied</span>
</span></span><span class="line"><span class="cl">wg show
</span></span></code></pre></div><h4 id="3-continue-configuring-the-client-device">3. Continue configuring the client device</h4>
<p>You already have public and private keys for the client device, other configuration parameters are:</p>
<h5 id="interface">Interface</h5>
<ul>
<li><strong>Address</strong> - It&rsquo;s address of peer in wireguard subnet. Put the same address you set in <code>AllowedIPs</code> field in previous step.</li>
<li><strong>DNS servers</strong> - if you have adguard/pihole running on the cloud server, you can use it as a DNS server. Put it&rsquo;s IP address here.</li>
<li><strong>MTU</strong> and <strong>ListenPort</strong> - you can leave them empty, they will be set automatically.</li>
</ul>
<h5 id="peer">Peer</h5>
<ul>
<li><strong>Endpoint</strong> - cloud server IP address and port (27277)</li>
<li><strong>Public key</strong> - public key of the cloud server</li>
<li><strong>AllowedIPs</strong> - here we put all subnets that we want to access from this current client device</li>
</ul>
<p>Here is an example of the configuration file for the client device:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[Interface]</span>
</span></span><span class="line"><span class="cl"><span class="na">PrivateKey</span> <span class="o">=</span> <span class="s">private-key-of-client-device</span>
</span></span><span class="line"><span class="cl"><span class="na">Address</span> <span class="o">=</span> <span class="s">172.16.10.2/32</span>
</span></span><span class="line"><span class="cl"><span class="na">DNS</span> <span class="o">=</span> <span class="s">172.21.0.114 # IP of adguard container on cloud server</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">[Peer]</span>
</span></span><span class="line"><span class="cl"><span class="na">PublicKey</span> <span class="o">=</span> <span class="s">public-key-of-cloud-server</span>
</span></span><span class="line"><span class="cl"><span class="na">AllowedIPs</span> <span class="o">=</span> <span class="s">192.168.0.0/16, 172.16.10.0/24, 172.21.0.0/16 # subnets of home, cloud and wireguard network</span>
</span></span><span class="line"><span class="cl"><span class="na">Endpoint</span> <span class="o">=</span> <span class="s">ip.of.cloud.servner:27277</span>
</span></span></code></pre></div><p>That&rsquo;s it! Disconnect your device from the home Wi-Fi, switch to mobile data and connect to the VPN.</p>
<p>Then try to:</p>
<ul>
<li>ping the cloud server and Mikrotik router IP addresses in wireguard subnet.</li>
<li>check ports of services in docker containers on cloud and home server VM.</li>
</ul>
<style>
td, th {
   border: none!important;
}
</style>
<table>
	<thead>
			<tr>
					<th>ping cloud</th>
					<th>ping home</th>
					<th>check adguard port</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><img loading="lazy" src="/images/posts/mikrotik-wg/screen-ping-vm.jpeg" type="" alt="ping cloud"  /></td>
					<td><img loading="lazy" src="/images/posts/mikrotik-wg/screen-ping-home.jpeg" type="" alt="ping home"  /></td>
					<td><img loading="lazy" src="/images/posts/mikrotik-wg/screen-port-scan.jpeg" type="" alt="port scan"  /></td>
			</tr>
	</tbody>
</table>
<h3 id="v-troubleshooting">V. Troubleshooting</h3>
<table>
	<thead>
			<tr>
					<th>Symptom</th>
					<th>Likely cause</th>
					<th>Fix</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Handshake shows &ldquo;never&rdquo;</td>
					<td>Firewall blocks UDP port on cloud VM, or wrong public key</td>
					<td>Check <code>ufw</code>/<code>iptables</code> allows the WireGuard UDP port; verify both peers have each other&rsquo;s correct public key</td>
			</tr>
			<tr>
					<td>Peer handshakes OK but home LAN is unreachable</td>
					<td>Missing route or <code>AllowedIPs</code> too narrow</td>
					<td>On cloud VM, ensure MikroTik peer <code>AllowedIPs</code> includes <code>192.168.0.0/16</code>; on MikroTik, add a route for cloud subnets via <code>cloud-wg</code></td>
			</tr>
			<tr>
					<td>Handshake works but DNS fails</td>
					<td>Wrong DNS IP in client config, or AdGuard not listening on WireGuard interface</td>
					<td>Confirm DNS container IP is reachable over WireGuard (<code>ping</code> it); check AdGuard binds to <code>0.0.0.0</code> not just <code>localhost</code></td>
			</tr>
			<tr>
					<td><code>AllowedIPs</code> routing mistake</td>
					<td>Client routes all traffic through VPN</td>
					<td>Set <code>AllowedIPs</code> to specific subnets only (split-tunnel), not <code>0.0.0.0/0</code>, unless you want full-tunnel</td>
			</tr>
			<tr>
					<td>MikroTik shows peer but no traffic</td>
					<td><code>persistent-keepalive</code> not set, NAT drops idle UDP</td>
					<td>Add <code>persistent-keepalive=25s</code> to the MikroTik peer config</td>
			</tr>
	</tbody>
</table>
<h3 id="vi-bonus-dns-configuration">VI. Bonus. DNS configuration</h3>
<p>Since I have 2 adguard instances and I use them as DNS servers everywhere, I will add DNS records for accessing my services:</p>
<ul>
<li>*<strong>.cloud.domain.com</strong> - pointing to traefik docker container on cloud server</li>
<li>*<strong>.home.domain.com</strong> - pointing to traefik docker container on home server</li>
</ul>
<p>Thus, I can access my services using domain names instead of IP addresses.</p>
<img src="/images/posts/mikrotik-wg/dns.png" width="30%">
<h2 id="final-thoughts">Final thoughts</h2>
<p>I hope this tutorial was helpful for you. I will keep it updated if I find any issues or improvements.</p>
<p>If you have any questions or suggestions, feel free to leave a comment below.</p>
<p>Credits to <a href="https://www.laroberto.com/">@laroberto</a> for their <a href="https://www.laroberto.com/remote-lan-access-with-wireguard/">guide on LAN access with WireGuard</a>. I followed it to set up the initial configuration and then adapted it to my needs.</p>
<h2 id="recommended-reading">Recommended reading</h2>
<ul>
<li><a href="/posts/saving-and-using-secrets-in-mikrotitk-routeros/">Storing and Using Secrets in MikroTik RouterOS</a> — how to avoid hardcoding passwords and tokens in your RouterOS scripts.</li>
</ul>]]></content:encoded>
    </item>
    
    <item>
      <title>Storing and Using Secrets in MikroTik RouterOS</title>
      <link>https://alikhil.dev/posts/saving-and-using-secrets-in-mikrotik-routeros/</link>
      <pubDate>Sun, 30 Mar 2025 16:01:25 +0300</pubDate>
      <author>Alik Khilazhev</author>
      <guid>https://alikhil.dev/posts/saving-and-using-secrets-in-mikrotik-routeros/</guid>
      <description>How to securely store and use secrets (passwords, tokens, API keys) in MikroTik RouterOS scripts without hardcoding them in plain text.</description>
      <content:encoded><![CDATA[<p>Recently I have replaced my stock ISP router with <a href="https://mikrotik.com/product/hex_s">Mikrotik Hex S</a>. I have been using it for a while and I am very happy with it. It is a very powerful device which can be programmed and automated with built-in scripting language.</p>
<p>When I started writing my first scripts I faced a problem: how to store and use secrets in my scripts. I have found a solution and I want to share it with you.</p>
<h2 id="the-problem">The problem</h2>
<p>Let&rsquo;s say I want to write a script that will send me telegram notifications. To do so I need to store my telegram bot token and chat id. Since I keep my RouterOS configuration in a git repository, I don&rsquo;t want to hardcode my secrets in the script.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="p">:</span><span class="n">global</span> <span class="n">sendTelegramMessage</span> <span class="k">do</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">local</span> <span class="n">botToken</span> <span class="s2">&#34;1234567890:ABCDEFGHIJKLMN&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">local</span> <span class="n">chatId</span> <span class="s2">&#34;10000000&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">local</span> <span class="n">message</span> <span class="s2">&#34;$1&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># telegram notification</span>
</span></span><span class="line"><span class="cl">    <span class="o">/</span><span class="k">tool</span> <span class="n">fetch</span> <span class="n">url</span><span class="o">=</span><span class="s2">&#34;https://api.telegram.org/bot$botToken/sendMessage\?chat_id=$chatId&amp;text=$message&#34;</span> <span class="n">keep</span><span class="o">-</span><span class="n">result</span><span class="o">=</span><span class="n">no</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="the-solution">The solution</h2>
<p>RouterOS has low level feature <code>/ppp secret</code> which can be used to store secrets. However, it could be inconvenient and a bit messy to use it directly in scripts. Instead, I would like to have more high level API to store and use secrets.</p>
<p>And, I have one. There is post in <a href="https://forum.mikrotik.com/viewtopic.php?p=916159#p916159">mikrotik forum</a> by user with nickname <strong>Amm0</strong>. Ammo has shared a script of global function which can be used to store and retrieve secrets like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">$SECRET set mySecret password=&#34;mySecretPassword&#34;
</span></span><span class="line"><span class="cl">:put [$SECRET get mySecret]
</span></span></code></pre></div><p><img loading="lazy" src="/images/posts/mirotik-secrets/terminal.png" type="" alt="Terminal Screenshot"  /></p>
<p>Now, I modify my script to use this function to keep it clean and secure:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="p">:</span><span class="n">global</span> <span class="n">sendTelegramMessage</span> <span class="k">do</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">local</span> <span class="n">botToken</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">set</span> <span class="n">botToken</span> <span class="s2">&#34;$[$SECRET get TELEGRAM_TOKEN]&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">local</span> <span class="n">chatId</span> <span class="s2">&#34;$[$SECRET get CHAT_ID]&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">:</span><span class="n">local</span> <span class="n">message</span> <span class="s2">&#34;$1&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># telegram notification</span>
</span></span><span class="line"><span class="cl">    <span class="o">/</span><span class="k">tool</span> <span class="n">fetch</span> <span class="n">url</span><span class="o">=</span><span class="s2">&#34;https://api.telegram.org/bot$botToken/sendMessage\?chat_id=$chatId&amp;text=$message&#34;</span> <span class="n">keep</span><span class="o">-</span><span class="n">result</span><span class="o">=</span><span class="n">no</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><details>
  <summary>global SECRET function source code</summary>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">### $SECRET
</span></span><span class="line"><span class="cl">#   get &lt;name&gt;
</span></span><span class="line"><span class="cl">#   set &lt;name&gt; password=&lt;password&gt;
</span></span><span class="line"><span class="cl">#   remove &lt;name
</span></span><span class="line"><span class="cl">#   print
</span></span><span class="line"><span class="cl">:global SECRET
</span></span><span class="line"><span class="cl">:set $SECRET do={
</span></span><span class="line"><span class="cl">    :global SECRET
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    # helpers
</span></span><span class="line"><span class="cl">    :local fixprofile do={
</span></span><span class="line"><span class="cl">        :if ([/ppp profile find name=&#34;null&#34;]) do={:put &#34;nothing&#34;} else={
</span></span><span class="line"><span class="cl">            /ppp profile add bridge-learning=no change-tcp-mss=no local-address=0.0.0.0 name=&#34;null&#34; only-one=yes remote-address=0.0.0.0 session-timeout=1s use-compression=no use-encryption=no use-mpls=no use-upnp=no
</span></span><span class="line"><span class="cl">        }
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">    :local lppp [:len [/ppp secret find where name=$2]]
</span></span><span class="line"><span class="cl">    :local checkexist do={
</span></span><span class="line"><span class="cl">        :if (lppp=0) do={
</span></span><span class="line"><span class="cl">            :error &#34;\$SECRET: cannot find $2 in secret store&#34;
</span></span><span class="line"><span class="cl">        }
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    # $SECRET
</span></span><span class="line"><span class="cl">    :if ([:typeof $1]!=&#34;str&#34;) do={
</span></span><span class="line"><span class="cl">        :put &#34;\$SECRET&#34;
</span></span><span class="line"><span class="cl">        :put &#34;   uses /ppp/secrets to store stuff like REST apikeys, or other sensative data&#34;
</span></span><span class="line"><span class="cl">        :put &#34;\t\$SECRET print - prints stored secret passwords&#34;
</span></span><span class="line"><span class="cl">        :put &#34;\t\$SECRET get &lt;name&gt; - gets a stored secret&#34;
</span></span><span class="line"><span class="cl">        :put &#34;\t\$SECRET set &lt;name&gt; password=\&#34;YOUR_SECRET\&#34; - sets a secret password&#34;
</span></span><span class="line"><span class="cl">        :put &#34;\t\$SECRET remove &lt;name&gt; - removes a secret&#34;
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    # $SECRET print
</span></span><span class="line"><span class="cl">    :if ($1~&#34;^pr&#34;) do={
</span></span><span class="line"><span class="cl">        /ppp secret print where comment~&#34;\\\$SECRET&#34;
</span></span><span class="line"><span class="cl">        :return [:nothing]
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    # $SECRET get
</span></span><span class="line"><span class="cl">    :if ($1~&#34;get&#34;) do={
</span></span><span class="line"><span class="cl">        $checkexist
</span></span><span class="line"><span class="cl">       :return [/ppp secret get $2 password]
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    # $SECRET set
</span></span><span class="line"><span class="cl">    :if ($1~&#34;set|add&#34;) do={
</span></span><span class="line"><span class="cl">        :if ([:typeof $password]=&#34;str&#34;) do={} else={:error &#34;\$SECRET: password= required&#34;}
</span></span><span class="line"><span class="cl">        :if (lppp=0) do={
</span></span><span class="line"><span class="cl">            /ppp secret add name=$2 password=$password
</span></span><span class="line"><span class="cl">        } else={
</span></span><span class="line"><span class="cl">            /ppp secret set $2 password=$password
</span></span><span class="line"><span class="cl">        }
</span></span><span class="line"><span class="cl">        $fixprofile
</span></span><span class="line"><span class="cl">        /ppp secret set $2 comment=&#34;used by \$SECRET&#34;
</span></span><span class="line"><span class="cl">        /ppp secret set $2 profile=&#34;null&#34;
</span></span><span class="line"><span class="cl">        /ppp secret set $2 service=&#34;async&#34;
</span></span><span class="line"><span class="cl">        :return [$SECRET get $2]
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    # $SECRET remove
</span></span><span class="line"><span class="cl">    :if ($1~&#34;rm|rem|del&#34;) do={
</span></span><span class="line"><span class="cl">        $checkexist
</span></span><span class="line"><span class="cl">        :return [/ppp secret remove $2]
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">    :error &#34;\$SECRET: bad command&#34;
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div></details>
<h2 id="conclusion">Conclusion</h2>
<p>The good thing about this approach is that secrets storing and retrieving mechanism encapsulated and can be easily changed in the future without changing the scripts. Also, it is easy to use and understand.</p>
<p>Keep your secrets safe and happy scripting!</p>]]></content:encoded>
    </item>
    
  </channel>
</rss>
