clawdie-ai/html/docs-clawdie-si/guides/nginx-ssl.html
Sam & Claude faf060e0ce docs: introduce Layered Memory Fabric terminology (Sam & Codex)
Replaces public split-brain wording with Layered Memory Fabric, documents the skills/brain/ops planes, and sketches the shared FreeBSD/Linux install contract around PostgreSQL, ZFS/OpenZFS, and platform isolation adapters.\n\nChecks: npx --yes prettier@3 --check touched docs/html; git diff --check

---
Build: pass | Tests: FAIL — 1 failed
2026-06-13 21:32:50 +02:00

510 lines
16 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nginx + SSL — Clawdie Docs</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=DM+Mono:wght@300;400&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/css/shared.css" />
</head>
<body>
<div class="hex-bg"></div>
<div class="sidebar-overlay" id="overlay"></div>
<header class="top-bar">
<button class="mobile-menu-btn" id="menuBtn" aria-label="Toggle menu">
&#9776;
</button>
<a href="/" class="brand"><span>&#9651;</span> Clawdie Docs</a>
<div class="nav-links">
<a href="https://clawdie.si">Home</a>
<a href="https://codeberg.org/Clawdie" target="_blank" rel="noopener"
>Source</a
>
</div>
</header>
<div class="docs-layout">
<nav class="sidebar" id="sidebar">
<div class="sidebar-section">
<span class="section-label">Getting Started</span>
<ul>
<li><a href="/">Introduction</a></li>
<li><a href="/docs/install.html">Installation</a></li>
<li><a href="/docs/iso.html">ISO Install</a></li>
<li><a href="/docs/split-brain.html">Layered Memory</a></li>
</ul>
</div>
<div class="sidebar-section">
<span class="section-label">Architecture</span>
<ul>
<li><a href="/docs/">How It Works</a></li>
<li><a href="/docs/#jails-not-docker">Jails, Not Docker</a></li>
<li><a href="/docs/#wayland-first-display">Wayland Display</a></li>
<li>
<a href="/docs/#prompt-injection-and-web-browsing"
>Prompt Injection</a
>
</li>
<li>
<a href="/guides/nanoclaw-upstream.html">NanoClaw Upstream</a>
</li>
<li>
<a
href="https://codeberg.org/Clawdie/Clawdie-AI/src/branch/main/docs/public/operate/monitoring.md"
target="_blank"
rel="noopener"
>Monitoring</a
>
</li>
<li>
<a
href="https://codeberg.org/Clawdie/Clawdie-AI/src/branch/main/docs/public/operate/security.md"
target="_blank"
rel="noopener"
>Security</a
>
</li>
</ul>
</div>
<div class="sidebar-section">
<span class="section-label">Setup Guides</span>
<ul>
<li>
<a href="/guides/nginx-ssl.html" class="active">Nginx + SSL</a>
</li>
<li><a href="/guides/tailscale-vpn.html">Tailscale VPN</a></li>
</ul>
</div>
<div class="sidebar-section">
<span class="section-label">Integrations</span>
<ul>
<li><a href="/guides/stripe-agents.html">Stripe Agents</a></li>
<li><a href="/guides/protonmail.html">ProtonMail</a></li>
</ul>
</div>
<div class="sidebar-section">
<span class="section-label">Project</span>
<ul>
<li><a href="/changelog.html">Changelog</a></li>
<li><a href="/license.html">License</a></li>
</ul>
</div>
</nav>
<main class="content">
<div class="breadcrumb">
<a href="/">Home</a><span class="sep">/</span>
<a href="/docs/">Docs</a><span class="sep">/</span>
Nginx + SSL
</div>
<div class="page-header">
<h1>Nginx + <span>SSL</span></h1>
<p class="subtitle">
Web server configuration with
<a href="https://letsencrypt.org/" target="_blank" rel="noopener"
>Let's Encrypt</a
>
on
<a href="https://www.freebsd.org/" target="_blank" rel="noopener"
>FreeBSD</a
>
</p>
</div>
<p>
Our
<a href="https://nginx.org/" target="_blank" rel="noopener">nginx</a>
setup serves multiple sites from a single FreeBSD server &mdash;
clawdie.si, osa.smilepowered.org, and docs.clawdie.si. All with HTTPS
via Let's Encrypt, HTTP-to-HTTPS redirects, and clean vhost
separation.
</p>
<div class="divider"></div>
<section>
<span class="phase-badge">Step 1</span>
<h2>Installation</h2>
<h3>1.1 Install nginx</h3>
<pre><code>pkg install -y nginx
sysrc nginx_enable="YES"</code></pre>
<h3>1.2 Directory layout</h3>
<pre><code>/usr/local/etc/nginx/
&#9500;&#9472;&#9472; nginx.conf # Main config
&#9500;&#9472;&#9472; vhosts/
&#9474; &#9500;&#9472;&#9472; clawdie.conf # clawdie.si
&#9474; &#9500;&#9472;&#9472; osa.conf # osa.smilepowered.org
&#9474; &#9492;&#9472;&#9472; docs.clawdie.si.conf # docs.clawdie.si
&#9500;&#9472;&#9472; ssl/
&#9474; &#9492;&#9472;&#9472; clawdie/
&#9474; &#9500;&#9472;&#9472; fullchain.cer # Certificate chain
&#9474; &#9492;&#9472;&#9472; clawdie.key # Private key
&#9492;&#9472;&#9472; mime.types
/usr/local/www/
&#9500;&#9472;&#9472; clawdie/ # clawdie.si document root
&#9474; &#9500;&#9472;&#9472; index.html
&#9474; &#9500;&#9472;&#9472; css/
&#9474; &#9500;&#9472;&#9472; docs/
&#9474; &#9492;&#9472;&#9472; guides/
&#9500;&#9472;&#9472; docs.clawdie.si/ # docs.clawdie.si document root
&#9474; &#9500;&#9472;&#9472; index.html
&#9474; &#9500;&#9472;&#9472; css/
&#9474; &#9492;&#9472;&#9472; docs/
&#9492;&#9472;&#9472; osa/ # osa.smilepowered.org</code></pre>
</section>
<div class="divider"></div>
<section>
<span class="phase-badge">Step 2</span>
<h2>Main configuration</h2>
<p>Edit <code>/usr/local/etc/nginx/nginx.conf</code>:</p>
<pre><code>worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# Include all virtual hosts
include vhosts/*.conf;
# Default server &mdash; catch-all
server {
listen 80 default_server;
server_name _;
return 444;
}
}</code></pre>
<div class="info-box">
<span class="info-label">Note</span>
<p>
The <code>return 444</code> on the default server drops
connections to unrecognized hostnames &mdash; no response, no
information leak.
</p>
</div>
</section>
<div class="divider"></div>
<section>
<span class="phase-badge">Step 3</span>
<h2>Virtual host: clawdie.si</h2>
<p>Create <code>/usr/local/etc/nginx/vhosts/clawdie.conf</code>:</p>
<pre><code># HTTP &rarr; HTTPS redirect
server {
listen 80;
server_name clawdie.si www.clawdie.si;
return 301 https://clawdie.si$request_uri;
}
# www &rarr; non-www redirect (HTTPS)
server {
listen 443 ssl;
server_name www.clawdie.si;
ssl_certificate /usr/local/etc/nginx/ssl/clawdie/fullchain.cer;
ssl_certificate_key /usr/local/etc/nginx/ssl/clawdie/clawdie.key;
return 301 https://clawdie.si$request_uri;
}
# Main site
server {
listen 443 ssl;
server_name clawdie.si;
root /usr/local/www/clawdie;
index index.html;
ssl_certificate /usr/local/etc/nginx/ssl/clawdie/fullchain.cer;
ssl_certificate_key /usr/local/etc/nginx/ssl/clawdie/clawdie.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
try_files $uri $uri/ =404;
}
}</code></pre>
<h3>Key decisions</h3>
<ul>
<li>
<strong>Non-www canonical:</strong> All traffic goes to
<code>clawdie.si</code> (not www)
</li>
<li>
<strong>HTTP &rarr; HTTPS:</strong> All plain HTTP redirects to
secure
</li>
<li><strong>TLS 1.2+:</strong> No older, insecure protocols</li>
<li>
<strong>Static serving:</strong> No application server needed for
the site
</li>
</ul>
</section>
<div class="divider"></div>
<section>
<span class="phase-badge">Step 4</span>
<h2>SSL with Let's Encrypt</h2>
<h3>4.1 Install acme.sh</h3>
<pre><code># acme.sh is a pure shell Let's Encrypt client
pkg install -y acme.sh
# Or install from source
curl https://get.acme.sh | sh</code></pre>
<h3>4.2 Issue certificate</h3>
<pre><code># Using webroot validation
acme.sh --issue \
-d clawdie.si \
-d www.clawdie.si \
-w /usr/local/www/clawdie</code></pre>
<h3>4.3 Install certificate</h3>
<pre><code>mkdir -p /usr/local/etc/nginx/ssl/clawdie
acme.sh --install-cert \
-d clawdie.si \
--key-file /usr/local/etc/nginx/ssl/clawdie/clawdie.key \
--fullchain-file /usr/local/etc/nginx/ssl/clawdie/fullchain.cer \
--reloadcmd "service nginx reload"</code></pre>
<div class="info-box success">
<span class="info-label">Auto-renewal</span>
<p>
acme.sh sets up a cron job automatically. Certificates renew every
60 days. The <code>--reloadcmd</code> ensures nginx picks up the
new cert.
</p>
</div>
<h3>4.4 Verify</h3>
<pre><code># Test the configuration
nginx -t
# Reload
service nginx reload
# Check certificate
openssl s_client -connect clawdie.si:443 -servername clawdie.si &lt; /dev/null 2>/dev/null | openssl x509 -noout -dates</code></pre>
</section>
<div class="divider"></div>
<section>
<span class="phase-badge">Step 5</span>
<h2>Adding more sites</h2>
<p>
The vhost pattern makes it easy to add new sites. For example,
<code>docs.clawdie.si</code> as a public static documentation
surface:
</p>
<pre><code># /usr/local/etc/nginx/vhosts/docs.clawdie.si.conf
server {
listen 80;
server_name docs.clawdie.si;
return 301 https://docs.clawdie.si$request_uri;
}
server {
listen 443 ssl;
server_name docs.clawdie.si;
root /usr/local/www/docs.clawdie.si;
index index.html;
ssl_certificate /usr/local/etc/nginx/ssl/docs/fullchain.cer;
ssl_certificate_key /usr/local/etc/nginx/ssl/docs/docs.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location /docs/ {
try_files $uri $uri/ /docs/index.html =404;
}
location / {
try_files $uri $uri/ =404;
}
}</code></pre>
<div class="info-box">
<span class="info-label">DNS reminder</span>
<p>
Don't forget to add the DNS A/AAAA record for the subdomain before
requesting the SSL certificate.
</p>
</div>
<div class="info-box">
<span class="info-label">Security headers</span>
<p>
Apply a small baseline to every public vhost:
<code>X-Content-Type-Options</code>, <code>X-Frame-Options</code>,
<code>X-XSS-Protection</code>, and <code>Referrer-Policy</code>.
It is low-effort hardening worth standardising.
</p>
</div>
</section>
<div class="divider"></div>
<section>
<h2>Useful commands</h2>
<table>
<thead>
<tr>
<th>Command</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>nginx -t</code></td>
<td>Test configuration syntax</td>
</tr>
<tr>
<td><code>service nginx reload</code></td>
<td>Reload without downtime</td>
</tr>
<tr>
<td><code>service nginx restart</code></td>
<td>Full restart</td>
</tr>
<tr>
<td><code>tail -f /var/log/nginx/error.log</code></td>
<td>Watch error log</td>
</tr>
<tr>
<td><code>tail -f /var/log/nginx/access.log</code></td>
<td>Watch access log</td>
</tr>
<tr>
<td><code>acme.sh --list</code></td>
<td>List managed certificates</td>
</tr>
<tr>
<td><code>acme.sh --renew -d clawdie.si</code></td>
<td>Force certificate renewal</td>
</tr>
</tbody>
</table>
</section>
<footer>
<div class="footer-left">
<a href="https://clawdie.si">Clawdie AI</a> &middot;
<a
href="https://osa.smilepowered.org"
target="_blank"
rel="noopener"
>OSA — Mission Statement</a
>
&middot; Nginx + SSL<br />
<a
href="https://codeberg.org/Clawdie/Clawdie-AI/src/branch/main/html/docs-clawdie-si/guides/nginx-ssl.html"
target="_blank"
rel="noopener"
>Page source</a
>
·
<a
href="https://codeberg.org/Clawdie/Clawdie-AI/src/branch/main/setup/cms.ts"
target="_blank"
rel="noopener"
>CMS nginx setup</a
>
·
<a
href="https://codeberg.org/Clawdie/Clawdie-AI/src/branch/main/docs/internal/CMS-DEPLOYMENT-PLAN.md"
target="_blank"
rel="noopener"
>Plan notes</a
><br />
Last updated: 06.mar.2026
</div>
<div class="footer-hex">&#9651;</div>
</footer>
</main>
<aside class="toc">
<p class="toc-title">On this page</p>
<nav id="toc-list"></nav>
</aside>
</div>
<script>
const toc = document.getElementById('toc-list');
if (toc) {
document.querySelectorAll('.content h2, .content h3').forEach((h) => {
if (!h.id)
h.id = h.textContent
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-');
const a = document.createElement('a');
a.href = '#' + h.id;
a.className = 'toc-link' + (h.tagName === 'H3' ? ' toc-sub' : '');
a.textContent = h.textContent;
toc.appendChild(a);
});
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((e, i) => {
if (e.isIntersecting)
setTimeout(() => e.target.classList.add('visible'), i * 80);
});
},
{ threshold: 0.08 },
);
document.querySelectorAll('section').forEach((s) => observer.observe(s));
const menuBtn = document.getElementById('menuBtn');
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('overlay');
if (menuBtn && sidebar) {
menuBtn.addEventListener('click', () => {
sidebar.classList.toggle('open');
overlay?.classList.toggle('open');
});
overlay?.addEventListener('click', () => {
sidebar.classList.remove('open');
overlay.classList.remove('open');
});
}
</script>
</body>
</html>