<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>Lahiru De Silva</title>
<link>https://lahirudesilva.com/notes.html</link>
<atom:link href="https://lahirudesilva.com/notes.xml" rel="self" type="application/rss+xml"/>
<description>Short notes on things I worked out, fixed, or want to share.</description>
<generator>quarto-1.10.18</generator>
<lastBuildDate>Sat, 22 Aug 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>Per-tenant subdomains with NGINX wildcard routing</title>
  <dc:creator>Lahiru De Silva</dc:creator>
  <link>https://lahirudesilva.com/notes/nginx-wildcard-subdomains/</link>
  <description><![CDATA[ 





<p>Tumblr gives every blog its own subdomain, and so does every SaaS that hands you <code>yourcompany.app.com</code> on signup. None of them add a vhost per customer. The whole thing is a wildcard DNS record plus one server block that treats the subdomain as a variable.</p>
<div class="cell" data-layout-align="default">
<div class="cell-output-display">
<div>
<p></p><figure class="figure"><p></p>
<div>
<pre class="mermaid mermaid-js">flowchart LR
    C["Browser&lt;br/&gt;alice.example.com"] --&gt;|"1 · DNS&lt;br/&gt;*.example.com"| D["Wildcard A record&lt;br/&gt;→ 203.0.113.10"]
    D --&gt;|"2 · HTTPS"| N["NGINX&lt;br/&gt;server_name regex&lt;br/&gt;captures 'alice'"]
    N --&gt;|"3 · proxy_pass&lt;br/&gt;X-Tenant: alice"| A["App backend&lt;br/&gt;one deployment"]
</pre>
</div>
<p></p></figure><p></p>
</div>
</div>
</div>
<section id="wildcard-dns" class="level2">
<h2 class="anchored" data-anchor-id="wildcard-dns">Wildcard DNS</h2>
<p>A single wildcard A record answers for every name at that level:</p>
<pre><code>*.example.com.    300    IN    A    203.0.113.10</code></pre>
<p>Every subdomain now resolves to the same address, so adding a tenant needs no DNS change at all. Two things to know about the matching:</p>
<ul>
<li>A wildcard covers exactly one label. <code>*.example.com</code> answers for <code>alice.example.com</code> but not for <code>a.b.example.com</code>.</li>
<li>An explicit record always beats the wildcard. If <code>www.example.com</code> has its own A record, that record wins and the wildcard never applies to it.</li>
</ul>
</section>
<section id="the-nginx-side" class="level2">
<h2 class="anchored" data-anchor-id="the-nginx-side">The NGINX side</h2>
<p>NGINX matches <code>server_name</code> with a regex when the value starts with <code>~</code>, and PCRE named captures become ordinary variables you can use anywhere in the block:</p>
<pre class="nginx"><code># Tenant sites. Matches any single-label subdomain and captures it as $tenant.
server {
    listen 443 ssl;
    server_name ~^(?&lt;tenant&gt;[^.]+)\.example\.com$;

    ssl_certificate     /etc/nginx/certs/wildcard.example.com.crt;
    ssl_certificate_key /etc/nginx/certs/wildcard.example.com.key;

    location / {
        proxy_pass http://app_backend;

        proxy_set_header Host      $host;
        proxy_set_header X-Tenant  $tenant;
        proxy_set_header X-Real-IP $remote_addr;
    }
}</code></pre>
<p><code>[^.]+</code> is doing real work there. Without it the pattern would also match <code>a.b.example.com</code> and capture <code>a.b</code>, which is not a tenant and which wildcard DNS would not have resolved anyway.</p>
</section>
<section id="reserved-subdomains-come-for-free" class="level2">
<h2 class="anchored" data-anchor-id="reserved-subdomains-come-for-free">Reserved subdomains come for free</h2>
<p>In Apache you exclude <code>www</code> from the rewrite with a negative <code>RewriteCond</code> before the rule. In NGINX you do not need a guard at all, because <code>server_name</code> matching has a fixed precedence:</p>
<ol type="1">
<li>Exact names</li>
<li>Leading wildcards, <code>*.example.com</code></li>
<li>Trailing wildcards, <code>example.com.*</code></li>
<li>Regular expressions, in the order they appear in the config</li>
</ol>
<p>Regexes come last, so any exact block wins automatically:</p>
<pre class="nginx"><code># Wins over the regex block above purely by being an exact match.
server {
    listen 443 ssl;
    server_name www.example.com example.com;
    # ... marketing site ...
}</code></pre>
<p>Add <code>api.example.com</code> or <code>admin.example.com</code> the same way and they are carved out of the tenant space with no change to the wildcard block.</p>
</section>
<section id="getting-the-tenant-to-the-application" class="level2">
<h2 class="anchored" data-anchor-id="getting-the-tenant-to-the-application">Getting the tenant to the application</h2>
<p>Passing it as a header keeps the upstream URL clean and works with any framework:</p>
<pre class="nginx"><code>proxy_set_header X-Tenant $tenant;</code></pre>
<p>The rewrite-to-query-parameter style works too, if the app already expects it:</p>
<pre class="nginx"><code>proxy_pass http://app_backend/user?id=$tenant;</code></pre>
<p><code>proxy_set_header</code> replaces any header of that name from the client, so a request carrying its own <code>X-Tenant</code> cannot spoof one. That is worth checking rather than assuming. Passing the client’s value through instead is a tenant isolation bug of the worst kind.</p>
<div class="callout callout-style-default callout-warning callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Warning</span>The subdomain is a routing hint, not authorization
</div>
</div>
<div class="callout-body-container callout-body">
<p><code>$tenant</code> is attacker-controlled: anyone can request <code>someoneelses-tenant.example.com</code>. It tells the application which tenant is being <em>asked for</em>, never which tenant the caller is <em>entitled to</em>. The session still has to be checked against it on every request.</p>
</div>
</div>
</section>
<section id="tls-is-the-part-that-actually-costs-you" class="level2">
<h2 class="anchored" data-anchor-id="tls-is-the-part-that-actually-costs-you">TLS is the part that actually costs you</h2>
<p>One certificate covers the whole scheme, but a wildcard certificate cannot be issued over the HTTP-01 challenge. Let’s Encrypt requires DNS-01 for <code>*.example.com</code>, which means your ACME client needs API credentials for the DNS provider. That is the real setup cost here, and it is worth sorting out before the routing rather than after.</p>
<p>Note that <code>*.example.com</code> does not cover the apex <code>example.com</code>, so the certificate needs both names on it.</p>


</section>

 ]]></description>
  <category>NGINX</category>
  <category>Multi-tenancy</category>
  <category>DNS</category>
  <guid>https://lahirudesilva.com/notes/nginx-wildcard-subdomains/</guid>
  <pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Quarto does not copy images referenced from raw HTML</title>
  <dc:creator>Lahiru De Silva</dc:creator>
  <link>https://lahirudesilva.com/notes/quarto-resources-raw-html/</link>
  <description><![CDATA[ 





<p>Quarto works out which files to copy into <code>_site/</code> by scanning the rendered document for things that look like assets. That scan understands Markdown image syntax and the HTML that Quarto itself generates from it, so the usual case just works:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode markdown code-with-copy"><code class="sourceCode markdown"><span id="cb1-1"><span class="al" style="color: #AD0000;
background-color: null;
font-style: inherit;">![Custom controller internals](K8s-custom-controller.png)</span></span></code></pre></div></div>
<p>It does not understand assets you reference from raw HTML you wrote by hand. An inline SVG that pulls in bitmaps through <code>&lt;image href&gt;</code> is the case that bit me:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode html code-with-copy"><code class="sourceCode html"><span id="cb2-1"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">svg</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> viewBox</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"0 0 1120 420"</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&gt;</span></span>
<span id="cb2-2">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">image</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> href</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"cc-router.png"</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> x</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"0"</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> y</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"0"</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> width</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"260"</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> height</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"180"</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">/&gt;</span></span>
<span id="cb2-3"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;/</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">svg</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&gt;</span></span></code></pre></div></div>
<p>That renders correctly in <code>quarto preview</code>, because the preview server is serving files straight out of the project directory. It breaks on <code>quarto render</code>, where the images were never copied across and every figure resolves to a 404.</p>
<p>The fix is to declare the files explicitly in the page’s front matter:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb3-1"><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">---</span></span>
<span id="cb3-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">title</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"..."</span></span>
<span id="cb3-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">resources</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb3-4"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"*.png"</span></span>
<span id="cb3-5"><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">---</span></span></code></pre></div></div>
<p><code>resources</code> accepts globs and takes paths relative to the document, so <code>"*.png"</code> covers a directory of figures without listing each one.</p>
<div class="callout callout-style-default callout-tip callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Tip
</div>
</div>
<div class="callout-body-container callout-body">
<p>Preview will not catch this, so it is worth running a real <code>quarto render</code> and opening the page out of <code>_site/</code> before publishing anything that uses hand-written HTML for its figures.</p>
</div>
</div>
<p>You can also set <code>resources</code> at the project level in <code>_quarto.yml</code>, but keep it on the page when only that page needs it. A project-wide glob copies the matching files from every directory, including ones you meant to leave out.</p>



 ]]></description>
  <category>Quarto</category>
  <guid>https://lahirudesilva.com/notes/quarto-resources-raw-html/</guid>
  <pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>
