<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>rust.solutions</title>
    <subtitle>Pragmatic systems engineering with Rust. Notes, tools and infrastructure.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://rust.solutions/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://rust.solutions"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-08-27T00:00:00+00:00</updated>
    <id>https://rust.solutions/atom.xml</id>
    <entry xml:lang="en">
        <title>DynamicUser and the StateDirectory pitfall</title>
        <published>2026-08-27T00:00:00+00:00</published>
        <updated>2026-08-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust.solutions/blog/dynamicuser-statedirectory/"/>
        <id>https://rust.solutions/blog/dynamicuser-statedirectory/</id>
        
        <content type="html" xml:base="https://rust.solutions/blog/dynamicuser-statedirectory/">&lt;p&gt;Every backend on this platform is a Rust binary launched through systemd, and
every unit is a locked-down &lt;code&gt;DynamicUser=yes&lt;&#x2F;code&gt; service. DynamicUser is great:
each instance gets a transient, unprivileged uid&#x2F;gid that exists for the life
of the unit and vanishes when it stops, taking account capabilities with it.
No shared accounts, no shell, nothing to leak between services.&lt;&#x2F;p&gt;
&lt;p&gt;But there is a trap. A dynamically allocated user does &lt;strong&gt;not&lt;&#x2F;strong&gt; automatically
get a home directory it can write to, and a naive &lt;code&gt;Persistent=true&lt;&#x2F;code&gt; does not
give you a readable storage root either. A Rust service calling
&lt;code&gt;std::fs::create_dir_all(&quot;&#x2F;var&#x2F;lib&#x2F;huck-storage&quot;)&lt;&#x2F;code&gt; under such a unit fails
with &lt;code&gt;Permission denied&lt;&#x2F;code&gt;, because the unit’s user cannot touch &lt;code&gt;&#x2F;var&#x2F;lib&lt;&#x2F;code&gt;
at all once &lt;code&gt;ProtectSystem=strict&lt;&#x2F;code&gt; is on. The directory must be &lt;em&gt;provisioned
and owned&lt;&#x2F;em&gt; by the unit before the process starts.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-one-liner-statedirectory&quot;&gt;The one-liner: StateDirectory&lt;&#x2F;h2&gt;
&lt;p&gt;The correct answer is systemd’s &lt;code&gt;StateDirectory=&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;ini&quot;&gt;[Service]
DynamicUser=yes
StateDirectory=huck-storage
Environment=HUCK_STATE_DIR=&#x2F;var&#x2F;lib&#x2F;huck-storage
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;StateDirectory=&lt;&#x2F;code&gt; makes systemd create &lt;code&gt;&#x2F;var&#x2F;lib&#x2F;huck-storage&lt;&#x2F;code&gt;, chown it to
the dynamic user, and make it writable — all before &lt;code&gt;ExecStart&lt;&#x2F;code&gt; runs.
That single directive is what lets an otherwise fully-unprivileged process
own its own persistent state.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-pitfall-we-hit-twice&quot;&gt;The pitfall we hit twice&lt;&#x2F;h2&gt;
&lt;p&gt;The subtlety is that a &lt;code&gt;StateDirectory&lt;&#x2F;code&gt; is &lt;strong&gt;per-unit&lt;&#x2F;strong&gt;. The moment two
services share a path, systemd refuses to start the second one: the
directory is already owned by another dynamic user, and the hardened unit
gets a “permission denied” on boot. That is why each service here declares
its own directory (&lt;code&gt;huck-config&lt;&#x2F;code&gt;, &lt;code&gt;huck-storage&lt;&#x2F;code&gt;, …) rather than a shared
&lt;code&gt;&#x2F;var&#x2F;lib&#x2F;huck&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;It is also why the state path must be passed explicitly via &lt;code&gt;Environment=&lt;&#x2F;code&gt;.
DynamicUser does not expand &lt;code&gt;~&lt;&#x2F;code&gt; to your new uid reliably, and relying on
&lt;code&gt;$HOME&lt;&#x2F;code&gt; in a hardened unit is fragile. One explicit variable, one matching
&lt;code&gt;StateDirectory=&lt;&#x2F;code&gt;, and the process always knows where it may write.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;atomic-writes&quot;&gt;Atomic writes&lt;&#x2F;h2&gt;
&lt;p&gt;Ownership solved, the remaining habit worth keeping is writing state
carefully. Our persistence pattern is load-at-boot plus write-on-mutation:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;std::fs::write(&amp;amp;tmp, &amp;amp;bytes)?;   &#x2F;&#x2F; state.json.tmp
std::fs::rename(&amp;amp;tmp, &amp;amp;path)?;   &#x2F;&#x2F; atomic over state.json
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Writing to a temp file then &lt;code&gt;rename&lt;&#x2F;code&gt;-ing over the target avoids a torn file
if the unit is killed mid-write — &lt;code&gt;rename&lt;&#x2F;code&gt; is atomic on the same
filesystem, so the service never observes a half-written state.json.&lt;&#x2F;p&gt;
&lt;p&gt;The rule of thumb: give every DynamicUser unit its own &lt;code&gt;StateDirectory&lt;&#x2F;code&gt;, hand
it the path through an explicit variable, and write state atomically. That is
the whole lock-down without the surprise.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Bringing rust.solutions online</title>
        <published>2026-08-26T00:00:00+00:00</published>
        <updated>2026-08-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust.solutions/blog/bringing-rust-solutions-online/"/>
        <id>https://rust.solutions/blog/bringing-rust-solutions-online/</id>
        
        <content type="html" xml:base="https://rust.solutions/blog/bringing-rust-solutions-online/">&lt;p&gt;This site is now live. Rather than a monolithic app, the domain runs on a
small edge pattern that keeps every subdomain independently deployable.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-edge-pattern&quot;&gt;The edge pattern&lt;&#x2F;h2&gt;
&lt;p&gt;A single server block owns &lt;code&gt;rust.solutions&lt;&#x2F;code&gt; and &lt;code&gt;*.rust.solutions&lt;&#x2F;code&gt;. A
&lt;code&gt;map&lt;&#x2F;code&gt; directive resolves each hostname to a loopback backend:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;nginx&quot;&gt;map $host $huck_backend {
    rust.solutions   http:&#x2F;&#x2F;127.0.0.1:13008;
    blog.rust.solutions http:&#x2F;&#x2F;127.0.0.1:13013;
    default &amp;quot;&amp;quot;;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Unknown subdomains fall through to an empty string and get rejected with
a 404 before any proxying happens. Known ones are forwarded to whatever
listens on their assigned port — a static file server today, a Rust
service tomorrow.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-loopback-ports&quot;&gt;Why loopback ports&lt;&#x2F;h2&gt;
&lt;p&gt;Each backend binds to &lt;code&gt;127.0.0.1&lt;&#x2F;code&gt; only. Nothing is directly reachable
from the internet; the TLS-terminating edge is the single front door.
Adding a service means binding one port and adding one map line.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-static-build&quot;&gt;The static build&lt;&#x2F;h2&gt;
&lt;p&gt;The apex is generated by &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.getzola.xyz&quot;&gt;Zola&lt;&#x2F;a&gt;: no JavaScript,
no client framework, one stylesheet, HTML minified at build time. The
whole page budget fits comfortably in a few kilobytes.&lt;&#x2F;p&gt;
&lt;p&gt;That’s the whole trick: boring components, clear seams between them.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
