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
510 lines
16 KiB
HTML
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">
|
|
☰
|
|
</button>
|
|
<a href="/" class="brand"><span>△</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 —
|
|
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/
|
|
├── nginx.conf # Main config
|
|
├── vhosts/
|
|
│ ├── clawdie.conf # clawdie.si
|
|
│ ├── osa.conf # osa.smilepowered.org
|
|
│ └── docs.clawdie.si.conf # docs.clawdie.si
|
|
├── ssl/
|
|
│ └── clawdie/
|
|
│ ├── fullchain.cer # Certificate chain
|
|
│ └── clawdie.key # Private key
|
|
└── mime.types
|
|
|
|
/usr/local/www/
|
|
├── clawdie/ # clawdie.si document root
|
|
│ ├── index.html
|
|
│ ├── css/
|
|
│ ├── docs/
|
|
│ └── guides/
|
|
├── docs.clawdie.si/ # docs.clawdie.si document root
|
|
│ ├── index.html
|
|
│ ├── css/
|
|
│ └── docs/
|
|
└── 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 — 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 — 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 → HTTPS redirect
|
|
server {
|
|
listen 80;
|
|
server_name clawdie.si www.clawdie.si;
|
|
return 301 https://clawdie.si$request_uri;
|
|
}
|
|
|
|
# www → 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 → 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 < /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> ·
|
|
<a
|
|
href="https://osa.smilepowered.org"
|
|
target="_blank"
|
|
rel="noopener"
|
|
>OSA — Mission Statement</a
|
|
>
|
|
· 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">△</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>
|