VanyaJ commited on
Commit
fed0bec
Β·
verified Β·
1 Parent(s): 6603c07

Add premium security setup guide html for Nginx Proxy Manager and Authentik

Browse files
Files changed (1) hide show
  1. security-setup-guide.html +1022 -0
security-setup-guide.html ADDED
@@ -0,0 +1,1022 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ko" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title id="html-title">CertBridge λ³΄μ•ˆ κ°•ν™” 및 에어갭 μ„€μ • κ°€μ΄λ“œ</title>
7
+ <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">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ /* ─────────────────────────────────────────
11
+ CertBridge Security Guide Design System
12
+ Dual Theme Sync (Light + Dark)
13
+ Premium Modern Style
14
+ ───────────────────────────────────────── */
15
+ * {
16
+ margin: 0;
17
+ padding: 0;
18
+ box-sizing: border-box;
19
+ transition: background-color 0.25s ease, border-color 0.25s ease, color 0.25s ease;
20
+ }
21
+
22
+ :root {
23
+ /* Light Theme (Default) */
24
+ --bg-primary: #ffffff;
25
+ --bg-secondary: #f8fafc;
26
+ --bg-card: #ffffff;
27
+ --bg-glass: rgba(0, 0, 0, 0.015);
28
+ --bg-hover: rgba(0, 0, 0, 0.025);
29
+
30
+ --text-primary: #1e293b;
31
+ --text-secondary: #475569;
32
+ --text-muted: #94a3b8;
33
+
34
+ --accent-blue: #2563eb;
35
+ --accent-blue-hover: #1d4ed8;
36
+ --accent-blue-subtle: rgba(37, 99, 235, 0.08);
37
+ --accent-emerald: #059669;
38
+ --accent-amber: #d97706;
39
+ --accent-red: #dc2626;
40
+ --accent-purple: #7c3aed;
41
+
42
+ --border-subtle: rgba(0, 0, 0, 0.08);
43
+ --border-active: rgba(37, 99, 235, 0.4);
44
+
45
+ --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.03);
46
+ --shadow-card-hover: 0 2px 8px rgba(0, 0, 0, 0.06);
47
+ --shadow-glow: none;
48
+
49
+ --grad: linear-gradient(135deg, #2563eb 0%, #0284c7 100%);
50
+ --grad-hero: linear-gradient(135deg, rgba(37, 99, 235, 0.05) 0%, rgba(2, 132, 199, 0.05) 100%);
51
+ --hero-text: #1e293b;
52
+ --hero-desc: #475569;
53
+ }
54
+
55
+ [data-theme='dark'] {
56
+ /* Dark Theme */
57
+ --bg-primary: #0f1117;
58
+ --bg-secondary: #1a1d27;
59
+ --bg-card: #1e2130;
60
+ --bg-glass: rgba(255, 255, 255, 0.04);
61
+ --bg-hover: rgba(255, 255, 255, 0.05);
62
+
63
+ --text-primary: #e8eaed;
64
+ --text-secondary: #9aa0b0;
65
+ --text-muted: #5f6577;
66
+
67
+ --accent-blue: #3b82f6;
68
+ --accent-blue-hover: #2563eb;
69
+ --accent-blue-subtle: rgba(59, 130, 246, 0.12);
70
+ --accent-emerald: #10b981;
71
+ --accent-amber: #f59e0b;
72
+ --accent-red: #ef4444;
73
+ --accent-purple: #8b5cf6;
74
+
75
+ --border-subtle: rgba(255, 255, 255, 0.07);
76
+ --border-active: rgba(59, 130, 246, 0.5);
77
+
78
+ --shadow-card: 0 1px 4px rgba(0, 0, 0, 0.2);
79
+ --shadow-card-hover: 0 4px 16px rgba(0, 0, 0, 0.3);
80
+ --shadow-glow: 0 0 20px rgba(59, 130, 246, 0.1);
81
+
82
+ --grad: linear-gradient(135deg, #3b82f6 0%, #0ea5e9 100%);
83
+ --grad-hero: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(14, 165, 233, 0.08) 100%);
84
+ --hero-text: #e8eaed;
85
+ --hero-desc: #9aa0b0;
86
+ }
87
+
88
+ body {
89
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
90
+ background-color: var(--bg-primary);
91
+ color: var(--text-primary);
92
+ line-height: 1.6;
93
+ display: flex;
94
+ min-height: 100vh;
95
+ }
96
+
97
+ ::selection {
98
+ background: var(--accent-blue-subtle);
99
+ color: var(--accent-blue);
100
+ }
101
+
102
+ a {
103
+ color: var(--accent-blue);
104
+ text-decoration: none;
105
+ font-weight: 500;
106
+ }
107
+ a:hover {
108
+ text-decoration: underline;
109
+ }
110
+
111
+ /* Sidebar Styling */
112
+ .sidebar {
113
+ position: fixed;
114
+ top: 0;
115
+ left: 0;
116
+ width: 280px;
117
+ height: 100vh;
118
+ background: var(--bg-secondary);
119
+ border-right: 1px solid var(--border-subtle);
120
+ padding: 24px 0;
121
+ overflow-y: auto;
122
+ z-index: 100;
123
+ }
124
+
125
+ .sidebar-logo {
126
+ padding: 0 24px 20px;
127
+ border-bottom: 1px solid var(--border-subtle);
128
+ margin-bottom: 16px;
129
+ }
130
+
131
+ .sidebar-logo h1 {
132
+ font-size: 16px;
133
+ font-weight: 700;
134
+ color: var(--accent-blue);
135
+ letter-spacing: -0.3px;
136
+ font-family: 'Outfit', sans-serif;
137
+ }
138
+
139
+ .sidebar-logo p {
140
+ font-size: 11px;
141
+ color: var(--text-muted);
142
+ margin-top: 4px;
143
+ }
144
+
145
+ .sidebar nav a {
146
+ display: flex;
147
+ align-items: center;
148
+ padding: 10px 24px;
149
+ font-size: 13px;
150
+ color: var(--text-secondary);
151
+ transition: all 0.2s;
152
+ border-left: 3px solid transparent;
153
+ text-decoration: none;
154
+ }
155
+
156
+ .sidebar nav a:hover, .sidebar nav a.active {
157
+ color: var(--accent-blue);
158
+ background: var(--accent-blue-subtle);
159
+ border-left-color: var(--accent-blue);
160
+ }
161
+
162
+ .sidebar nav a .num {
163
+ display: inline-flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ width: 20px;
167
+ height: 20px;
168
+ border-radius: 4px;
169
+ background: var(--bg-glass);
170
+ font-size: 10px;
171
+ font-weight: 600;
172
+ margin-right: 10px;
173
+ color: var(--text-muted);
174
+ }
175
+
176
+ .sidebar nav a.active .num {
177
+ background: var(--accent-blue);
178
+ color: white;
179
+ }
180
+
181
+ /* Main Area */
182
+ .main {
183
+ margin-left: 280px;
184
+ flex: 1;
185
+ padding: 0;
186
+ overflow-y: auto;
187
+ height: 100vh;
188
+ }
189
+
190
+ /* Embedded in Iframe */
191
+ body.in-iframe .sidebar {
192
+ display: none !important;
193
+ }
194
+
195
+ body.in-iframe .main {
196
+ margin-left: 0 !important;
197
+ width: 100% !important;
198
+ height: 100% !important;
199
+ }
200
+
201
+ body.in-iframe .content {
202
+ padding: 32px 24px 60px;
203
+ max-width: 100%;
204
+ }
205
+
206
+ /* Hero Header */
207
+ .hero {
208
+ background: var(--grad-hero);
209
+ padding: 48px 40px;
210
+ border-bottom: 1px solid var(--border-subtle);
211
+ position: relative;
212
+ overflow: hidden;
213
+ }
214
+
215
+ .hero h1 {
216
+ font-size: 26px;
217
+ font-weight: 700;
218
+ color: var(--hero-text);
219
+ font-family: 'Outfit', sans-serif;
220
+ letter-spacing: -0.5px;
221
+ }
222
+
223
+ .hero p {
224
+ font-size: 13.5px;
225
+ color: var(--hero-desc);
226
+ margin-top: 8px;
227
+ max-width: 800px;
228
+ }
229
+
230
+ .hero .badge {
231
+ display: inline-block;
232
+ padding: 4px 12px;
233
+ background: var(--bg-glass);
234
+ border: 1px solid var(--border-subtle);
235
+ border-radius: 20px;
236
+ font-size: 11px;
237
+ font-weight: 600;
238
+ color: var(--text-secondary);
239
+ margin-top: 12px;
240
+ }
241
+
242
+ /* Content Container */
243
+ .content {
244
+ padding: 48px;
245
+ max-width: 1000px;
246
+ margin: 0 auto;
247
+ }
248
+
249
+ .section {
250
+ margin-bottom: 48px;
251
+ scroll-margin-top: 40px;
252
+ }
253
+
254
+ h2 {
255
+ font-size: 18px;
256
+ font-weight: 700;
257
+ margin-bottom: 16px;
258
+ position: relative;
259
+ padding-left: 12px;
260
+ color: var(--text-primary);
261
+ font-family: 'Outfit', sans-serif;
262
+ }
263
+
264
+ h2::before {
265
+ content: '';
266
+ position: absolute;
267
+ left: 0;
268
+ top: 3px;
269
+ bottom: 3px;
270
+ width: 3px;
271
+ border-radius: 1.5px;
272
+ background: var(--accent-blue);
273
+ }
274
+
275
+ h3 {
276
+ font-size: 14px;
277
+ font-weight: 600;
278
+ margin: 24px 0 12px;
279
+ color: var(--accent-blue);
280
+ }
281
+
282
+ h4 {
283
+ font-size: 13px;
284
+ font-weight: 600;
285
+ margin: 16px 0 8px;
286
+ color: var(--text-primary);
287
+ }
288
+
289
+ p, li {
290
+ margin-bottom: 8px;
291
+ color: var(--text-secondary);
292
+ font-size: 13px;
293
+ line-height: 1.6;
294
+ }
295
+
296
+ ul, ol {
297
+ margin: 0 0 16px 20px;
298
+ }
299
+
300
+ /* Cards & Layout */
301
+ .card {
302
+ background: var(--bg-secondary);
303
+ border: 1px solid var(--border-subtle);
304
+ border-radius: 12px;
305
+ padding: 20px;
306
+ margin: 16px 0;
307
+ box-shadow: var(--shadow-card);
308
+ }
309
+
310
+ .card.tip, .card.info {
311
+ border-color: rgba(37, 99, 235, 0.2);
312
+ background: var(--accent-blue-subtle);
313
+ }
314
+
315
+ .card.tip h4, .card.info h4 {
316
+ color: var(--accent-blue);
317
+ margin-top: 0;
318
+ }
319
+
320
+ .card.warn {
321
+ border-color: rgba(220, 38, 38, 0.2);
322
+ background: rgba(220, 38, 38, 0.03);
323
+ }
324
+
325
+ .card.warn h4 {
326
+ color: var(--accent-red);
327
+ margin-top: 0;
328
+ }
329
+
330
+ /* Table */
331
+ table {
332
+ width: 100%;
333
+ border-collapse: collapse;
334
+ margin: 16px 0;
335
+ border-radius: 8px;
336
+ overflow: hidden;
337
+ border: 1px solid var(--border-subtle);
338
+ }
339
+
340
+ th {
341
+ background: var(--bg-glass);
342
+ padding: 10px 14px;
343
+ text-align: left;
344
+ font-size: 12px;
345
+ font-weight: 600;
346
+ color: var(--text-primary);
347
+ border-bottom: 1px solid var(--border-subtle);
348
+ }
349
+
350
+ td {
351
+ padding: 10px 14px;
352
+ font-size: 12px;
353
+ border-top: 1px solid var(--border-subtle);
354
+ color: var(--text-secondary);
355
+ background: var(--bg-secondary);
356
+ }
357
+
358
+ tr:hover td {
359
+ background: var(--bg-hover);
360
+ }
361
+
362
+ .req-grid {
363
+ display: grid;
364
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
365
+ gap: 16px;
366
+ margin: 16px 0;
367
+ }
368
+
369
+ .req-item {
370
+ background: var(--bg-secondary);
371
+ border: 1px solid var(--border-subtle);
372
+ border-radius: 12px;
373
+ padding: 16px;
374
+ text-align: center;
375
+ box-shadow: var(--shadow-card);
376
+ }
377
+
378
+ .req-item .icon {
379
+ font-size: 24px;
380
+ margin-bottom: 6px;
381
+ }
382
+
383
+ .req-item .label {
384
+ font-size: 10px;
385
+ color: var(--text-muted);
386
+ text-transform: uppercase;
387
+ letter-spacing: 0.5px;
388
+ }
389
+
390
+ .req-item .value {
391
+ font-size: 13.5px;
392
+ font-weight: 600;
393
+ color: var(--text-primary);
394
+ margin-top: 4px;
395
+ }
396
+
397
+ .step {
398
+ display: flex;
399
+ gap: 16px;
400
+ margin: 16px 0;
401
+ padding: 16px;
402
+ background: var(--bg-secondary);
403
+ border: 1px solid var(--border-subtle);
404
+ border-radius: 12px;
405
+ box-shadow: var(--shadow-card);
406
+ }
407
+
408
+ .step-num {
409
+ flex-shrink: 0;
410
+ width: 28px;
411
+ height: 28px;
412
+ border-radius: 6px;
413
+ background: var(--grad);
414
+ display: flex;
415
+ align-items: center;
416
+ justify-content: center;
417
+ font-weight: 700;
418
+ font-size: 12px;
419
+ color: #fff;
420
+ }
421
+
422
+ .step-body h4 {
423
+ margin: 0 0 4px;
424
+ color: var(--text-primary);
425
+ }
426
+
427
+ .step-body p {
428
+ margin: 0;
429
+ color: var(--text-secondary);
430
+ font-size: 12.5px;
431
+ }
432
+
433
+ .port-badge {
434
+ display: inline-block;
435
+ padding: 2px 6px;
436
+ border-radius: 4px;
437
+ background: var(--accent-blue-subtle);
438
+ color: var(--accent-blue);
439
+ font-family: 'JetBrains Mono', monospace;
440
+ font-size: 11px;
441
+ font-weight: 600;
442
+ }
443
+
444
+ .top-toolbar {
445
+ position: absolute;
446
+ top: 20px;
447
+ right: 20px;
448
+ z-index: 10;
449
+ display: flex;
450
+ gap: 8px;
451
+ }
452
+
453
+ .lang-selector {
454
+ background: var(--bg-secondary);
455
+ border: 1px solid var(--border-subtle);
456
+ padding: 4px 10px;
457
+ border-radius: 6px;
458
+ color: var(--text-secondary);
459
+ font-size: 11.5px;
460
+ outline: none;
461
+ cursor: pointer;
462
+ box-shadow: var(--shadow-card);
463
+ }
464
+
465
+ .lang-selector:hover {
466
+ border-color: var(--accent-blue);
467
+ color: var(--text-primary);
468
+ }
469
+
470
+ /* Code */
471
+ code {
472
+ font-family: 'JetBrains Mono', 'SF Mono', monospace;
473
+ font-size: 11px;
474
+ background: var(--accent-blue-subtle);
475
+ padding: 2px 5px;
476
+ border-radius: 4px;
477
+ color: var(--accent-blue);
478
+ }
479
+
480
+ .code-wrap {
481
+ position: relative;
482
+ margin: 16px 0;
483
+ border-radius: 8px;
484
+ overflow: hidden;
485
+ border: 1px solid var(--border-subtle);
486
+ box-shadow: var(--shadow-card);
487
+ }
488
+
489
+ .code-header {
490
+ display: flex;
491
+ align-items: center;
492
+ justify-content: space-between;
493
+ padding: 6px 14px;
494
+ background: var(--bg-hover);
495
+ border-bottom: 1px solid var(--border-subtle);
496
+ }
497
+
498
+ .code-header span {
499
+ font-size: 11px;
500
+ color: var(--text-muted);
501
+ font-family: 'JetBrains Mono', monospace;
502
+ }
503
+
504
+ .copy-btn {
505
+ padding: 2px 8px;
506
+ border: 1px solid var(--border-subtle);
507
+ border-radius: 4px;
508
+ background: var(--bg-secondary);
509
+ color: var(--text-secondary);
510
+ cursor: pointer;
511
+ font-size: 10px;
512
+ transition: all 0.15s;
513
+ }
514
+
515
+ .copy-btn:hover {
516
+ background: var(--accent-blue);
517
+ color: white;
518
+ border-color: var(--accent-blue);
519
+ }
520
+
521
+ pre {
522
+ background: var(--bg-card);
523
+ padding: 12px 16px;
524
+ overflow-x: auto;
525
+ font-family: 'JetBrains Mono', monospace;
526
+ font-size: 11.5px;
527
+ line-height: 1.5;
528
+ color: var(--text-secondary);
529
+ }
530
+
531
+ pre .comment { color: var(--text-muted); font-style: italic; }
532
+ pre .keyword { color: var(--accent-purple); font-weight: 600; }
533
+ pre .string { color: var(--accent-emerald); }
534
+ pre .cmd { color: var(--accent-blue); }
535
+
536
+ @media (max-width: 900px) {
537
+ .sidebar { display: none; }
538
+ .main { margin-left: 0; }
539
+ .content { padding: 24px; }
540
+ .hero { padding: 32px 24px; }
541
+ .hero h1 { font-size: 22px; }
542
+ }
543
+ </style>
544
+ </head>
545
+ <body>
546
+
547
+ <aside class="sidebar">
548
+ <div class="sidebar-logo">
549
+ <h1>πŸ›‘οΈ CertBridge Security</h1>
550
+ <p data-i18n="sidebar.ver">λ³΄μ•ˆ 및 에어갭 κ°€μ΄λ“œ v0.1.0</p>
551
+ </div>
552
+ <nav id="nav">
553
+ <a href="#overview" class="active"><span class="num">1</span><span data-i18n="nav.overview">μ•„ν‚€ν…μ²˜ κ°œμš”</span></a>
554
+ <a href="#airgap-prep"><span class="num">2</span><span data-i18n="nav.prep">에어갭 νŒ¨ν‚€μ§€ μ€€λΉ„</span></a>
555
+ <a href="#server-install"><span class="num">3</span><span data-i18n="nav.install">μ˜€ν”„λΌμΈ μ„€μΉ˜ ν”„λ‘œν† μ½œ</span></a>
556
+ <a href="#npm-setup"><span class="num">4</span><span data-i18n="nav.npm">Nginx Proxy Manager</span></a>
557
+ <a href="#authentik-setup"><span class="num">5</span><span data-i18n="nav.authentik">Authentik SSO 연동</span></a>
558
+ <a href="#troubleshooting"><span class="num">6</span><span data-i18n="nav.trouble">문제 ν•΄κ²°</span></a>
559
+ </nav>
560
+ </aside>
561
+
562
+ <div class="main">
563
+ <div class="top-toolbar">
564
+ <select id="lang-select" class="lang-selector" onchange="changeLanguage(this.value)">
565
+ <option value="ko">ν•œκ΅­μ–΄</option>
566
+ <option value="en">English</option>
567
+ </select>
568
+ </div>
569
+
570
+ <div class="hero">
571
+ <h1 data-i18n="hero.title">CertBridge λ³΄μ•ˆ κ°•ν™” 및 에어갭 κ°€μ΄λ“œ</h1>
572
+ <p data-i18n="hero.desc">μ˜€ν”„λΌμΈ(Air-Gap) μš°λΆ„νˆ¬ ν™˜κ²½μ—μ„œ Nginx Proxy Manager와 Authentik SSOλ₯Ό ν™œμš©ν•˜μ—¬ SSL λ³΄μ•ˆ ν„°λ―Έλ„€μ΄μ…˜ 및 계정 톡합 인증 μ‹œμŠ€ν…œμ„ μ•ˆμ „ν•˜κ²Œ κ΅¬μ„±ν•˜λŠ” κ³ κΈ‰ κ°€μ΄λ“œμž…λ‹ˆλ‹€.</p>
573
+ <span class="badge" data-i18n="hero.badge">πŸ”’ v0.1.0 Β· Ubuntu amd64 Β· NPM & Authentik</span>
574
+ </div>
575
+
576
+ <div class="content">
577
+
578
+ <!-- 1. Overview -->
579
+ <section class="section" id="overview">
580
+ <h2 data-i18n="overview.title">1. λ³΄μ•ˆ κ°•ν™” μ•„ν‚€ν…μ²˜ κ°œμš”</h2>
581
+ <p data-i18n="overview.desc1">κΈ°μ‘΄ 쀑앙 μ„œλ²„ μΈν”„λΌλŠ” 포트 λ…ΈμΆœ 및 무인증 ν†΅μ‹ μœΌλ‘œ 인해 λ³΄μ•ˆ μœ„ν˜‘μ— μ·¨μ•½ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ³Έ κ°œμ • μŠ€νƒμ—μ„œλŠ” <b>Nginx Proxy Manager (NPM)</b>와 <b>Authentik SSO</b>λ₯Ό 쀑앙 인프라에 ν†΅ν•©ν•˜μ—¬ μ „λ°© λ³΄μ•ˆ μž₯벽을 κ΅¬μΆ•ν•©λ‹ˆλ‹€.</p>
582
+
583
+ <div class="card info">
584
+ <h4 data-i18n="overview.strategy_title">πŸ’‘ μžμ› μ΅œμ ν™” 섀계 μ „λž΅ (Postgres/Redis 곡유)</h4>
585
+ <p data-i18n="overview.strategy_desc">μƒˆλ‘œ μΆ”κ°€λ˜λŠ” SSO(Authentik) μ„œλΉ„μŠ€μ˜ μ˜€λ²„ν—€λ“œλ₯Ό μ œμ–΄ν•˜κΈ° μœ„ν•΄, κΈ°μ‘΄ PostgreSQL 및 Redis μ»¨ν…Œμ΄λ„ˆ μžμ›μ„ κ³ μŠ€λž€νžˆ μž¬ν™œμš©ν•©λ‹ˆλ‹€.</p>
586
+ <ul>
587
+ <li><b>PostgreSQL 16:</b> <code>authentik</code> μ΄λΌλŠ” λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό κΈ°μ‘΄ PostgreSQL 인프라 내에 μƒμ„±ν•˜μ—¬ μžμ›μ„ κ³΅μœ ν•©λ‹ˆλ‹€.</li>
588
+ <li><b>Redis 7:</b> DB Index <code>3</code>을 독립 ν• λ‹Ήν•˜μ—¬ κΈ°μ‘΄ CertBridge용 인덱슀 <code>2</code> 및 n8n λ“±κ³Όμ˜ μΆ©λŒμ„ λ°©μ§€ν•©λ‹ˆλ‹€.</li>
589
+ </ul>
590
+ </div>
591
+
592
+ <h3 data-i18n="overview.net_title">🌐 μ„œλΉ„μŠ€ 포트 λ§΅ & λ„€νŠΈμ›Œν¬ ν† ν΄λ‘œμ§€</h3>
593
+ <table>
594
+ <thead>
595
+ <tr>
596
+ <th>μ»¨ν…Œμ΄λ„ˆλͺ…</th>
597
+ <th>기본 포트</th>
598
+ <th>μ—­ν• </th>
599
+ <th>λ³΄μ•ˆ μƒνƒœ</th>
600
+ </tr>
601
+ </thead>
602
+ <tbody>
603
+ <tr>
604
+ <td><code>certbridge-npm</code></td>
605
+ <td><span class="port-badge">80</span>, <span class="port-badge">443</span>, <span class="port-badge">81</span></td>
606
+ <td>Edge Reverse Proxy & SSL (Web UI)</td>
607
+ <td>μ™ΈλΆ€ λ…ΈμΆœ (μœ μΌν•œ μ§„μž…μ )</td>
608
+ </tr>
609
+ <tr>
610
+ <td><code>certbridge-authentik-server</code></td>
611
+ <td><span class="port-badge">9000</span>, <span class="port-badge">9443</span></td>
612
+ <td>Authentik Core Portal & API</td>
613
+ <td>λ‚΄λΆ€ λΈŒλ¦Ώμ§€λ§ λ˜λŠ” NPM 연동</td>
614
+ </tr>
615
+ <tr>
616
+ <td><code>certbridge-api</code></td>
617
+ <td><span class="port-badge">8090</span></td>
618
+ <td>CertBridge REST API Server</td>
619
+ <td>내뢀망 μ „μš© (NPM을 ν†΅ν•΄μ„œλ§Œ HTTPS μ™ΈλΆ€ λ…ΈμΆœ)</td>
620
+ </tr>
621
+ <tr>
622
+ <td><code>certbridge-minio</code></td>
623
+ <td><span class="port-badge">9010</span>, <span class="port-badge">9011</span></td>
624
+ <td>Object Storage & Management Console</td>
625
+ <td>NPM Proxy Provider둜 SSO 연동 ν†΅μ œ</td>
626
+ </tr>
627
+ <tr>
628
+ <td><code>certbridge-neo4j</code></td>
629
+ <td><span class="port-badge">7474</span>, <span class="port-badge">7687</span></td>
630
+ <td>Neo4j Graph Database & Browser UI</td>
631
+ <td>NPM Proxy Provider둜 SSO 연동 ν†΅μ œ</td>
632
+ </tr>
633
+ </tbody>
634
+ </table>
635
+ </section>
636
+
637
+ <!-- 2. Air-gap Prep -->
638
+ <section class="section" id="airgap-prep">
639
+ <h2 data-i18n="prep.title">2. 에어갭(μ˜€ν”„λΌμΈ) νŒ¨ν‚€μ§€ μ€€λΉ„</h2>
640
+ <p data-i18n="prep.desc">인터넷 연결이 μ „ν˜€ λΆˆκ°€λŠ₯ν•œ <b>에어갭(Air-Gap) 폐쇄망 μš°λΆ„νˆ¬ μ„œλ²„</b>μ—μ„œ μ„€μΉ˜ν•˜κΈ° μœ„ν•΄μ„œλŠ”, 인터넷이 μ—°κ²°λœ 사전 λ¨Έμ‹ μ—μ„œ ν•„μš”ν•œ λͺ¨λ“  Docker 이미지와 νŒ¨ν‚€μ§€λ₯Ό λ‹€μš΄λ‘œλ“œν•΄μ•Ό ν•©λ‹ˆλ‹€.</p>
641
+
642
+ <h3 data-i18n="prep.step1">2-1. Hugging Faceμ—μ„œ 에어갭 파일 λ‹€μš΄λ‘œλ“œ</h3>
643
+ <p data-i18n="prep.desc2">μš°λ¦¬λŠ” Hugging Face μ €μž₯μ†Œ (<code>VanyaJ/certbridge</code>)에 미리 λΉŒλ“œλœ <b>linux/amd64</b> ν”Œλž«νΌ μ „μš© Docker 이미지 μ•„μΉ΄μ΄λΈŒμ™€ μ„œλ²„ νŒ¨ν‚€μ§€λ₯Ό 배포해 λ‘μ—ˆμŠ΅λ‹ˆλ‹€.</p>
644
+
645
+ <div class="code-wrap">
646
+ <div class="code-header"><span>Download Assets</span><button class="copy-btn" onclick="copyCode(this)">볡사</button></div>
647
+ <pre><span class="comment"># 1. CertBridge 핡심 μ„œλ²„ μ•„μΉ΄μ΄λΈŒ λ‹€μš΄λ‘œλ“œ</span>
648
+ <span class="cmd">wget</span> https://huggingface.co/VanyaJ/certbridge/resolve/main/certbridge-server-v0.1.0-ubuntu.tar.gz
649
+
650
+ <span class="comment"># 2. 에어갭 Docker 이미지 λ‹€μš΄λ‘œλ“œ (docker-images/ 디렉토리에 λ°°μΉ˜ν•  것)</span>
651
+ <span class="cmd">mkdir</span> -p docker-images
652
+ <span class="cmd">wget</span> -P docker-images/ https://huggingface.co/VanyaJ/certbridge/resolve/main/docker-images/pgvector_pgvector_pg16.tar.gz
653
+ <span class="cmd">wget</span> -P docker-images/ https://huggingface.co/VanyaJ/certbridge/resolve/main/docker-images/redis_7-alpine.tar.gz
654
+ <span class="cmd">wget</span> -P docker-images/ https://huggingface.co/VanyaJ/certbridge/resolve/main/docker-images/minio_minio_latest.tar.gz
655
+ <span class="cmd">wget</span> -P docker-images/ https://huggingface.co/VanyaJ/certbridge/resolve/main/docker-images/neo4j_5-community.tar.gz
656
+ <span class="cmd">wget</span> -P docker-images/ https://huggingface.co/VanyaJ/certbridge/resolve/main/docker-images/node_20-alpine.tar.gz
657
+ <span class="cmd">wget</span> -P docker-images/ https://huggingface.co/VanyaJ/certbridge/resolve/main/docker-images/jc21_nginx-proxy-manager_latest.tar.gz
658
+ <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>
659
+ </div>
660
+
661
+ <h3 data-i18n="prep.step2">2-2. (λŒ€μ•ˆ) prepare-airgap-package.sh ν™œμš©</h3>
662
+ <p data-i18n="prep.desc3">인터넷이 μ—°κ²°λœ μ™ΈλΆ€ μš°λΆ„νˆ¬ PCμ—μ„œ 직접 이 슀크립트λ₯Ό μˆ˜ν–‰ν•˜λ©΄, Docker 이미지 및 Node/Docker deb μ„€μΉ˜ νŒ¨ν‚€μ§€κΉŒμ§€ ν•˜λ‚˜μ˜ λ²ˆλ“€λ‘œ λ¬Άμ–΄ μ••μΆ•ν•΄ μ€λ‹ˆλ‹€.</p>
663
+ <div class="code-wrap">
664
+ <div class="code-header"><span>bash</span><button class="copy-btn" onclick="copyCode(this)">볡사</button></div>
665
+ <pre><span class="comment"># 슀크립트 μ‹€ν–‰ν•˜μ—¬ ν•˜λ‚˜μ˜ λŒ€μš©λŸ‰ νŒ¨ν‚€μ§€λ‘œ 아카이빙</span>
666
+ <span class="cmd">bash</span> prepare-airgap-package.sh</pre>
667
+ </div>
668
+ </section>
669
+
670
+ <!-- 3. Offline Installation -->
671
+ <section class="section" id="server-install">
672
+ <h2 data-i18n="install.title">3. μ˜€ν”„λΌμΈ μ„€μΉ˜ ν”„λ‘œν† μ½œ (Ubuntu)</h2>
673
+ <p data-i18n="install.desc">λ‹€μš΄λ‘œλ“œν•œ νŒ¨ν‚€μ§€μ™€ <code>docker-images</code> 폴더λ₯Ό 이동식 μ €μž₯μž₯치(USB/μ™Έμž₯ν•˜λ“œ λ“±) λ˜λŠ” 내뢀망 파일 곡유λ₯Ό 톡해 에어갭 λŒ€μƒ μš°λΆ„νˆ¬ μ„œλ²„λ‘œ λ³΅μ‚¬ν•©λ‹ˆλ‹€.</p>
674
+
675
+ <div class="step">
676
+ <div class="step-num">1</div>
677
+ <div class="step-body">
678
+ <h4 data-i18n="install.step1_title">νŒ¨ν‚€μ§€ μ••μΆ• ν•΄μ œ 및 이미지 디렉토리 병합</h4>
679
+ <p data-i18n="install.step1_desc">μ„œλ²„ νŒ¨ν‚€μ§€λ₯Ό ν•΄μ œν•œ ���둜 μ•„λž˜λ‘œ, κ°œλ³„ λ‹€μš΄λ‘œλ“œν•œ <code>docker-images</code> 디렉토리λ₯Ό λ³‘ν•©ν•©λ‹ˆλ‹€.</p>
680
+ <div class="code-wrap">
681
+ <pre><span class="cmd">tar</span> -xzf certbridge-server-v0.1.0-ubuntu.tar.gz
682
+ <span class="cmd">cd</span> certbridge-server-ubuntu
683
+ <span class="comment"># λ‹€μš΄λ‘œλ“œν•œ docker-images 폴더λ₯Ό 이곳으둜 볡사</span>
684
+ <span class="cmd">mv</span> /path/to/downloaded/docker-images ./</pre>
685
+ </div>
686
+ </div>
687
+ </div>
688
+
689
+ <div class="step">
690
+ <div class="step-num">2</div>
691
+ <div class="step-body">
692
+ <h4 data-i18n="install.step2_title">μžλ™ μ…‹μ—… 슀크립트 μ‹€ν–‰</h4>
693
+ <p data-i18n="install.step2_desc">μ˜€ν”„λΌμΈ μ„€μΉ˜μš© μ…‹μ—… 슀크립트 <code>setup-airgap.sh</code>에 μ‹€ν–‰ κΆŒν•œμ„ λΆ€μ—¬ν•˜κ³  κ°€λ™ν•©λ‹ˆλ‹€. 이 μŠ€ν¬λ¦½νŠΈλŠ” Docker load λͺ…령을 톡해 μ˜€ν”„λΌμΈ tar 및 tar.gz 이미지λ₯Ό 전체 λ³΅κ΅¬ν•˜κ³  인프라 μ„œλΉ„μŠ€λ₯Ό κΈ°λ™ν•©λ‹ˆλ‹€.</p>
694
+ <div class="code-wrap">
695
+ <pre><span class="cmd">chmod</span> +x setup-airgap.sh
696
+ <span class="cmd">sudo</span> ./setup-airgap.sh</pre>
697
+ </div>
698
+ </div>
699
+ </div>
700
+
701
+ <div class="step">
702
+ <div class="step-num">3</div>
703
+ <div class="step-body">
704
+ <h4 data-i18n="install.step3_title">독립 λͺ¨λ“œ(--profile standalone) μ„œλΉ„μŠ€ 기동 μƒνƒœ 쑰회</h4>
705
+ <p data-i18n="install.step3_desc">NPM 및 Authentik μ„œλΉ„μŠ€λ₯Ό ν¬ν•¨ν•˜μ—¬ 정상 ꡬ동 쀑인지 μ•„λž˜ λͺ…λ ΉμœΌλ‘œ ν™•μΈν•©λ‹ˆλ‹€.</p>
706
+ <div class="code-wrap">
707
+ <pre><span class="cmd">sudo docker compose</span> --profile standalone ps</pre>
708
+ </div>
709
+ </div>
710
+ </div>
711
+ </section>
712
+
713
+ <!-- 4. Nginx Proxy Manager Setup -->
714
+ <section class="section" id="npm-setup">
715
+ <h2 data-i18n="npm.title">4. Nginx Proxy Manager μ„€μ • κ°€μ΄λ“œ</h2>
716
+ <p data-i18n="npm.desc">Nginx Proxy Manager(NPM)λŠ” CertBridge μŠ€νƒμ˜ μ΅œμ „λ°©μ—μ„œ λͺ¨λ“  μ™ΈλΆ€ μœ μž… μš”μ²­μ„ μ²˜λ¦¬ν•˜κ³ , SSL μ•”ν˜Έν™” 톡신을 μ’…λ‹¨ν•˜λ©°, 도메인/경둜 λΆ„κΈ°λ₯Ό μˆ˜ν–‰ν•©λ‹ˆλ‹€.</p>
717
+
718
+ <div class="step">
719
+ <div class="step-num">1</div>
720
+ <div class="step-body">
721
+ <h4 data-i18n="npm.step1_title">κ΄€λ¦¬μž ν™”λ©΄(Admin UI) 둜그인</h4>
722
+ <p data-i18n="npm.step1_desc">μ›Ή λΈŒλΌμš°μ €λ‘œ <code>http://<SERVER_IP>:81</code> 에 μ ‘μ†ν•©λ‹ˆλ‹€. 졜초 κΈ°λ³Έ 둜그인 계정은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.</p>
723
+ <ul>
724
+ <li><b>Email Address:</b> <code>admin@example.com</code></li>
725
+ <li><b>Password:</b> <code>changeme</code></li>
726
+ </ul>
727
+ <p class="comment">β€» 둜그인 직후 μ¦‰μ‹œ κ΄€λ¦¬μž 이메일과 λΉ„λ°€λ²ˆν˜Έλ₯Ό λ³€κ²½ν•΄μ•Ό ν•©λ‹ˆλ‹€.</p>
728
+ </div>
729
+ </div>
730
+
731
+ <div class="step">
732
+ <div class="step-num">2</div>
733
+ <div class="step-body">
734
+ <h4 data-i18n="npm.step2_title">사섀 SSL/TLS μΈμ¦μ„œ μΆ”κ°€ (에어갭 ν™˜κ²½)</h4>
735
+ <p data-i18n="npm.step2_desc">인프라망이 에어갭 μ˜€ν”„λΌμΈμ΄λ―€λ‘œ Let's Encrypt μžλ™ λ°œκΈ‰μ„ μ‚¬μš©ν•  수 μ—†μŠ΅λ‹ˆλ‹€. κΈ°κ΄€ λ‚΄λΆ€ CA λ˜λŠ” μ‚¬μ„€λ‘œ λ°œκΈ‰λ°›μ€ μΈμ¦μ„œ νŒŒμΌμ„ μˆ˜λ™ 등둝해야 ν•©λ‹ˆλ‹€.</p>
736
+ <ol>
737
+ <li>NPM 상단 λ©”λ‰΄μ˜ <b>"SSL Certificates"</b> νƒ­ 클릭</li>
738
+ <li>우츑 상단 <b>"Add SSL Certificate"</b> &gt; <b>"Custom"</b> 선택</li>
739
+ <li>μΈμ¦μ„œ 이름 μž…λ ₯ ν›„, <b>Certificate Key File</b> (.key) 및 <b>Certificate File</b> (.crt/pem) μ—…λ‘œλ“œ ν›„ μ €μž₯</li>
740
+ </ol>
741
+ </div>
742
+ </div>
743
+
744
+ <div class="step">
745
+ <div class="step-num">3</div>
746
+ <div class="step-body">
747
+ <h4 data-i18n="npm.step3_title">Proxy Host μΆ”κ°€ (CertBridge API λ“±)</h4>
748
+ <p data-i18n="npm.step3_desc">NPM ν”„λ‘μ‹œ 호슀트λ₯Ό μƒμ„±ν•˜μ—¬ μ™ΈλΆ€ HTTPS μš”μ²­μ„ λ‚΄λΆ€ μ»¨ν…Œμ΄λ„ˆλ‘œ μ „λ‹¬ν•©λ‹ˆλ‹€.</p>
749
+ <ul>
750
+ <li><b>Domain Names:</b> <code>certbridge.local</code> (λ˜λŠ” κΈ°κ΄€μš© 도메인)</li>
751
+ <li><b>Scheme:</b> <code>http</code></li>
752
+ <li><b>Forward Hostname / IP:</b> <code>certbridge-api</code> (Docker λΈŒλ¦Ώμ§€ μ»¨ν…Œμ΄λ„ˆ 호슀트λͺ…)</li>
753
+ <li><b>Forward Port:</b> <code>8090</code></li>
754
+ <li><b>Block Common Exploits:</b> ν™œμ„±ν™”</li>
755
+ <li><b>SSL νƒ­:</b> μœ„μ—μ„œ λ“±λ‘ν•œ Custom SSL 선택 및 <b>Force SSL</b> ν™œμ„±ν™”</li>
756
+ </ul>
757
+ </div>
758
+ </div>
759
+ </section>
760
+
761
+ <!-- 5. Authentik Setup -->
762
+ <section class="section" id="authentik-setup">
763
+ <h2 data-i18n="authentik.title">5. Authentik SSO / MFA μ„€μ • ν”„λ‘œν† μ½œ</h2>
764
+ <p data-i18n="authentik.desc">Authentik은 μ‹±κΈ€ μ‚¬μΈμ˜¨(SSO)κ³Ό λ©€ν‹°νŒ©ν„° 인증(MFA)을 μΌκ΄€λ˜κ²Œ μ œκ³΅ν•˜μ—¬, API, MinIO Console, Neo4j Graph DB둜의 무단 μΉ¨μž…μ„ ν†΅μ œν•©λ‹ˆλ‹€.</p>
765
+
766
+ <div class="step">
767
+ <div class="step-num">1</div>
768
+ <div class="step-body">
769
+ <h4 data-i18n="authentik.step1_title">졜초 μΈμŠ€ν„΄μŠ€ μ…‹μ—… 및 μ΄ˆκΈ°ν™”</h4>
770
+ <p data-i18n="authentik.step1_desc">μ›Ή λΈŒλΌμš°μ €λ‘œ <code>http://<SERVER_IP>:9000/if/flow/initial-setup/</code> 에 μ ‘μ†ν•©λ‹ˆλ‹€. 졜초 λΆ€νŠΈμŠ€νŠΈλž© ν™”λ©΄μ—μ„œ κ΄€λ¦¬μž 계정 <code>akadmin</code>에 λŒ€ν•œ μ•ˆμ „ν•œ λΉ„λ°€λ²ˆν˜Έλ₯Ό μ„€μ •ν•˜μ—¬ μ΄ˆκΈ°ν™”λ₯Ό μ™„λ£Œν•©λ‹ˆλ‹€.</p>
771
+ </div>
772
+ </div>
773
+
774
+ <div class="step">
775
+ <div class="step-num">2</div>
776
+ <div class="step-body">
777
+ <h4 data-i18n="authentik.step2_title">Proxy Provider 생성 (NPM Forward Auth μ—°λ™μš©)</h4>
778
+ <p data-i18n="authentik.step2_desc">MinIO와 Neo4j λ“± κ°œλ³„ λ³΄μ•ˆμ΄ ν•„μš”ν•œ λ ˆκ±°μ‹œ ν™”λ©΄ 보호λ₯Ό μœ„ν•΄ Proxy Providerλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.</p>
779
+ <ol>
780
+ <li>Authentik Admin Interface μ§„μž… (우츑 상단 <b>"Admin interface"</b>)</li>
781
+ <li>쒌츑 메뉴 <b>Applications</b> &gt; <b>Providers</b> &gt; <b>"Create"</b> 클릭</li>
782
+ <li><b>Proxy Provider</b> 선택 ν›„ μ„€μ •:
783
+ <ul>
784
+ <li><b>Name:</b> <code>infra-proxy-provider</code></li>
785
+ <li><b>Authorization flow:</b> κΈ°λ³Έ flow 선택 (e.g. default-authorization-flow)</li>
786
+ <li><b>External host:</b> <code>http://minio.certbridge.local</code> (μ™ΈλΆ€ λ…ΈμΆœλ  URL)</li>
787
+ <li><b>Mode:</b> <code>Forward auth (single application)</code> 선택</li>
788
+ </ul>
789
+ </li>
790
+ </ol>
791
+ </div>
792
+ </div>
793
+
794
+ <div class="step">
795
+ <div class="step-num">3</div>
796
+ <div class="step-body">
797
+ <h4 data-i18n="authentik.step3_title">NPM Custom Nginx Configuration 적용</h4>
798
+ <p data-i18n="authentik.step3_desc">Nginx Proxy Managerμ—μ„œ ν•΄λ‹Ή Proxy Host둜 λ“€μ–΄μ˜€λŠ” νŠΈλž˜ν”½μ΄ Authentik을 거쳐 μΈμ¦λ˜λ„λ‘ Custom Nginx μ„€μ • 블둝을 λ„£μ–΄μ€λ‹ˆλ‹€.</p>
799
+ <div class="code-wrap">
800
+ <div class="code-header"><span>NPM Advanced Config (Custom Nginx Configuration)</span></div>
801
+ <pre># Authentik Forward Auth Integration
802
+ location / {
803
+ # Authentik Server IP / Container Port
804
+ auth_request /outpost.goauthentik.io/auth/request;
805
+ error_page 401 = @goauthentik_login;
806
+
807
+ # Pass the actual request back to Authentik
808
+ proxy_pass $forward_scheme://$server:$port;
809
+
810
+ # Auth variables sync
811
+ auth_request_set $auth_cookie $upstream_http_set_cookie;
812
+ add_header Set-Cookie $auth_cookie;
813
+
814
+ # Headers
815
+ auth_request_set $authentik_username $upstream_http_x_authentik_username;
816
+ auth_request_set $authentik_groups $upstream_http_x_authentik_groups;
817
+ auth_request_set $authentik_email $upstream_http_x_authentik_email;
818
+ auth_request_set $authentik_name $upstream_http_x_authentik_name;
819
+
820
+ proxy_set_header X-Authentik-Username $authentik_username;
821
+ proxy_set_header X-Authentik-Groups $authentik_groups;
822
+ proxy_set_header X-Authentik-Email $authentik_email;
823
+ proxy_set_header X-Authentik-Name $authentik_name;
824
+ }
825
+
826
+ location = /outpost.goauthentik.io/auth/request {
827
+ proxy_pass http://certbridge-authentik-server:9000/outpost.goauthentik.io/auth/request;
828
+ proxy_set_header Host $host;
829
+ proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
830
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
831
+ proxy_set_header Content-Length "";
832
+ proxy_method GET;
833
+ }
834
+
835
+ location @goauthentik_login {
836
+ return 302 http://certbridge-authentik-server:9000/outpost.goauthentik.io/auth/start?rd=$scheme://$http_host$request_uri;
837
+ }</pre>
838
+ </div>
839
+ </div>
840
+ </div>
841
+ </section>
842
+
843
+ <!-- 6. Troubleshooting -->
844
+ <section class="section" id="troubleshooting">
845
+ <h2 data-i18n="trouble.title">6. 문제 ν•΄κ²° (Troubleshooting)</h2>
846
+
847
+ <div class="card warn">
848
+ <h4>1. DB Connection Refused (postgres/redis)</h4>
849
+ <p>Authentik μ»¨ν…Œμ΄λ„ˆκ°€ μ‹œμž‘ν•  λ•Œ PostgreSQL λ˜λŠ” Redis에 μ ‘μ†ν•˜μ§€ λͺ»ν•΄ ν¬λž˜μ‹œλ˜λŠ” 경우:</p>
850
+ <p><b>원인:</b> postgresλ‚˜ redisκ°€ 아직 κΈ°λ™λ˜μ§€ μ•Šμ•˜κ±°λ‚˜ ν—¬μŠ€μ²΄ν¬ 검증 단계에 μ‹œκ°„μ΄ ν•„μš”ν•œ κ²½μš°μž…λ‹ˆλ‹€.</p>
851
+ <p><b>ν•΄κ²°μ±…:</b> <code>docker compose logs -f certbridge-db</code> λͺ…λ ΉμœΌλ‘œ DB μ„œλ²„ μ‹œμž‘ 둜그λ₯Ό ν™•μΈν•˜κ³ , <code>docker compose restart certbridge-authentik-server</code>λ₯Ό 톡해 Authentik을 μž¬μ‹œμž‘ν•΄ μ£Όμ‹­μ‹œμ˜€.</p>
852
+ </div>
853
+
854
+ <div class="card warn">
855
+ <h4>2. Authentik DB init failure (Postgres)</h4>
856
+ <p>Authentik DBκ°€ μžλ™μœΌλ‘œ μƒμ„±λ˜μ§€ μ•Šμ•„ 인증 μ‹œμŠ€ν…œμ΄ κ΅¬λ™λ˜μ§€ μ•ŠλŠ” 경우:</p>
857
+ <p><b>원인:</b> docker-compose.yml의 λ³Όλ₯¨μ— <code>00-init-authentik-db.sql</code> 이 μ œλŒ€λ‘œ λ°”μΈλ”©λ˜μ§€ μ•Šμ•„ <code>authentik</code> μ΄λΌλŠ” λ°μ΄ν„°λ² μ΄μŠ€κ°€ Postgres 내에 λˆ„λ½λœ κ²½μš°μž…λ‹ˆλ‹€.</p>
858
+ <p><b>ν•΄κ²°μ±…:</b> <code>docker compose exec -it certbridge-db psql -U certbridge -c "CREATE DATABASE authentik;"</code> 을 터미널에 직접 μˆ˜ν–‰ν•˜μ—¬ DBλ₯Ό μˆ˜λ™ μƒμ„±ν•˜κ³  Authentik을 μž¬κΈ°λ™ν•˜μ‹­μ‹œμ˜€.</p>
859
+ </div>
860
+ </section>
861
+ </div>
862
+ </div>
863
+
864
+ <script>
865
+ /* ─────────────────────────────────────────
866
+ i18n Dictionary
867
+ ───────────────────────────────────────── */
868
+ const i18n = {
869
+ ko: {
870
+ "sidebar.ver": "λ³΄μ•ˆ 및 에어갭 κ°€μ΄λ“œ v0.1.0",
871
+ "nav.overview": "μ•„ν‚€ν…μ²˜ κ°œμš”",
872
+ "nav.prep": "에어갭 νŒ¨ν‚€μ§€ μ€€λΉ„",
873
+ "nav.install": "μ˜€ν”„λΌμΈ μ„€μΉ˜ ν”„λ‘œν† μ½œ",
874
+ "nav.npm": "Nginx Proxy Manager",
875
+ "nav.authentik": "Authentik SSO 연동",
876
+ "nav.trouble": "문제 ν•΄κ²°",
877
+ "hero.title": "CertBridge λ³΄μ•ˆ κ°•ν™” 및 에어갭 κ°€μ΄λ“œ",
878
+ "hero.desc": "μ˜€ν”„λΌμΈ(Air-Gap) μš°λΆ„νˆ¬ ν™˜κ²½μ—μ„œ Nginx Proxy Manager와 Authentik SSOλ₯Ό ν™œμš©ν•˜μ—¬ SSL λ³΄μ•ˆ ν„°λ―Έλ„€μ΄μ…˜ 및 계정 톡합 인증 μ‹œμŠ€ν…œμ„ μ•ˆμ „ν•˜κ²Œ κ΅¬μ„±ν•˜λŠ” κ³ κΈ‰ κ°€μ΄λ“œμž…λ‹ˆλ‹€.",
879
+ "hero.badge": "πŸ”’ v0.1.0 Β· Ubuntu amd64 Β· NPM & Authentik",
880
+ "overview.title": "1. λ³΄μ•ˆ κ°•ν™” μ•„ν‚€ν…μ²˜ κ°œμš”",
881
+ "overview.desc1": "κΈ°μ‘΄ 쀑앙 μ„œλ²„ μΈν”„λΌλŠ” 포트 λ…ΈμΆœ 및 무인증 ν†΅μ‹ μœΌλ‘œ 인해 λ³΄μ•ˆ μœ„ν˜‘μ— μ·¨μ•½ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ³Έ κ°œμ • μŠ€νƒμ—μ„œλŠ” Nginx Proxy Manager (NPM)와 Authentik SSOλ₯Ό 쀑앙 인프라에 ν†΅ν•©ν•˜μ—¬ μ „λ°© λ³΄μ•ˆ μž₯벽을 κ΅¬μΆ•ν•©λ‹ˆλ‹€.",
882
+ "overview.strategy_title": "πŸ’‘ μžμ› μ΅œμ ν™” 섀계 μ „λž΅ (Postgres/Redis 곡유)",
883
+ "overview.strategy_desc": "μƒˆλ‘œ μΆ”κ°€λ˜λŠ” SSO(Authentik) μ„œλΉ„μŠ€μ˜ μ˜€λ²„ν—€λ“œλ₯Ό μ œμ–΄ν•˜κΈ° μœ„ν•΄, κΈ°μ‘΄ PostgreSQL 및 Redis μ»¨ν…Œμ΄λ„ˆ μžμ›μ„ κ³ μŠ€λž€νžˆ μž¬ν™œμš©ν•©λ‹ˆλ‹€.",
884
+ "overview.net_title": "🌐 μ„œλΉ„μŠ€ 포트 λ§΅ & λ„€νŠΈμ›Œν¬ ν† ν΄λ‘œμ§€",
885
+ "prep.title": "2. 에어갭(μ˜€ν”„λΌμΈ) νŒ¨ν‚€μ§€ μ€€λΉ„",
886
+ "prep.desc": "인터넷 연결이 μ „ν˜€ λΆˆκ°€λŠ₯ν•œ 에어갭(Air-Gap) 폐쇄망 μš°λΆ„νˆ¬ μ„œλ²„μ—μ„œ μ„€μΉ˜ν•˜κΈ° μœ„ν•΄μ„œλŠ”, 인터넷이 μ—°κ²°λœ 사전 λ¨Έμ‹ μ—μ„œ ν•„μš”ν•œ λͺ¨λ“  Docker 이미지와 νŒ¨ν‚€μ§€λ₯Ό λ‹€μš΄λ‘œλ“œν•΄μ•Ό ν•©λ‹ˆλ‹€.",
887
+ "prep.step1": "2-1. Hugging Faceμ—μ„œ 에어갭 파일 λ‹€μš΄λ‘œλ“œ",
888
+ "prep.desc2": "μš°λ¦¬λŠ” Hugging Face μ €μž₯μ†Œ (VanyaJ/certbridge)에 미리 λΉŒλ“œλœ linux/amd64 ν”Œλž«νΌ μ „μš© Docker 이미지 μ•„μΉ΄μ΄λΈŒμ™€ μ„œλ²„ νŒ¨ν‚€μ§€λ₯Ό 배포해 λ‘μ—ˆμŠ΅λ‹ˆλ‹€.",
889
+ "prep.step2": "2-2. (λŒ€μ•ˆ) prepare-airgap-package.sh ν™œμš©",
890
+ "prep.desc3": "인터넷이 μ—°κ²°λœ μ™ΈλΆ€ μš°λΆ„νˆ¬ PCμ—μ„œ 직접 이 슀크립트λ₯Ό μˆ˜ν–‰ν•˜λ©΄, Docker 이미지 및 Node/Docker deb μ„€μΉ˜ νŒ¨ν‚€μ§€κΉŒμ§€ ν•˜λ‚˜μ˜ λ²ˆλ“€λ‘œ λ¬Άμ–΄ μ••μΆ•ν•΄ μ€λ‹ˆλ‹€.",
891
+ "install.title": "3. μ˜€ν”„λΌμΈ μ„€μΉ˜ ν”„λ‘œν† μ½œ (Ubuntu)",
892
+ "install.desc": "λ‹€μš΄λ‘œλ“œν•œ νŒ¨ν‚€μ§€μ™€ docker-images 폴더λ₯Ό 이동식 μ €μž₯μž₯치(USB/μ™Έμž₯ν•˜λ“œ λ“±) λ˜λŠ” 내뢀망 파일 곡유λ₯Ό 톡해 에어갭 λŒ€μƒ μš°λΆ„νˆ¬ μ„œλ²„λ‘œ λ³΅μ‚¬ν•©λ‹ˆλ‹€.",
893
+ "install.step1_title": "νŒ¨ν‚€μ§€ μ••μΆ• ν•΄μ œ 및 이미지 디렉토리 병합",
894
+ "install.step1_desc": "μ„œλ²„ νŒ¨ν‚€μ§€λ₯Ό ν•΄μ œν•œ 경둜 μ•„λž˜λ‘œ, κ°œλ³„ λ‹€μš΄λ‘œλ“œν•œ docker-images 디렉토리λ₯Ό λ³‘ν•©ν•©λ‹ˆλ‹€.",
895
+ "install.step2_title": "μžλ™ μ…‹μ—… 슀크립트 μ‹€ν–‰",
896
+ "install.step2_desc": "μ˜€ν”„λΌμΈ μ„€μΉ˜μš© μ…‹μ—… 슀크립트 setup-airgap.sh에 μ‹€ν–‰ κΆŒν•œμ„ λΆ€μ—¬ν•˜κ³  κ°€λ™ν•©λ‹ˆλ‹€. 이 μŠ€ν¬λ¦½νŠΈλŠ” Docker load λͺ…령을 톡해 μ˜€ν”„λΌμΈ tar 및 tar.gz 이미지λ₯Ό 전체 λ³΅κ΅¬ν•˜κ³  인프라 μ„œλΉ„μŠ€λ₯Ό κΈ°λ™ν•©λ‹ˆλ‹€.",
897
+ "install.step3_title": "독립 λͺ¨λ“œ(--profile standalone) μ„œλΉ„μŠ€ 기동 μƒνƒœ 쑰회",
898
+ "install.step3_desc": "NPM 및 Authentik μ„œλΉ„μŠ€λ₯Ό ν¬ν•¨ν•˜μ—¬ 정상 ꡬ동 쀑인지 μ•„λž˜ λͺ…λ ΉμœΌλ‘œ ν™•μΈν•©λ‹ˆλ‹€.",
899
+ "npm.title": "4. Nginx Proxy Manager μ„€μ • κ°€μ΄λ“œ",
900
+ "npm.desc": "Nginx Proxy Manager(NPM)λŠ” CertBridge μŠ€νƒμ˜ μ΅œμ „λ°©μ—μ„œ λͺ¨λ“  μ™ΈλΆ€ μœ μž… μš”μ²­μ„ μ²˜λ¦¬ν•˜κ³ , SSL μ•”ν˜Έν™” 톡신을 μ’…λ‹¨ν•˜λ©°, 도메인/경둜 λΆ„κΈ°λ₯Ό μˆ˜ν–‰ν•©λ‹ˆλ‹€.",
901
+ "npm.step1_title": "κ΄€λ¦¬μž ν™”λ©΄(Admin UI) 둜그인",
902
+ "npm.step1_desc": "μ›Ή λΈŒλΌμš°μ €λ‘œ http://<SERVER_IP>:81 에 μ ‘μ†ν•©λ‹ˆλ‹€. 졜초 κΈ°λ³Έ 둜그인 계정은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.",
903
+ "npm.step2_title": "사섀 SSL/TLS μΈμ¦μ„œ μΆ”κ°€ (에어갭 ν™˜κ²½)",
904
+ "npm.step2_desc": "인프라망이 에어갭 μ˜€ν”„λΌμΈμ΄λ―€λ‘œ Let's Encrypt μžλ™ λ°œκΈ‰μ„ μ‚¬μš©ν•  수 μ—†μŠ΅λ‹ˆλ‹€. κΈ°κ΄€ λ‚΄λΆ€ CA λ˜λŠ” μ‚¬μ„€λ‘œ λ°œκΈ‰λ°›μ€ μΈμ¦μ„œ νŒŒμΌμ„ μˆ˜λ™ 등둝해야 ν•©λ‹ˆλ‹€.",
905
+ "npm.step3_title": "Proxy Host μΆ”κ°€ (CertBridge API λ“±)",
906
+ "npm.step3_desc": "NPM ν”„λ‘μ‹œ 호슀트λ₯Ό μƒμ„±ν•˜μ—¬ μ™ΈλΆ€ HTTPS μš”μ²­μ„ λ‚΄λΆ€ μ»¨ν…Œμ΄λ„ˆλ‘œ μ „λ‹¬ν•©λ‹ˆλ‹€.",
907
+ "authentik.title": "5. Authentik SSO / MFA μ„€μ • ν”„λ‘œν† μ½œ",
908
+ "authentik.desc": "Authentik은 μ‹±κΈ€ μ‚¬μΈμ˜¨(SSO)κ³Ό λ©€ν‹°νŒ©ν„° 인증(MFA)을 μΌκ΄€λ˜κ²Œ μ œκ³΅ν•˜μ—¬, API, MinIO Console, Neo4j Graph DB둜의 무단 μΉ¨μž…μ„ ν†΅μ œν•©λ‹ˆλ‹€.",
909
+ "authentik.step1_title": "졜초 μΈμŠ€ν„΄μŠ€ μ…‹μ—… 및 μ΄ˆκΈ°ν™”",
910
+ "authentik.step1_desc": "μ›Ή λΈŒλΌμš°μ €λ‘œ http://<SERVER_IP>:9000/if/flow/initial-setup/ 에 μ ‘μ†ν•©λ‹ˆλ‹€. 졜초 λΆ€νŠΈμŠ€νŠΈλž© ν™”λ©΄μ—μ„œ κ΄€λ¦¬μž 계정 akadmin에 λŒ€ν•œ μ•ˆμ „ν•œ λΉ„λ°€λ²ˆν˜Έλ₯Ό μ„€μ •ν•˜μ—¬ μ΄ˆκΈ°ν™”λ₯Ό μ™„λ£Œν•©λ‹ˆλ‹€.",
911
+ "authentik.step2_title": "Proxy Provider 생성 (NPM Forward Auth μ—°λ™μš©)",
912
+ "authentik.step2_desc": "MinIO와 Neo4j λ“± κ°œλ³„ λ³΄μ•ˆμ΄ ν•„μš”ν•œ λ ˆκ±°μ‹œ ν™”λ©΄ 보호λ₯Ό μœ„ν•΄ Proxy Providerλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.",
913
+ "authentik.step3_title": "NPM Custom Nginx Configuration 적용",
914
+ "authentik.step3_desc": "Nginx Proxy Managerμ—μ„œ ν•΄λ‹Ή Proxy Host둜 λ“€μ–΄μ˜€λŠ” νŠΈλž˜ν”½μ΄ Authentik을 거쳐 μΈμ¦λ˜λ„λ‘ Custom Nginx μ„€μ • 블둝을 λ„£μ–΄μ€λ‹ˆλ‹€.",
915
+ "trouble.title": "6. 문제 ν•΄κ²° (Troubleshooting)"
916
+ },
917
+ en: {
918
+ "sidebar.ver": "Security & Air-gap Guide v0.1.0",
919
+ "nav.overview": "Architecture Overview",
920
+ "nav.prep": "Air-gap Package Prep",
921
+ "nav.install": "Offline Install Protocol",
922
+ "nav.npm": "Nginx Proxy Manager",
923
+ "nav.authentik": "Authentik SSO Setup",
924
+ "nav.trouble": "Troubleshooting",
925
+ "hero.title": "CertBridge Security & Air-gap Guide",
926
+ "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.",
927
+ "hero.badge": "πŸ”’ v0.1.0 Β· Ubuntu amd64 Β· NPM & Authentik",
928
+ "overview.title": "1. Security-Hardened Architecture Overview",
929
+ "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.",
930
+ "overview.strategy_title": "πŸ’‘ Resource Optimization Strategy (Shared Postgres/Redis)",
931
+ "overview.strategy_desc": "To control the overhead of the newly added SSO (Authentik) services, existing PostgreSQL and Redis container resources are fully reused.",
932
+ "overview.net_title": "🌐 Port Mapping & Network Topology",
933
+ "prep.title": "2. Offline Air-gap Package Preparation",
934
+ "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.",
935
+ "prep.step1": "2-1. Downloading Air-gap Assets from Hugging Face",
936
+ "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).",
937
+ "prep.step2": "2-2. (Alternative) Using prepare-airgap-package.sh",
938
+ "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.",
939
+ "install.title": "3. Offline Ubuntu Installation Protocol",
940
+ "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.",
941
+ "install.step1_title": "Extract Packages and Merge Image Directories",
942
+ "install.step1_desc": "Extract the server package and merge the separately downloaded docker-images directory into it.",
943
+ "install.step2_title": "Execute Automatic Setup Script",
944
+ "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.",
945
+ "install.step3_title": "Inspect Standalone Mode Services Status",
946
+ "install.step3_desc": "Verify that all services, including NPM and Authentik, are successfully running with the command below.",
947
+ "npm.title": "4. Nginx Proxy Manager Configuration Guide",
948
+ "npm.desc": "Nginx Proxy Manager (NPM) sits at the front line, routing all incoming external traffic, terminating SSL, and managing domain name resolutions.",
949
+ "npm.step1_title": "Login to NPM Admin UI",
950
+ "npm.step1_desc": "Open your web browser and navigate to http://<SERVER_IP>:81. The default bootstrap credentials are:",
951
+ "npm.step2_title": "Add Private SSL/TLS Certificate (Air-gap)",
952
+ "npm.step2_desc": "Since Let's Encrypt cannot auto-issue certificates in an offline environment, you must manually upload a private CA-signed certificate.",
953
+ "npm.step3_title": "Add Proxy Host (for CertBridge API etc.)",
954
+ "npm.step3_desc": "Create a new proxy host in NPM to forward external HTTPS requests to internal docker containers safely.",
955
+ "authentik.title": "5. Authentik SSO / MFA Federation Protocol",
956
+ "authentik.desc": "Authentik provides centralized SSO and MFA, preventing unauthorized entry to the API, MinIO Console, and Neo4j Graph DB.",
957
+ "authentik.step1_title": "Initial Setup and Admin Initialization",
958
+ "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.",
959
+ "authentik.step2_title": "Create Proxy Provider (for NPM Forward Auth)",
960
+ "authentik.step2_desc": "Create a Proxy Provider in Authentik to protect web applications like MinIO and Neo4j.",
961
+ "authentik.step3_title": "Apply Custom Nginx Configuration in NPM",
962
+ "authentik.step3_desc": "Insert custom Nginx configurations into NPM proxy hosts so that all requests route through Authentik authentication filters.",
963
+ "trouble.title": "6. Troubleshooting Guide"
964
+ }
965
+ };
966
+
967
+ /* ─────────────────────────────────────────
968
+ Language Changer
969
+ ───────────────────────────────────────── */
970
+ function changeLanguage(lang) {
971
+ document.querySelectorAll("[data-i18n]").forEach(el => {
972
+ const key = el.getAttribute("data-i18n");
973
+ if (i18n[lang] && i18n[lang][key]) {
974
+ el.innerHTML = i18n[lang][key];
975
+ }
976
+ });
977
+ document.getElementById("html-title").innerText = i18n[lang]["hero.title"] || "CertBridge Security Guide";
978
+ }
979
+
980
+ /* ─────────────────────────────────────────
981
+ Copy to Clipboard
982
+ ───────────────────────────────────────── */
983
+ function copyCode(btn) {
984
+ const pre = btn.closest('.code-wrap').querySelector('pre');
985
+ const text = pre.innerText;
986
+ navigator.clipboard.writeText(text).then(() => {
987
+ const originalText = btn.innerText;
988
+ btn.innerText = "볡사됨!";
989
+ btn.style.background = "var(--accent-emerald)";
990
+ btn.style.borderColor = "var(--accent-emerald)";
991
+ btn.style.color = "white";
992
+ setTimeout(() => {
993
+ btn.innerText = originalText;
994
+ btn.style.background = "var(--bg-secondary)";
995
+ btn.style.borderColor = "var(--border-subtle)";
996
+ btn.style.color = "var(--text-secondary)";
997
+ }, 2000);
998
+ });
999
+ }
1000
+
1001
+ // Sidebar Navigation Highlighting
1002
+ const sections = document.querySelectorAll(".section");
1003
+ const navLinks = document.querySelectorAll(".sidebar nav a");
1004
+
1005
+ window.addEventListener("scroll", () => {
1006
+ let current = "";
1007
+ sections.forEach(section => {
1008
+ const sectionTop = section.offsetTop;
1009
+ if (pageYOffset >= sectionTop - 60) {
1010
+ current = section.getAttribute("id");
1011
+ }
1012
+ });
1013
+ navLinks.forEach(link => {
1014
+ link.classList.remove("active");
1015
+ if (link.getAttribute("href").substring(1) === current) {
1016
+ link.classList.add("active");
1017
+ }
1018
+ });
1019
+ });
1020
+ </script>
1021
+ </body>
1022
+ </html>