certbridge / security-setup-guide.html
VanyaJ's picture
Add premium security setup guide html for Nginx Proxy Manager and Authentik
fed0bec verified
Raw
History Blame Contribute Delete
42.7 kB
<!DOCTYPE html>
<html lang="ko" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="html-title">CertBridge λ³΄μ•ˆ κ°•ν™” 및 에어갭 μ„€μ • κ°€μ΄λ“œ</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* ─────────────────────────────────────────
CertBridge Security Guide Design System
Dual Theme Sync (Light + Dark)
Premium Modern Style
───────────────────────────────────────── */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
transition: background-color 0.25s ease, border-color 0.25s ease, color 0.25s ease;
}
:root {
/* Light Theme (Default) */
--bg-primary: #ffffff;
--bg-secondary: #f8fafc;
--bg-card: #ffffff;
--bg-glass: rgba(0, 0, 0, 0.015);
--bg-hover: rgba(0, 0, 0, 0.025);
--text-primary: #1e293b;
--text-secondary: #475569;
--text-muted: #94a3b8;
--accent-blue: #2563eb;
--accent-blue-hover: #1d4ed8;
--accent-blue-subtle: rgba(37, 99, 235, 0.08);
--accent-emerald: #059669;
--accent-amber: #d97706;
--accent-red: #dc2626;
--accent-purple: #7c3aed;
--border-subtle: rgba(0, 0, 0, 0.08);
--border-active: rgba(37, 99, 235, 0.4);
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.03);
--shadow-card-hover: 0 2px 8px rgba(0, 0, 0, 0.06);
--shadow-glow: none;
--grad: linear-gradient(135deg, #2563eb 0%, #0284c7 100%);
--grad-hero: linear-gradient(135deg, rgba(37, 99, 235, 0.05) 0%, rgba(2, 132, 199, 0.05) 100%);
--hero-text: #1e293b;
--hero-desc: #475569;
}
[data-theme='dark'] {
/* Dark Theme */
--bg-primary: #0f1117;
--bg-secondary: #1a1d27;
--bg-card: #1e2130;
--bg-glass: rgba(255, 255, 255, 0.04);
--bg-hover: rgba(255, 255, 255, 0.05);
--text-primary: #e8eaed;
--text-secondary: #9aa0b0;
--text-muted: #5f6577;
--accent-blue: #3b82f6;
--accent-blue-hover: #2563eb;
--accent-blue-subtle: rgba(59, 130, 246, 0.12);
--accent-emerald: #10b981;
--accent-amber: #f59e0b;
--accent-red: #ef4444;
--accent-purple: #8b5cf6;
--border-subtle: rgba(255, 255, 255, 0.07);
--border-active: rgba(59, 130, 246, 0.5);
--shadow-card: 0 1px 4px rgba(0, 0, 0, 0.2);
--shadow-card-hover: 0 4px 16px rgba(0, 0, 0, 0.3);
--shadow-glow: 0 0 20px rgba(59, 130, 246, 0.1);
--grad: linear-gradient(135deg, #3b82f6 0%, #0ea5e9 100%);
--grad-hero: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(14, 165, 233, 0.08) 100%);
--hero-text: #e8eaed;
--hero-desc: #9aa0b0;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
display: flex;
min-height: 100vh;
}
::selection {
background: var(--accent-blue-subtle);
color: var(--accent-blue);
}
a {
color: var(--accent-blue);
text-decoration: none;
font-weight: 500;
}
a:hover {
text-decoration: underline;
}
/* Sidebar Styling */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 280px;
height: 100vh;
background: var(--bg-secondary);
border-right: 1px solid var(--border-subtle);
padding: 24px 0;
overflow-y: auto;
z-index: 100;
}
.sidebar-logo {
padding: 0 24px 20px;
border-bottom: 1px solid var(--border-subtle);
margin-bottom: 16px;
}
.sidebar-logo h1 {
font-size: 16px;
font-weight: 700;
color: var(--accent-blue);
letter-spacing: -0.3px;
font-family: 'Outfit', sans-serif;
}
.sidebar-logo p {
font-size: 11px;
color: var(--text-muted);
margin-top: 4px;
}
.sidebar nav a {
display: flex;
align-items: center;
padding: 10px 24px;
font-size: 13px;
color: var(--text-secondary);
transition: all 0.2s;
border-left: 3px solid transparent;
text-decoration: none;
}
.sidebar nav a:hover, .sidebar nav a.active {
color: var(--accent-blue);
background: var(--accent-blue-subtle);
border-left-color: var(--accent-blue);
}
.sidebar nav a .num {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 4px;
background: var(--bg-glass);
font-size: 10px;
font-weight: 600;
margin-right: 10px;
color: var(--text-muted);
}
.sidebar nav a.active .num {
background: var(--accent-blue);
color: white;
}
/* Main Area */
.main {
margin-left: 280px;
flex: 1;
padding: 0;
overflow-y: auto;
height: 100vh;
}
/* Embedded in Iframe */
body.in-iframe .sidebar {
display: none !important;
}
body.in-iframe .main {
margin-left: 0 !important;
width: 100% !important;
height: 100% !important;
}
body.in-iframe .content {
padding: 32px 24px 60px;
max-width: 100%;
}
/* Hero Header */
.hero {
background: var(--grad-hero);
padding: 48px 40px;
border-bottom: 1px solid var(--border-subtle);
position: relative;
overflow: hidden;
}
.hero h1 {
font-size: 26px;
font-weight: 700;
color: var(--hero-text);
font-family: 'Outfit', sans-serif;
letter-spacing: -0.5px;
}
.hero p {
font-size: 13.5px;
color: var(--hero-desc);
margin-top: 8px;
max-width: 800px;
}
.hero .badge {
display: inline-block;
padding: 4px 12px;
background: var(--bg-glass);
border: 1px solid var(--border-subtle);
border-radius: 20px;
font-size: 11px;
font-weight: 600;
color: var(--text-secondary);
margin-top: 12px;
}
/* Content Container */
.content {
padding: 48px;
max-width: 1000px;
margin: 0 auto;
}
.section {
margin-bottom: 48px;
scroll-margin-top: 40px;
}
h2 {
font-size: 18px;
font-weight: 700;
margin-bottom: 16px;
position: relative;
padding-left: 12px;
color: var(--text-primary);
font-family: 'Outfit', sans-serif;
}
h2::before {
content: '';
position: absolute;
left: 0;
top: 3px;
bottom: 3px;
width: 3px;
border-radius: 1.5px;
background: var(--accent-blue);
}
h3 {
font-size: 14px;
font-weight: 600;
margin: 24px 0 12px;
color: var(--accent-blue);
}
h4 {
font-size: 13px;
font-weight: 600;
margin: 16px 0 8px;
color: var(--text-primary);
}
p, li {
margin-bottom: 8px;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.6;
}
ul, ol {
margin: 0 0 16px 20px;
}
/* Cards & Layout */
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-subtle);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
box-shadow: var(--shadow-card);
}
.card.tip, .card.info {
border-color: rgba(37, 99, 235, 0.2);
background: var(--accent-blue-subtle);
}
.card.tip h4, .card.info h4 {
color: var(--accent-blue);
margin-top: 0;
}
.card.warn {
border-color: rgba(220, 38, 38, 0.2);
background: rgba(220, 38, 38, 0.03);
}
.card.warn h4 {
color: var(--accent-red);
margin-top: 0;
}
/* Table */
table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--border-subtle);
}
th {
background: var(--bg-glass);
padding: 10px 14px;
text-align: left;
font-size: 12px;
font-weight: 600;
color: var(--text-primary);
border-bottom: 1px solid var(--border-subtle);
}
td {
padding: 10px 14px;
font-size: 12px;
border-top: 1px solid var(--border-subtle);
color: var(--text-secondary);
background: var(--bg-secondary);
}
tr:hover td {
background: var(--bg-hover);
}
.req-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin: 16px 0;
}
.req-item {
background: var(--bg-secondary);
border: 1px solid var(--border-subtle);
border-radius: 12px;
padding: 16px;
text-align: center;
box-shadow: var(--shadow-card);
}
.req-item .icon {
font-size: 24px;
margin-bottom: 6px;
}
.req-item .label {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.req-item .value {
font-size: 13.5px;
font-weight: 600;
color: var(--text-primary);
margin-top: 4px;
}
.step {
display: flex;
gap: 16px;
margin: 16px 0;
padding: 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-subtle);
border-radius: 12px;
box-shadow: var(--shadow-card);
}
.step-num {
flex-shrink: 0;
width: 28px;
height: 28px;
border-radius: 6px;
background: var(--grad);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 12px;
color: #fff;
}
.step-body h4 {
margin: 0 0 4px;
color: var(--text-primary);
}
.step-body p {
margin: 0;
color: var(--text-secondary);
font-size: 12.5px;
}
.port-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
background: var(--accent-blue-subtle);
color: var(--accent-blue);
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
font-weight: 600;
}
.top-toolbar {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
display: flex;
gap: 8px;
}
.lang-selector {
background: var(--bg-secondary);
border: 1px solid var(--border-subtle);
padding: 4px 10px;
border-radius: 6px;
color: var(--text-secondary);
font-size: 11.5px;
outline: none;
cursor: pointer;
box-shadow: var(--shadow-card);
}
.lang-selector:hover {
border-color: var(--accent-blue);
color: var(--text-primary);
}
/* Code */
code {
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-size: 11px;
background: var(--accent-blue-subtle);
padding: 2px 5px;
border-radius: 4px;
color: var(--accent-blue);
}
.code-wrap {
position: relative;
margin: 16px 0;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--border-subtle);
box-shadow: var(--shadow-card);
}
.code-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 14px;
background: var(--bg-hover);
border-bottom: 1px solid var(--border-subtle);
}
.code-header span {
font-size: 11px;
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
}
.copy-btn {
padding: 2px 8px;
border: 1px solid var(--border-subtle);
border-radius: 4px;
background: var(--bg-secondary);
color: var(--text-secondary);
cursor: pointer;
font-size: 10px;
transition: all 0.15s;
}
.copy-btn:hover {
background: var(--accent-blue);
color: white;
border-color: var(--accent-blue);
}
pre {
background: var(--bg-card);
padding: 12px 16px;
overflow-x: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 11.5px;
line-height: 1.5;
color: var(--text-secondary);
}
pre .comment { color: var(--text-muted); font-style: italic; }
pre .keyword { color: var(--accent-purple); font-weight: 600; }
pre .string { color: var(--accent-emerald); }
pre .cmd { color: var(--accent-blue); }
@media (max-width: 900px) {
.sidebar { display: none; }
.main { margin-left: 0; }
.content { padding: 24px; }
.hero { padding: 32px 24px; }
.hero h1 { font-size: 22px; }
}
</style>
</head>
<body>
<aside class="sidebar">
<div class="sidebar-logo">
<h1>πŸ›‘οΈ CertBridge Security</h1>
<p data-i18n="sidebar.ver">λ³΄μ•ˆ 및 에어갭 κ°€μ΄λ“œ v0.1.0</p>
</div>
<nav id="nav">
<a href="#overview" class="active"><span class="num">1</span><span data-i18n="nav.overview">μ•„ν‚€ν…μ²˜ κ°œμš”</span></a>
<a href="#airgap-prep"><span class="num">2</span><span data-i18n="nav.prep">에어갭 νŒ¨ν‚€μ§€ μ€€λΉ„</span></a>
<a href="#server-install"><span class="num">3</span><span data-i18n="nav.install">μ˜€ν”„λΌμΈ μ„€μΉ˜ ν”„λ‘œν† μ½œ</span></a>
<a href="#npm-setup"><span class="num">4</span><span data-i18n="nav.npm">Nginx Proxy Manager</span></a>
<a href="#authentik-setup"><span class="num">5</span><span data-i18n="nav.authentik">Authentik SSO 연동</span></a>
<a href="#troubleshooting"><span class="num">6</span><span data-i18n="nav.trouble">문제 ν•΄κ²°</span></a>
</nav>
</aside>
<div class="main">
<div class="top-toolbar">
<select id="lang-select" class="lang-selector" onchange="changeLanguage(this.value)">
<option value="ko">ν•œκ΅­μ–΄</option>
<option value="en">English</option>
</select>
</div>
<div class="hero">
<h1 data-i18n="hero.title">CertBridge λ³΄μ•ˆ κ°•ν™” 및 에어갭 κ°€μ΄λ“œ</h1>
<p data-i18n="hero.desc">μ˜€ν”„λΌμΈ(Air-Gap) μš°λΆ„νˆ¬ ν™˜κ²½μ—μ„œ Nginx Proxy Manager와 Authentik SSOλ₯Ό ν™œμš©ν•˜μ—¬ SSL λ³΄μ•ˆ ν„°λ―Έλ„€μ΄μ…˜ 및 계정 톡합 인증 μ‹œμŠ€ν…œμ„ μ•ˆμ „ν•˜κ²Œ κ΅¬μ„±ν•˜λŠ” κ³ κΈ‰ κ°€μ΄λ“œμž…λ‹ˆλ‹€.</p>
<span class="badge" data-i18n="hero.badge">πŸ”’ v0.1.0 Β· Ubuntu amd64 Β· NPM & Authentik</span>
</div>
<div class="content">
<!-- 1. Overview -->
<section class="section" id="overview">
<h2 data-i18n="overview.title">1. λ³΄μ•ˆ κ°•ν™” μ•„ν‚€ν…μ²˜ κ°œμš”</h2>
<p data-i18n="overview.desc1">κΈ°μ‘΄ 쀑앙 μ„œλ²„ μΈν”„λΌλŠ” 포트 λ…ΈμΆœ 및 무인증 ν†΅μ‹ μœΌλ‘œ 인해 λ³΄μ•ˆ μœ„ν˜‘μ— μ·¨μ•½ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ³Έ κ°œμ • μŠ€νƒμ—μ„œλŠ” <b>Nginx Proxy Manager (NPM)</b>와 <b>Authentik SSO</b>λ₯Ό 쀑앙 인프라에 ν†΅ν•©ν•˜μ—¬ μ „λ°© λ³΄μ•ˆ μž₯벽을 κ΅¬μΆ•ν•©λ‹ˆλ‹€.</p>
<div class="card info">
<h4 data-i18n="overview.strategy_title">πŸ’‘ μžμ› μ΅œμ ν™” 섀계 μ „λž΅ (Postgres/Redis 곡유)</h4>
<p data-i18n="overview.strategy_desc">μƒˆλ‘œ μΆ”κ°€λ˜λŠ” SSO(Authentik) μ„œλΉ„μŠ€μ˜ μ˜€λ²„ν—€λ“œλ₯Ό μ œμ–΄ν•˜κΈ° μœ„ν•΄, κΈ°μ‘΄ PostgreSQL 및 Redis μ»¨ν…Œμ΄λ„ˆ μžμ›μ„ κ³ μŠ€λž€νžˆ μž¬ν™œμš©ν•©λ‹ˆλ‹€.</p>
<ul>
<li><b>PostgreSQL 16:</b> <code>authentik</code> μ΄λΌλŠ” λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό κΈ°μ‘΄ PostgreSQL 인프라 내에 μƒμ„±ν•˜μ—¬ μžμ›μ„ κ³΅μœ ν•©λ‹ˆλ‹€.</li>
<li><b>Redis 7:</b> DB Index <code>3</code>을 독립 ν• λ‹Ήν•˜μ—¬ κΈ°μ‘΄ CertBridge용 인덱슀 <code>2</code> 및 n8n λ“±κ³Όμ˜ μΆ©λŒμ„ λ°©μ§€ν•©λ‹ˆλ‹€.</li>
</ul>
</div>
<h3 data-i18n="overview.net_title">🌐 μ„œλΉ„μŠ€ 포트 λ§΅ & λ„€νŠΈμ›Œν¬ ν† ν΄λ‘œμ§€</h3>
<table>
<thead>
<tr>
<th>μ»¨ν…Œμ΄λ„ˆλͺ…</th>
<th>기본 포트</th>
<th>μ—­ν• </th>
<th>λ³΄μ•ˆ μƒνƒœ</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>certbridge-npm</code></td>
<td><span class="port-badge">80</span>, <span class="port-badge">443</span>, <span class="port-badge">81</span></td>
<td>Edge Reverse Proxy & SSL (Web UI)</td>
<td>μ™ΈλΆ€ λ…ΈμΆœ (μœ μΌν•œ μ§„μž…μ )</td>
</tr>
<tr>
<td><code>certbridge-authentik-server</code></td>
<td><span class="port-badge">9000</span>, <span class="port-badge">9443</span></td>
<td>Authentik Core Portal & API</td>
<td>λ‚΄λΆ€ λΈŒλ¦Ώμ§€λ§ λ˜λŠ” NPM 연동</td>
</tr>
<tr>
<td><code>certbridge-api</code></td>
<td><span class="port-badge">8090</span></td>
<td>CertBridge REST API Server</td>
<td>내뢀망 μ „μš© (NPM을 ν†΅ν•΄μ„œλ§Œ HTTPS μ™ΈλΆ€ λ…ΈμΆœ)</td>
</tr>
<tr>
<td><code>certbridge-minio</code></td>
<td><span class="port-badge">9010</span>, <span class="port-badge">9011</span></td>
<td>Object Storage & Management Console</td>
<td>NPM Proxy Provider둜 SSO 연동 ν†΅μ œ</td>
</tr>
<tr>
<td><code>certbridge-neo4j</code></td>
<td><span class="port-badge">7474</span>, <span class="port-badge">7687</span></td>
<td>Neo4j Graph Database & Browser UI</td>
<td>NPM Proxy Provider둜 SSO 연동 ν†΅μ œ</td>
</tr>
</tbody>
</table>
</section>
<!-- 2. Air-gap Prep -->
<section class="section" id="airgap-prep">
<h2 data-i18n="prep.title">2. 에어갭(μ˜€ν”„λΌμΈ) νŒ¨ν‚€μ§€ μ€€λΉ„</h2>
<p data-i18n="prep.desc">인터넷 연결이 μ „ν˜€ λΆˆκ°€λŠ₯ν•œ <b>에어갭(Air-Gap) 폐쇄망 μš°λΆ„νˆ¬ μ„œλ²„</b>μ—μ„œ μ„€μΉ˜ν•˜κΈ° μœ„ν•΄μ„œλŠ”, 인터넷이 μ—°κ²°λœ 사전 λ¨Έμ‹ μ—μ„œ ν•„μš”ν•œ λͺ¨λ“  Docker 이미지와 νŒ¨ν‚€μ§€λ₯Ό λ‹€μš΄λ‘œλ“œν•΄μ•Ό ν•©λ‹ˆλ‹€.</p>
<h3 data-i18n="prep.step1">2-1. Hugging Faceμ—μ„œ 에어갭 파일 λ‹€μš΄λ‘œλ“œ</h3>
<p data-i18n="prep.desc2">μš°λ¦¬λŠ” Hugging Face μ €μž₯μ†Œ (<code>VanyaJ/certbridge</code>)에 미리 λΉŒλ“œλœ <b>linux/amd64</b> ν”Œλž«νΌ μ „μš© Docker 이미지 μ•„μΉ΄μ΄λΈŒμ™€ μ„œλ²„ νŒ¨ν‚€μ§€λ₯Ό 배포해 λ‘μ—ˆμŠ΅λ‹ˆλ‹€.</p>
<div class="code-wrap">
<div class="code-header"><span>Download Assets</span><button class="copy-btn" onclick="copyCode(this)">볡사</button></div>
<pre><span class="comment"># 1. CertBridge 핡심 μ„œλ²„ μ•„μΉ΄μ΄λΈŒ λ‹€μš΄λ‘œλ“œ</span>
<span class="cmd">wget</span> https://huggingface.co/VanyaJ/certbridge/resolve/main/certbridge-server-v0.1.0-ubuntu.tar.gz
<span class="comment"># 2. 에어갭 Docker 이미지 λ‹€μš΄λ‘œλ“œ (docker-images/ 디렉토리에 λ°°μΉ˜ν•  것)</span>
<span class="cmd">mkdir</span> -p docker-images
<span class="cmd">wget</span> -P docker-images/ https://huggingface.co/VanyaJ/certbridge/resolve/main/docker-images/pgvector_pgvector_pg16.tar.gz
<span class="cmd">wget</span> -P docker-images/ https://huggingface.co/VanyaJ/certbridge/resolve/main/docker-images/redis_7-alpine.tar.gz
<span class="cmd">wget</span> -P docker-images/ https://huggingface.co/VanyaJ/certbridge/resolve/main/docker-images/minio_minio_latest.tar.gz
<span class="cmd">wget</span> -P docker-images/ https://huggingface.co/VanyaJ/certbridge/resolve/main/docker-images/neo4j_5-community.tar.gz
<span class="cmd">wget</span> -P docker-images/ https://huggingface.co/VanyaJ/certbridge/resolve/main/docker-images/node_20-alpine.tar.gz
<span class="cmd">wget</span> -P docker-images/ https://huggingface.co/VanyaJ/certbridge/resolve/main/docker-images/jc21_nginx-proxy-manager_latest.tar.gz
<span class="cmd">wget</span> -P docker-images/ https://huggingface.co/VanyaJ/certbridge/resolve/main/docker-images/ghcr.io_goauthentik_server_2024.4.2.tar.gz</pre>
</div>
<h3 data-i18n="prep.step2">2-2. (λŒ€μ•ˆ) prepare-airgap-package.sh ν™œμš©</h3>
<p data-i18n="prep.desc3">인터넷이 μ—°κ²°λœ μ™ΈλΆ€ μš°λΆ„νˆ¬ PCμ—μ„œ 직접 이 슀크립트λ₯Ό μˆ˜ν–‰ν•˜λ©΄, Docker 이미지 및 Node/Docker deb μ„€μΉ˜ νŒ¨ν‚€μ§€κΉŒμ§€ ν•˜λ‚˜μ˜ λ²ˆλ“€λ‘œ λ¬Άμ–΄ μ••μΆ•ν•΄ μ€λ‹ˆλ‹€.</p>
<div class="code-wrap">
<div class="code-header"><span>bash</span><button class="copy-btn" onclick="copyCode(this)">볡사</button></div>
<pre><span class="comment"># 슀크립트 μ‹€ν–‰ν•˜μ—¬ ν•˜λ‚˜μ˜ λŒ€μš©λŸ‰ νŒ¨ν‚€μ§€λ‘œ 아카이빙</span>
<span class="cmd">bash</span> prepare-airgap-package.sh</pre>
</div>
</section>
<!-- 3. Offline Installation -->
<section class="section" id="server-install">
<h2 data-i18n="install.title">3. μ˜€ν”„λΌμΈ μ„€μΉ˜ ν”„λ‘œν† μ½œ (Ubuntu)</h2>
<p data-i18n="install.desc">λ‹€μš΄λ‘œλ“œν•œ νŒ¨ν‚€μ§€μ™€ <code>docker-images</code> 폴더λ₯Ό 이동식 μ €μž₯μž₯치(USB/μ™Έμž₯ν•˜λ“œ λ“±) λ˜λŠ” 내뢀망 파일 곡유λ₯Ό 톡해 에어갭 λŒ€μƒ μš°λΆ„νˆ¬ μ„œλ²„λ‘œ λ³΅μ‚¬ν•©λ‹ˆλ‹€.</p>
<div class="step">
<div class="step-num">1</div>
<div class="step-body">
<h4 data-i18n="install.step1_title">νŒ¨ν‚€μ§€ μ••μΆ• ν•΄μ œ 및 이미지 디렉토리 병합</h4>
<p data-i18n="install.step1_desc">μ„œλ²„ νŒ¨ν‚€μ§€λ₯Ό ν•΄μ œν•œ 경둜 μ•„λž˜λ‘œ, κ°œλ³„ λ‹€μš΄λ‘œλ“œν•œ <code>docker-images</code> 디렉토리λ₯Ό λ³‘ν•©ν•©λ‹ˆλ‹€.</p>
<div class="code-wrap">
<pre><span class="cmd">tar</span> -xzf certbridge-server-v0.1.0-ubuntu.tar.gz
<span class="cmd">cd</span> certbridge-server-ubuntu
<span class="comment"># λ‹€μš΄λ‘œλ“œν•œ docker-images 폴더λ₯Ό 이곳으둜 볡사</span>
<span class="cmd">mv</span> /path/to/downloaded/docker-images ./</pre>
</div>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-body">
<h4 data-i18n="install.step2_title">μžλ™ μ…‹μ—… 슀크립트 μ‹€ν–‰</h4>
<p data-i18n="install.step2_desc">μ˜€ν”„λΌμΈ μ„€μΉ˜μš© μ…‹μ—… 슀크립트 <code>setup-airgap.sh</code>에 μ‹€ν–‰ κΆŒν•œμ„ λΆ€μ—¬ν•˜κ³  κ°€λ™ν•©λ‹ˆλ‹€. 이 μŠ€ν¬λ¦½νŠΈλŠ” Docker load λͺ…령을 톡해 μ˜€ν”„λΌμΈ tar 및 tar.gz 이미지λ₯Ό 전체 λ³΅κ΅¬ν•˜κ³  인프라 μ„œλΉ„μŠ€λ₯Ό κΈ°λ™ν•©λ‹ˆλ‹€.</p>
<div class="code-wrap">
<pre><span class="cmd">chmod</span> +x setup-airgap.sh
<span class="cmd">sudo</span> ./setup-airgap.sh</pre>
</div>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-body">
<h4 data-i18n="install.step3_title">독립 λͺ¨λ“œ(--profile standalone) μ„œλΉ„μŠ€ 기동 μƒνƒœ 쑰회</h4>
<p data-i18n="install.step3_desc">NPM 및 Authentik μ„œλΉ„μŠ€λ₯Ό ν¬ν•¨ν•˜μ—¬ 정상 ꡬ동 쀑인지 μ•„λž˜ λͺ…λ ΉμœΌλ‘œ ν™•μΈν•©λ‹ˆλ‹€.</p>
<div class="code-wrap">
<pre><span class="cmd">sudo docker compose</span> --profile standalone ps</pre>
</div>
</div>
</div>
</section>
<!-- 4. Nginx Proxy Manager Setup -->
<section class="section" id="npm-setup">
<h2 data-i18n="npm.title">4. Nginx Proxy Manager μ„€μ • κ°€μ΄λ“œ</h2>
<p data-i18n="npm.desc">Nginx Proxy Manager(NPM)λŠ” CertBridge μŠ€νƒμ˜ μ΅œμ „λ°©μ—μ„œ λͺ¨λ“  μ™ΈλΆ€ μœ μž… μš”μ²­μ„ μ²˜λ¦¬ν•˜κ³ , SSL μ•”ν˜Έν™” 톡신을 μ’…λ‹¨ν•˜λ©°, 도메인/경둜 λΆ„κΈ°λ₯Ό μˆ˜ν–‰ν•©λ‹ˆλ‹€.</p>
<div class="step">
<div class="step-num">1</div>
<div class="step-body">
<h4 data-i18n="npm.step1_title">κ΄€λ¦¬μž ν™”λ©΄(Admin UI) 둜그인</h4>
<p data-i18n="npm.step1_desc">μ›Ή λΈŒλΌμš°μ €λ‘œ <code>http://<SERVER_IP>:81</code> 에 μ ‘μ†ν•©λ‹ˆλ‹€. 졜초 κΈ°λ³Έ 둜그인 계정은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.</p>
<ul>
<li><b>Email Address:</b> <code>admin@example.com</code></li>
<li><b>Password:</b> <code>changeme</code></li>
</ul>
<p class="comment">β€» 둜그인 직후 μ¦‰μ‹œ κ΄€λ¦¬μž 이메일과 λΉ„λ°€λ²ˆν˜Έλ₯Ό λ³€κ²½ν•΄μ•Ό ν•©λ‹ˆλ‹€.</p>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-body">
<h4 data-i18n="npm.step2_title">사섀 SSL/TLS μΈμ¦μ„œ μΆ”κ°€ (에어갭 ν™˜κ²½)</h4>
<p data-i18n="npm.step2_desc">인프라망이 에어갭 μ˜€ν”„λΌμΈμ΄λ―€λ‘œ Let's Encrypt μžλ™ λ°œκΈ‰μ„ μ‚¬μš©ν•  수 μ—†μŠ΅λ‹ˆλ‹€. κΈ°κ΄€ λ‚΄λΆ€ CA λ˜λŠ” μ‚¬μ„€λ‘œ λ°œκΈ‰λ°›μ€ μΈμ¦μ„œ νŒŒμΌμ„ μˆ˜λ™ 등둝해야 ν•©λ‹ˆλ‹€.</p>
<ol>
<li>NPM 상단 λ©”λ‰΄μ˜ <b>"SSL Certificates"</b> νƒ­ 클릭</li>
<li>우츑 상단 <b>"Add SSL Certificate"</b> &gt; <b>"Custom"</b> 선택</li>
<li>μΈμ¦μ„œ 이름 μž…λ ₯ ν›„, <b>Certificate Key File</b> (.key) 및 <b>Certificate File</b> (.crt/pem) μ—…λ‘œλ“œ ν›„ μ €μž₯</li>
</ol>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-body">
<h4 data-i18n="npm.step3_title">Proxy Host μΆ”κ°€ (CertBridge API λ“±)</h4>
<p data-i18n="npm.step3_desc">NPM ν”„λ‘μ‹œ 호슀트λ₯Ό μƒμ„±ν•˜μ—¬ μ™ΈλΆ€ HTTPS μš”μ²­μ„ λ‚΄λΆ€ μ»¨ν…Œμ΄λ„ˆλ‘œ μ „λ‹¬ν•©λ‹ˆλ‹€.</p>
<ul>
<li><b>Domain Names:</b> <code>certbridge.local</code> (λ˜λŠ” κΈ°κ΄€μš© 도메인)</li>
<li><b>Scheme:</b> <code>http</code></li>
<li><b>Forward Hostname / IP:</b> <code>certbridge-api</code> (Docker λΈŒλ¦Ώμ§€ μ»¨ν…Œμ΄λ„ˆ 호슀트λͺ…)</li>
<li><b>Forward Port:</b> <code>8090</code></li>
<li><b>Block Common Exploits:</b> ν™œμ„±ν™”</li>
<li><b>SSL νƒ­:</b> μœ„μ—μ„œ λ“±λ‘ν•œ Custom SSL 선택 및 <b>Force SSL</b> ν™œμ„±ν™”</li>
</ul>
</div>
</div>
</section>
<!-- 5. Authentik Setup -->
<section class="section" id="authentik-setup">
<h2 data-i18n="authentik.title">5. Authentik SSO / MFA μ„€μ • ν”„λ‘œν† μ½œ</h2>
<p data-i18n="authentik.desc">Authentik은 μ‹±κΈ€ μ‚¬μΈμ˜¨(SSO)κ³Ό λ©€ν‹°νŒ©ν„° 인증(MFA)을 μΌκ΄€λ˜κ²Œ μ œκ³΅ν•˜μ—¬, API, MinIO Console, Neo4j Graph DB둜의 무단 μΉ¨μž…μ„ ν†΅μ œν•©λ‹ˆλ‹€.</p>
<div class="step">
<div class="step-num">1</div>
<div class="step-body">
<h4 data-i18n="authentik.step1_title">졜초 μΈμŠ€ν„΄μŠ€ μ…‹μ—… 및 μ΄ˆκΈ°ν™”</h4>
<p data-i18n="authentik.step1_desc">μ›Ή λΈŒλΌμš°μ €λ‘œ <code>http://<SERVER_IP>:9000/if/flow/initial-setup/</code> 에 μ ‘μ†ν•©λ‹ˆλ‹€. 졜초 λΆ€νŠΈμŠ€νŠΈλž© ν™”λ©΄μ—μ„œ κ΄€λ¦¬μž 계정 <code>akadmin</code>에 λŒ€ν•œ μ•ˆμ „ν•œ λΉ„λ°€λ²ˆν˜Έλ₯Ό μ„€μ •ν•˜μ—¬ μ΄ˆκΈ°ν™”λ₯Ό μ™„λ£Œν•©λ‹ˆλ‹€.</p>
</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-body">
<h4 data-i18n="authentik.step2_title">Proxy Provider 생성 (NPM Forward Auth μ—°λ™μš©)</h4>
<p data-i18n="authentik.step2_desc">MinIO와 Neo4j λ“± κ°œλ³„ λ³΄μ•ˆμ΄ ν•„μš”ν•œ λ ˆκ±°μ‹œ ν™”λ©΄ 보호λ₯Ό μœ„ν•΄ Proxy Providerλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.</p>
<ol>
<li>Authentik Admin Interface μ§„μž… (우츑 상단 <b>"Admin interface"</b>)</li>
<li>쒌츑 메뉴 <b>Applications</b> &gt; <b>Providers</b> &gt; <b>"Create"</b> 클릭</li>
<li><b>Proxy Provider</b> 선택 ν›„ μ„€μ •:
<ul>
<li><b>Name:</b> <code>infra-proxy-provider</code></li>
<li><b>Authorization flow:</b> κΈ°λ³Έ flow 선택 (e.g. default-authorization-flow)</li>
<li><b>External host:</b> <code>http://minio.certbridge.local</code> (μ™ΈλΆ€ λ…ΈμΆœλ  URL)</li>
<li><b>Mode:</b> <code>Forward auth (single application)</code> 선택</li>
</ul>
</li>
</ol>
</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-body">
<h4 data-i18n="authentik.step3_title">NPM Custom Nginx Configuration 적용</h4>
<p data-i18n="authentik.step3_desc">Nginx Proxy Managerμ—μ„œ ν•΄λ‹Ή Proxy Host둜 λ“€μ–΄μ˜€λŠ” νŠΈλž˜ν”½μ΄ Authentik을 거쳐 μΈμ¦λ˜λ„λ‘ Custom Nginx μ„€μ • 블둝을 λ„£μ–΄μ€λ‹ˆλ‹€.</p>
<div class="code-wrap">
<div class="code-header"><span>NPM Advanced Config (Custom Nginx Configuration)</span></div>
<pre># Authentik Forward Auth Integration
location / {
# Authentik Server IP / Container Port
auth_request /outpost.goauthentik.io/auth/request;
error_page 401 = @goauthentik_login;
# Pass the actual request back to Authentik
proxy_pass $forward_scheme://$server:$port;
# Auth variables sync
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
# Headers
auth_request_set $authentik_username $upstream_http_x_authentik_username;
auth_request_set $authentik_groups $upstream_http_x_authentik_groups;
auth_request_set $authentik_email $upstream_http_x_authentik_email;
auth_request_set $authentik_name $upstream_http_x_authentik_name;
proxy_set_header X-Authentik-Username $authentik_username;
proxy_set_header X-Authentik-Groups $authentik_groups;
proxy_set_header X-Authentik-Email $authentik_email;
proxy_set_header X-Authentik-Name $authentik_name;
}
location = /outpost.goauthentik.io/auth/request {
proxy_pass http://certbridge-authentik-server:9000/outpost.goauthentik.io/auth/request;
proxy_set_header Host $host;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Content-Length "";
proxy_method GET;
}
location @goauthentik_login {
return 302 http://certbridge-authentik-server:9000/outpost.goauthentik.io/auth/start?rd=$scheme://$http_host$request_uri;
}</pre>
</div>
</div>
</div>
</section>
<!-- 6. Troubleshooting -->
<section class="section" id="troubleshooting">
<h2 data-i18n="trouble.title">6. 문제 ν•΄κ²° (Troubleshooting)</h2>
<div class="card warn">
<h4>1. DB Connection Refused (postgres/redis)</h4>
<p>Authentik μ»¨ν…Œμ΄λ„ˆκ°€ μ‹œμž‘ν•  λ•Œ PostgreSQL λ˜λŠ” Redis에 μ ‘μ†ν•˜μ§€ λͺ»ν•΄ ν¬λž˜μ‹œλ˜λŠ” 경우:</p>
<p><b>원인:</b> postgresλ‚˜ redisκ°€ 아직 κΈ°λ™λ˜μ§€ μ•Šμ•˜κ±°λ‚˜ ν—¬μŠ€μ²΄ν¬ 검증 단계에 μ‹œκ°„μ΄ ν•„μš”ν•œ κ²½μš°μž…λ‹ˆλ‹€.</p>
<p><b>ν•΄κ²°μ±…:</b> <code>docker compose logs -f certbridge-db</code> λͺ…λ ΉμœΌλ‘œ DB μ„œλ²„ μ‹œμž‘ 둜그λ₯Ό ν™•μΈν•˜κ³ , <code>docker compose restart certbridge-authentik-server</code>λ₯Ό 톡해 Authentik을 μž¬μ‹œμž‘ν•΄ μ£Όμ‹­μ‹œμ˜€.</p>
</div>
<div class="card warn">
<h4>2. Authentik DB init failure (Postgres)</h4>
<p>Authentik DBκ°€ μžλ™μœΌλ‘œ μƒμ„±λ˜μ§€ μ•Šμ•„ 인증 μ‹œμŠ€ν…œμ΄ κ΅¬λ™λ˜μ§€ μ•ŠλŠ” 경우:</p>
<p><b>원인:</b> docker-compose.yml의 λ³Όλ₯¨μ— <code>00-init-authentik-db.sql</code> 이 μ œλŒ€λ‘œ λ°”μΈλ”©λ˜μ§€ μ•Šμ•„ <code>authentik</code> μ΄λΌλŠ” λ°μ΄ν„°λ² μ΄μŠ€κ°€ Postgres 내에 λˆ„λ½λœ κ²½μš°μž…λ‹ˆλ‹€.</p>
<p><b>ν•΄κ²°μ±…:</b> <code>docker compose exec -it certbridge-db psql -U certbridge -c "CREATE DATABASE authentik;"</code> 을 터미널에 직접 μˆ˜ν–‰ν•˜μ—¬ DBλ₯Ό μˆ˜λ™ μƒμ„±ν•˜κ³  Authentik을 μž¬κΈ°λ™ν•˜μ‹­μ‹œμ˜€.</p>
</div>
</section>
</div>
</div>
<script>
/* ─────────────────────────────────────────
i18n Dictionary
───────────────────────────────────────── */
const i18n = {
ko: {
"sidebar.ver": "λ³΄μ•ˆ 및 에어갭 κ°€μ΄λ“œ v0.1.0",
"nav.overview": "μ•„ν‚€ν…μ²˜ κ°œμš”",
"nav.prep": "에어갭 νŒ¨ν‚€μ§€ μ€€λΉ„",
"nav.install": "μ˜€ν”„λΌμΈ μ„€μΉ˜ ν”„λ‘œν† μ½œ",
"nav.npm": "Nginx Proxy Manager",
"nav.authentik": "Authentik SSO 연동",
"nav.trouble": "문제 ν•΄κ²°",
"hero.title": "CertBridge λ³΄μ•ˆ κ°•ν™” 및 에어갭 κ°€μ΄λ“œ",
"hero.desc": "μ˜€ν”„λΌμΈ(Air-Gap) μš°λΆ„νˆ¬ ν™˜κ²½μ—μ„œ Nginx Proxy Manager와 Authentik SSOλ₯Ό ν™œμš©ν•˜μ—¬ SSL λ³΄μ•ˆ ν„°λ―Έλ„€μ΄μ…˜ 및 계정 톡합 인증 μ‹œμŠ€ν…œμ„ μ•ˆμ „ν•˜κ²Œ κ΅¬μ„±ν•˜λŠ” κ³ κΈ‰ κ°€μ΄λ“œμž…λ‹ˆλ‹€.",
"hero.badge": "πŸ”’ v0.1.0 Β· Ubuntu amd64 Β· NPM & Authentik",
"overview.title": "1. λ³΄μ•ˆ κ°•ν™” μ•„ν‚€ν…μ²˜ κ°œμš”",
"overview.desc1": "κΈ°μ‘΄ 쀑앙 μ„œλ²„ μΈν”„λΌλŠ” 포트 λ…ΈμΆœ 및 무인증 ν†΅μ‹ μœΌλ‘œ 인해 λ³΄μ•ˆ μœ„ν˜‘μ— μ·¨μ•½ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ³Έ κ°œμ • μŠ€νƒμ—μ„œλŠ” Nginx Proxy Manager (NPM)와 Authentik SSOλ₯Ό 쀑앙 인프라에 ν†΅ν•©ν•˜μ—¬ μ „λ°© λ³΄μ•ˆ μž₯벽을 κ΅¬μΆ•ν•©λ‹ˆλ‹€.",
"overview.strategy_title": "πŸ’‘ μžμ› μ΅œμ ν™” 섀계 μ „λž΅ (Postgres/Redis 곡유)",
"overview.strategy_desc": "μƒˆλ‘œ μΆ”κ°€λ˜λŠ” SSO(Authentik) μ„œλΉ„μŠ€μ˜ μ˜€λ²„ν—€λ“œλ₯Ό μ œμ–΄ν•˜κΈ° μœ„ν•΄, κΈ°μ‘΄ PostgreSQL 및 Redis μ»¨ν…Œμ΄λ„ˆ μžμ›μ„ κ³ μŠ€λž€νžˆ μž¬ν™œμš©ν•©λ‹ˆλ‹€.",
"overview.net_title": "🌐 μ„œλΉ„μŠ€ 포트 λ§΅ & λ„€νŠΈμ›Œν¬ ν† ν΄λ‘œμ§€",
"prep.title": "2. 에어갭(μ˜€ν”„λΌμΈ) νŒ¨ν‚€μ§€ μ€€λΉ„",
"prep.desc": "인터넷 연결이 μ „ν˜€ λΆˆκ°€λŠ₯ν•œ 에어갭(Air-Gap) 폐쇄망 μš°λΆ„νˆ¬ μ„œλ²„μ—μ„œ μ„€μΉ˜ν•˜κΈ° μœ„ν•΄μ„œλŠ”, 인터넷이 μ—°κ²°λœ 사전 λ¨Έμ‹ μ—μ„œ ν•„μš”ν•œ λͺ¨λ“  Docker 이미지와 νŒ¨ν‚€μ§€λ₯Ό λ‹€μš΄λ‘œλ“œν•΄μ•Ό ν•©λ‹ˆλ‹€.",
"prep.step1": "2-1. Hugging Faceμ—μ„œ 에어갭 파일 λ‹€μš΄λ‘œλ“œ",
"prep.desc2": "μš°λ¦¬λŠ” Hugging Face μ €μž₯μ†Œ (VanyaJ/certbridge)에 미리 λΉŒλ“œλœ linux/amd64 ν”Œλž«νΌ μ „μš© Docker 이미지 μ•„μΉ΄μ΄λΈŒμ™€ μ„œλ²„ νŒ¨ν‚€μ§€λ₯Ό 배포해 λ‘μ—ˆμŠ΅λ‹ˆλ‹€.",
"prep.step2": "2-2. (λŒ€μ•ˆ) prepare-airgap-package.sh ν™œμš©",
"prep.desc3": "인터넷이 μ—°κ²°λœ μ™ΈλΆ€ μš°λΆ„νˆ¬ PCμ—μ„œ 직접 이 슀크립트λ₯Ό μˆ˜ν–‰ν•˜λ©΄, Docker 이미지 및 Node/Docker deb μ„€μΉ˜ νŒ¨ν‚€μ§€κΉŒμ§€ ν•˜λ‚˜μ˜ λ²ˆλ“€λ‘œ λ¬Άμ–΄ μ••μΆ•ν•΄ μ€λ‹ˆλ‹€.",
"install.title": "3. μ˜€ν”„λΌμΈ μ„€μΉ˜ ν”„λ‘œν† μ½œ (Ubuntu)",
"install.desc": "λ‹€μš΄λ‘œλ“œν•œ νŒ¨ν‚€μ§€μ™€ docker-images 폴더λ₯Ό 이동식 μ €μž₯μž₯치(USB/μ™Έμž₯ν•˜λ“œ λ“±) λ˜λŠ” 내뢀망 파일 곡유λ₯Ό 톡해 에어갭 λŒ€μƒ μš°λΆ„νˆ¬ μ„œλ²„λ‘œ λ³΅μ‚¬ν•©λ‹ˆλ‹€.",
"install.step1_title": "νŒ¨ν‚€μ§€ μ••μΆ• ν•΄μ œ 및 이미지 디렉토리 병합",
"install.step1_desc": "μ„œλ²„ νŒ¨ν‚€μ§€λ₯Ό ν•΄μ œν•œ 경둜 μ•„λž˜λ‘œ, κ°œλ³„ λ‹€μš΄λ‘œλ“œν•œ docker-images 디렉토리λ₯Ό λ³‘ν•©ν•©λ‹ˆλ‹€.",
"install.step2_title": "μžλ™ μ…‹μ—… 슀크립트 μ‹€ν–‰",
"install.step2_desc": "μ˜€ν”„λΌμΈ μ„€μΉ˜μš© μ…‹μ—… 슀크립트 setup-airgap.sh에 μ‹€ν–‰ κΆŒν•œμ„ λΆ€μ—¬ν•˜κ³  κ°€λ™ν•©λ‹ˆλ‹€. 이 μŠ€ν¬λ¦½νŠΈλŠ” Docker load λͺ…령을 톡해 μ˜€ν”„λΌμΈ tar 및 tar.gz 이미지λ₯Ό 전체 λ³΅κ΅¬ν•˜κ³  인프라 μ„œλΉ„μŠ€λ₯Ό κΈ°λ™ν•©λ‹ˆλ‹€.",
"install.step3_title": "독립 λͺ¨λ“œ(--profile standalone) μ„œλΉ„μŠ€ 기동 μƒνƒœ 쑰회",
"install.step3_desc": "NPM 및 Authentik μ„œλΉ„μŠ€λ₯Ό ν¬ν•¨ν•˜μ—¬ 정상 ꡬ동 쀑인지 μ•„λž˜ λͺ…λ ΉμœΌλ‘œ ν™•μΈν•©λ‹ˆλ‹€.",
"npm.title": "4. Nginx Proxy Manager μ„€μ • κ°€μ΄λ“œ",
"npm.desc": "Nginx Proxy Manager(NPM)λŠ” CertBridge μŠ€νƒμ˜ μ΅œμ „λ°©μ—μ„œ λͺ¨λ“  μ™ΈλΆ€ μœ μž… μš”μ²­μ„ μ²˜λ¦¬ν•˜κ³ , SSL μ•”ν˜Έν™” 톡신을 μ’…λ‹¨ν•˜λ©°, 도메인/경둜 λΆ„κΈ°λ₯Ό μˆ˜ν–‰ν•©λ‹ˆλ‹€.",
"npm.step1_title": "κ΄€λ¦¬μž ν™”λ©΄(Admin UI) 둜그인",
"npm.step1_desc": "μ›Ή λΈŒλΌμš°μ €λ‘œ http://<SERVER_IP>:81 에 μ ‘μ†ν•©λ‹ˆλ‹€. 졜초 κΈ°λ³Έ 둜그인 계정은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.",
"npm.step2_title": "사섀 SSL/TLS μΈμ¦μ„œ μΆ”κ°€ (에어갭 ν™˜κ²½)",
"npm.step2_desc": "인프라망이 에어갭 μ˜€ν”„λΌμΈμ΄λ―€λ‘œ Let's Encrypt μžλ™ λ°œκΈ‰μ„ μ‚¬μš©ν•  수 μ—†μŠ΅λ‹ˆλ‹€. κΈ°κ΄€ λ‚΄λΆ€ CA λ˜λŠ” μ‚¬μ„€λ‘œ λ°œκΈ‰λ°›μ€ μΈμ¦μ„œ νŒŒμΌμ„ μˆ˜λ™ 등둝해야 ν•©λ‹ˆλ‹€.",
"npm.step3_title": "Proxy Host μΆ”κ°€ (CertBridge API λ“±)",
"npm.step3_desc": "NPM ν”„λ‘μ‹œ 호슀트λ₯Ό μƒμ„±ν•˜μ—¬ μ™ΈλΆ€ HTTPS μš”μ²­μ„ λ‚΄λΆ€ μ»¨ν…Œμ΄λ„ˆλ‘œ μ „λ‹¬ν•©λ‹ˆλ‹€.",
"authentik.title": "5. Authentik SSO / MFA μ„€μ • ν”„λ‘œν† μ½œ",
"authentik.desc": "Authentik은 μ‹±κΈ€ μ‚¬μΈμ˜¨(SSO)κ³Ό λ©€ν‹°νŒ©ν„° 인증(MFA)을 μΌκ΄€λ˜κ²Œ μ œκ³΅ν•˜μ—¬, API, MinIO Console, Neo4j Graph DB둜의 무단 μΉ¨μž…μ„ ν†΅μ œν•©λ‹ˆλ‹€.",
"authentik.step1_title": "졜초 μΈμŠ€ν„΄μŠ€ μ…‹μ—… 및 μ΄ˆκΈ°ν™”",
"authentik.step1_desc": "μ›Ή λΈŒλΌμš°μ €λ‘œ http://<SERVER_IP>:9000/if/flow/initial-setup/ 에 μ ‘μ†ν•©λ‹ˆλ‹€. 졜초 λΆ€νŠΈμŠ€νŠΈλž© ν™”λ©΄μ—μ„œ κ΄€λ¦¬μž 계정 akadmin에 λŒ€ν•œ μ•ˆμ „ν•œ λΉ„λ°€λ²ˆν˜Έλ₯Ό μ„€μ •ν•˜μ—¬ μ΄ˆκΈ°ν™”λ₯Ό μ™„λ£Œν•©λ‹ˆλ‹€.",
"authentik.step2_title": "Proxy Provider 생성 (NPM Forward Auth μ—°λ™μš©)",
"authentik.step2_desc": "MinIO와 Neo4j λ“± κ°œλ³„ λ³΄μ•ˆμ΄ ν•„μš”ν•œ λ ˆκ±°μ‹œ ν™”λ©΄ 보호λ₯Ό μœ„ν•΄ Proxy Providerλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.",
"authentik.step3_title": "NPM Custom Nginx Configuration 적용",
"authentik.step3_desc": "Nginx Proxy Managerμ—μ„œ ν•΄λ‹Ή Proxy Host둜 λ“€μ–΄μ˜€λŠ” νŠΈλž˜ν”½μ΄ Authentik을 거쳐 μΈμ¦λ˜λ„λ‘ Custom Nginx μ„€μ • 블둝을 λ„£μ–΄μ€λ‹ˆλ‹€.",
"trouble.title": "6. 문제 ν•΄κ²° (Troubleshooting)"
},
en: {
"sidebar.ver": "Security & Air-gap Guide v0.1.0",
"nav.overview": "Architecture Overview",
"nav.prep": "Air-gap Package Prep",
"nav.install": "Offline Install Protocol",
"nav.npm": "Nginx Proxy Manager",
"nav.authentik": "Authentik SSO Setup",
"nav.trouble": "Troubleshooting",
"hero.title": "CertBridge Security & Air-gap Guide",
"hero.desc": "An advanced guide on securely configuring SSL termination and Single Sign-On (SSO) with MFA using Nginx Proxy Manager and Authentik in an offline (Air-Gap) Ubuntu environment.",
"hero.badge": "πŸ”’ v0.1.0 Β· Ubuntu amd64 Β· NPM & Authentik",
"overview.title": "1. Security-Hardened Architecture Overview",
"overview.desc1": "The existing core server infrastructure could be vulnerable due to exposed ports and unauthenticated communication. In this revised stack, Nginx Proxy Manager (NPM) and Authentik SSO are integrated to establish a robust front-line security barrier.",
"overview.strategy_title": "πŸ’‘ Resource Optimization Strategy (Shared Postgres/Redis)",
"overview.strategy_desc": "To control the overhead of the newly added SSO (Authentik) services, existing PostgreSQL and Redis container resources are fully reused.",
"overview.net_title": "🌐 Port Mapping & Network Topology",
"prep.title": "2. Offline Air-gap Package Preparation",
"prep.desc": "To deploy on an air-gapped Ubuntu server without internet access, you must first pre-download all required Docker images and installer packages on an internet-enabled machine.",
"prep.step1": "2-1. Downloading Air-gap Assets from Hugging Face",
"prep.desc2": "We have pre-packaged and published the linux/amd64 compatible Docker image archives and server deployment bundles in our Hugging Face repository (VanyaJ/certbridge).",
"prep.step2": "2-2. (Alternative) Using prepare-airgap-package.sh",
"prep.desc3": "Running this script on an internet-enabled Ubuntu PC will pull and package all Docker images, along with Node and Docker deb setup files, into a single archive.",
"install.title": "3. Offline Ubuntu Installation Protocol",
"install.desc": "Copy the downloaded packages and docker-images directory to your air-gapped target Ubuntu server using a portable USB storage or local network file sharing.",
"install.step1_title": "Extract Packages and Merge Image Directories",
"install.step1_desc": "Extract the server package and merge the separately downloaded docker-images directory into it.",
"install.step2_title": "Execute Automatic Setup Script",
"install.step2_desc": "Grant execution permissions to setup-airgap.sh and run it. The script loads offline tar and tar.gz images and fires up all system services.",
"install.step3_title": "Inspect Standalone Mode Services Status",
"install.step3_desc": "Verify that all services, including NPM and Authentik, are successfully running with the command below.",
"npm.title": "4. Nginx Proxy Manager Configuration Guide",
"npm.desc": "Nginx Proxy Manager (NPM) sits at the front line, routing all incoming external traffic, terminating SSL, and managing domain name resolutions.",
"npm.step1_title": "Login to NPM Admin UI",
"npm.step1_desc": "Open your web browser and navigate to http://<SERVER_IP>:81. The default bootstrap credentials are:",
"npm.step2_title": "Add Private SSL/TLS Certificate (Air-gap)",
"npm.step2_desc": "Since Let's Encrypt cannot auto-issue certificates in an offline environment, you must manually upload a private CA-signed certificate.",
"npm.step3_title": "Add Proxy Host (for CertBridge API etc.)",
"npm.step3_desc": "Create a new proxy host in NPM to forward external HTTPS requests to internal docker containers safely.",
"authentik.title": "5. Authentik SSO / MFA Federation Protocol",
"authentik.desc": "Authentik provides centralized SSO and MFA, preventing unauthorized entry to the API, MinIO Console, and Neo4j Graph DB.",
"authentik.step1_title": "Initial Setup and Admin Initialization",
"authentik.step1_desc": "Navigate to http://<SERVER_IP>:9000/if/flow/initial-setup/ on your browser. Complete initialization by defining a secure password for the default admin account akadmin.",
"authentik.step2_title": "Create Proxy Provider (for NPM Forward Auth)",
"authentik.step2_desc": "Create a Proxy Provider in Authentik to protect web applications like MinIO and Neo4j.",
"authentik.step3_title": "Apply Custom Nginx Configuration in NPM",
"authentik.step3_desc": "Insert custom Nginx configurations into NPM proxy hosts so that all requests route through Authentik authentication filters.",
"trouble.title": "6. Troubleshooting Guide"
}
};
/* ─────────────────────────────────────────
Language Changer
───────────────────────────────────────── */
function changeLanguage(lang) {
document.querySelectorAll("[data-i18n]").forEach(el => {
const key = el.getAttribute("data-i18n");
if (i18n[lang] && i18n[lang][key]) {
el.innerHTML = i18n[lang][key];
}
});
document.getElementById("html-title").innerText = i18n[lang]["hero.title"] || "CertBridge Security Guide";
}
/* ─────────────────────────────────────────
Copy to Clipboard
───────────────────────────────────────── */
function copyCode(btn) {
const pre = btn.closest('.code-wrap').querySelector('pre');
const text = pre.innerText;
navigator.clipboard.writeText(text).then(() => {
const originalText = btn.innerText;
btn.innerText = "볡사됨!";
btn.style.background = "var(--accent-emerald)";
btn.style.borderColor = "var(--accent-emerald)";
btn.style.color = "white";
setTimeout(() => {
btn.innerText = originalText;
btn.style.background = "var(--bg-secondary)";
btn.style.borderColor = "var(--border-subtle)";
btn.style.color = "var(--text-secondary)";
}, 2000);
});
}
// Sidebar Navigation Highlighting
const sections = document.querySelectorAll(".section");
const navLinks = document.querySelectorAll(".sidebar nav a");
window.addEventListener("scroll", () => {
let current = "";
sections.forEach(section => {
const sectionTop = section.offsetTop;
if (pageYOffset >= sectionTop - 60) {
current = section.getAttribute("id");
}
});
navLinks.forEach(link => {
link.classList.remove("active");
if (link.getAttribute("href").substring(1) === current) {
link.classList.add("active");
}
});
});
</script>
</body>
</html>