97f649a3e1
- Copy delegate-job-skill/skills/delegate-job/ → skills/delegate-job/ - Move requirements.txt (paho-mqtt>=2.0.0) into the new location - Refactor outdated hardcoded paths (~/PuKi/lab/, ~/.hermes/skills/) to dynamic resolution - Add MQTT connection timeout / retry hardening - Remove legacy delegate-job-skill/ directory - Update .gitignore Note: delegate-job-skill git history is squashed — preserved content, dropped commit lineage.
1416 lines
57 KiB
HTML
1416 lines
57 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>delegate-job — MQTT Broker Setup Guide</title>
|
|
<meta name="description" content="자율 에이전트 위임 스킬 delegate-job의 MQTT 브로커 PoC 환경에서 사내 보안 TLS 및 ACL 통제 프로덕션 환경으로의 컷오버 가이드.">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@500;600;700;800&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
|
|
<style>
|
|
/* ----------------------------------------------------------------------
|
|
1. Design Tokens & CSS Variables (Theme Support)
|
|
---------------------------------------------------------------------- */
|
|
:root {
|
|
/* Light Mode (Default) - Clean Slate/Vercel Aesthetic */
|
|
--bg-base: #ffffff;
|
|
--bg-sidebar: #fafafa;
|
|
--bg-surface: #ffffff;
|
|
--bg-surface-hover: #f3f4f6;
|
|
--border: #e5e7eb;
|
|
--border-focus: #111827;
|
|
|
|
--fg-base: #111827;
|
|
--fg-muted: #4b5563;
|
|
--fg-subtle: #9ca3af;
|
|
|
|
--accent: #4f46e5; /* Indigo */
|
|
--accent-glow: rgba(79, 70, 229, 0.05);
|
|
--accent-teal: #0d9488; /* Teal */
|
|
--accent-teal-glow: rgba(13, 148, 136, 0.05);
|
|
|
|
--state-pending: #d97706; /* Amber */
|
|
--state-running: #2563eb; /* Blue */
|
|
--state-completed: #059669; /* Emerald */
|
|
--state-error: #dc2626; /* Red */
|
|
--state-cancel: #4b5563;
|
|
--state-timeout: #db2777;
|
|
|
|
--code-bg: #f8fafc;
|
|
--code-fg: #1e293b;
|
|
--code-border: #e2e8f0;
|
|
--code-comment: #64748b;
|
|
--code-keyword: #4f46e5;
|
|
--code-string: #0d9488;
|
|
--code-number: #d97706;
|
|
|
|
--svg-bg: #f8fafc;
|
|
--svg-node-bg: #ffffff;
|
|
--svg-node-stroke: #e5e7eb;
|
|
--svg-text-base: #111827;
|
|
--svg-text-muted: #4b5563;
|
|
|
|
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
--font-display: 'Outfit', var(--font-sans);
|
|
--font-mono: 'Fira Code', 'JetBrains Mono', Courier, monospace;
|
|
|
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.02);
|
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.03), 0 2px 4px -2px rgba(0, 0, 0, 0.03);
|
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.04), 0 4px 6px -4px rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
/* Dark Mode - Clean Linear/Stripe Aesthetic */
|
|
--bg-base: #08090a;
|
|
--bg-sidebar: #0c0d0e;
|
|
--bg-surface: #121315;
|
|
--bg-surface-hover: #1b1d20;
|
|
--border: #1f2022;
|
|
--border-focus: #f3f4f6;
|
|
|
|
--fg-base: #f3f4f6;
|
|
--fg-muted: #9ca3af;
|
|
--fg-subtle: #4b5563;
|
|
|
|
--accent: #6366f1; /* Bright Indigo */
|
|
--accent-glow: rgba(99, 102, 241, 0.12);
|
|
--accent-teal: #14b8a6; /* Teal */
|
|
--accent-teal-glow: rgba(20, 184, 166, 0.12);
|
|
|
|
--code-bg: #0d0e10;
|
|
--code-fg: #e5e7eb;
|
|
--code-border: #1a1b1d;
|
|
--code-comment: #4b5563;
|
|
--code-keyword: #818cf8;
|
|
--code-string: #34d399;
|
|
--code-number: #fbbf24;
|
|
|
|
--svg-bg: #0b0d10;
|
|
--svg-node-bg: #121315;
|
|
--svg-node-stroke: #1f2022;
|
|
--svg-text-base: #f3f4f6;
|
|
--svg-text-muted: #9ca3af;
|
|
|
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
|
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.5);
|
|
}
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
2. Global Layout Reset
|
|
---------------------------------------------------------------------- */
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
html, body {
|
|
background-color: var(--bg-base);
|
|
color: var(--fg-base);
|
|
font-family: var(--font-sans);
|
|
font-size: 16px;
|
|
line-height: 1.7;
|
|
min-height: 100vh;
|
|
-webkit-font-smoothing: antialiased;
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
body {
|
|
display: flex;
|
|
}
|
|
|
|
/* Layout Container */
|
|
.app-container {
|
|
display: grid;
|
|
grid-template-columns: 280px minmax(0, 1fr) 260px;
|
|
gap: 40px;
|
|
max-width: 1440px;
|
|
margin: 0 auto;
|
|
padding: 0 32px;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
3. Left Sidebar (Project Info & Navigation)
|
|
---------------------------------------------------------------------- */
|
|
.left-sidebar {
|
|
position: sticky;
|
|
top: 0;
|
|
height: 100vh;
|
|
padding: 48px 0;
|
|
overflow-y: auto;
|
|
border-right: 1px solid var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 32px;
|
|
z-index: 10;
|
|
}
|
|
|
|
.logo-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
text-decoration: none;
|
|
color: var(--fg-base);
|
|
font-family: var(--font-display);
|
|
font-weight: 700;
|
|
font-size: 1.2rem;
|
|
}
|
|
.logo-symbol {
|
|
fill: none;
|
|
stroke: var(--accent);
|
|
stroke-width: 2.5;
|
|
}
|
|
|
|
.sidebar-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.sidebar-title {
|
|
font-family: var(--font-display);
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--fg-subtle);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.sidebar-links {
|
|
list-style: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.sidebar-link {
|
|
font-size: 0.9rem;
|
|
color: var(--fg-muted);
|
|
text-decoration: none;
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.sidebar-link:hover {
|
|
background-color: var(--bg-surface-hover);
|
|
color: var(--fg-base);
|
|
}
|
|
.sidebar-link.active {
|
|
background-color: var(--accent-glow);
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.sidebar-badge {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
background-color: var(--border);
|
|
color: var(--fg-muted);
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
4. Right Sidebar (Active Table of Contents Outline)
|
|
---------------------------------------------------------------------- */
|
|
.right-sidebar {
|
|
position: sticky;
|
|
top: 0;
|
|
height: 100vh;
|
|
padding: 48px 0;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.toc-link {
|
|
font-size: 0.85rem;
|
|
color: var(--fg-muted);
|
|
text-decoration: none;
|
|
display: block;
|
|
padding: 4px 8px;
|
|
border-left: 2px solid transparent;
|
|
transition: all 0.2s ease;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.toc-link:hover {
|
|
color: var(--fg-base);
|
|
}
|
|
.toc-link.active {
|
|
color: var(--accent);
|
|
border-left-color: var(--accent);
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
5. Scroll Progress Bar
|
|
---------------------------------------------------------------------- */
|
|
#progress-container {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 2px;
|
|
z-index: 200;
|
|
pointer-events: none;
|
|
background: transparent;
|
|
}
|
|
#progress-bar {
|
|
height: 100%;
|
|
width: 100%;
|
|
background: var(--accent);
|
|
transform-origin: 0 50%;
|
|
transform: scaleX(0);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: no-preference) {
|
|
@supports (animation-timeline: scroll()) {
|
|
#progress-bar {
|
|
animation: grow-progress auto linear;
|
|
animation-timeline: scroll(root);
|
|
}
|
|
@keyframes grow-progress {
|
|
from { transform: scaleX(0); }
|
|
to { transform: scaleX(1); }
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
6. Main Content Area & Sections
|
|
---------------------------------------------------------------------- */
|
|
.main-content {
|
|
padding: 48px 0 120px;
|
|
max-width: 820px;
|
|
width: 100%;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 56px;
|
|
}
|
|
|
|
/* Title Headings styling */
|
|
.bread-crumb {
|
|
font-size: 0.85rem;
|
|
font-family: var(--font-mono);
|
|
color: var(--fg-subtle);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
h1 {
|
|
font-family: var(--font-display);
|
|
font-weight: 800;
|
|
font-size: 2.5rem;
|
|
letter-spacing: -0.02em;
|
|
line-height: 1.2;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 1.2rem;
|
|
color: var(--fg-muted);
|
|
font-weight: 400;
|
|
margin-bottom: 32px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
h2 {
|
|
font-family: var(--font-display);
|
|
font-weight: 700;
|
|
font-size: 1.6rem;
|
|
letter-spacing: -0.015em;
|
|
margin-bottom: 24px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
h3 {
|
|
font-family: var(--font-display);
|
|
font-weight: 600;
|
|
font-size: 1.15rem;
|
|
margin: 32px 0 16px;
|
|
color: var(--fg-base);
|
|
}
|
|
|
|
p {
|
|
color: var(--fg-muted);
|
|
margin-bottom: 20px;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
/* Logical Section Card */
|
|
.section-card {
|
|
scroll-margin-top: 48px;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
7. SVG Diagram Layout
|
|
---------------------------------------------------------------------- */
|
|
.diagram-panel {
|
|
background-color: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 24px;
|
|
box-shadow: var(--shadow-sm);
|
|
margin: 24px 0;
|
|
}
|
|
|
|
.svg-container {
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
background-color: var(--svg-bg);
|
|
padding: 16px 0;
|
|
}
|
|
|
|
.diagram-svg {
|
|
display: block;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* SVG Styling variables mapping */
|
|
.svg-node-box {
|
|
fill: var(--svg-node-bg);
|
|
stroke: var(--svg-node-stroke);
|
|
stroke-width: 1.5;
|
|
}
|
|
.svg-txt-main {
|
|
fill: var(--svg-text-base);
|
|
font-family: var(--font-sans);
|
|
font-weight: 700;
|
|
}
|
|
.svg-txt-sub {
|
|
fill: var(--svg-text-muted);
|
|
font-family: var(--font-sans);
|
|
}
|
|
.svg-arrow-line {
|
|
stroke: var(--fg-muted);
|
|
stroke-width: 1.5;
|
|
fill: none;
|
|
}
|
|
.svg-arrow-line.accent {
|
|
stroke: var(--accent);
|
|
stroke-width: 2;
|
|
}
|
|
.svg-arrow-line.accent-teal {
|
|
stroke: var(--accent-teal);
|
|
stroke-width: 2;
|
|
}
|
|
|
|
@keyframes dash-animation {
|
|
to {
|
|
stroke-dashoffset: -20;
|
|
}
|
|
}
|
|
|
|
.svg-flow-path {
|
|
stroke-dasharray: 5, 5;
|
|
animation: dash-animation 1.5s linear infinite;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
8. Tables & Custom Data Grids
|
|
---------------------------------------------------------------------- */
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 20px 0 32px;
|
|
font-size: 0.92rem;
|
|
}
|
|
|
|
th, td {
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 14px 16px;
|
|
text-align: left;
|
|
vertical-align: top;
|
|
}
|
|
|
|
th {
|
|
font-family: var(--font-display);
|
|
font-weight: 600;
|
|
color: var(--fg-base);
|
|
background-color: var(--bg-sidebar);
|
|
}
|
|
|
|
tr:hover td {
|
|
background-color: var(--bg-surface-hover);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
9. Callouts & Warning Panels
|
|
---------------------------------------------------------------------- */
|
|
.callout {
|
|
border-left: 3px solid var(--border);
|
|
padding: 16px 20px;
|
|
margin: 28px 0;
|
|
border-radius: 0 8px 8px 0;
|
|
background-color: var(--bg-sidebar);
|
|
display: flex;
|
|
gap: 16px;
|
|
}
|
|
.callout-icon {
|
|
flex-shrink: 0;
|
|
color: var(--fg-muted);
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.callout-body {
|
|
flex: 1;
|
|
}
|
|
.callout-body h4 {
|
|
font-size: 0.95rem;
|
|
font-family: var(--font-display);
|
|
font-weight: 700;
|
|
margin-bottom: 6px;
|
|
}
|
|
.callout-body p {
|
|
font-size: 0.9rem;
|
|
margin-bottom: 0;
|
|
color: var(--fg-muted);
|
|
}
|
|
|
|
.callout.tip {
|
|
border-left-color: var(--accent-teal);
|
|
background-color: var(--accent-teal-glow);
|
|
}
|
|
.callout.tip .callout-icon {
|
|
color: var(--accent-teal);
|
|
}
|
|
.callout.important {
|
|
border-left-color: var(--accent);
|
|
background-color: var(--accent-glow);
|
|
}
|
|
.callout.important .callout-icon {
|
|
color: var(--accent);
|
|
}
|
|
.callout.warning {
|
|
border-left-color: var(--state-pending);
|
|
background-color: rgba(217, 119, 6, 0.05);
|
|
}
|
|
.callout.warning .callout-icon {
|
|
color: var(--state-pending);
|
|
}
|
|
.callout.danger {
|
|
border-left-color: var(--state-error);
|
|
background-color: rgba(220, 38, 38, 0.05);
|
|
}
|
|
.callout.danger .callout-icon {
|
|
color: var(--state-error);
|
|
}
|
|
|
|
/* Markdown Link Indicator Badge */
|
|
.markdown-link::after {
|
|
content: 'MD';
|
|
font-family: var(--font-sans);
|
|
font-size: 0.6rem;
|
|
font-weight: 700;
|
|
background-color: var(--border);
|
|
color: var(--fg-muted);
|
|
padding: 1px 4px;
|
|
border-radius: 4px;
|
|
margin-left: auto;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
/* Table Responsive Wrapper */
|
|
.table-wrapper {
|
|
overflow-x: auto;
|
|
margin: 24px 0;
|
|
width: 100%;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
.table-wrapper table {
|
|
margin: 0;
|
|
border: none;
|
|
}
|
|
|
|
/* Responsive Grid 2 Columns for Mobile Viewports */
|
|
@media (max-width: 768px) {
|
|
.grid-2col {
|
|
grid-template-columns: 1fr !important;
|
|
gap: 16px !important;
|
|
}
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
10. Code Blocks & Highlight Styling
|
|
---------------------------------------------------------------------- */
|
|
.code-container {
|
|
background-color: var(--code-bg);
|
|
border: 1px solid var(--code-border);
|
|
border-radius: 8px;
|
|
margin: 24px 0;
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-sm);
|
|
position: relative;
|
|
}
|
|
|
|
.code-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 8px 16px;
|
|
background-color: rgba(255, 255, 255, 0.01);
|
|
border-bottom: 1px solid var(--code-border);
|
|
font-size: 0.78rem;
|
|
color: var(--fg-subtle);
|
|
}
|
|
.code-lang-label {
|
|
font-family: var(--font-mono);
|
|
text-transform: uppercase;
|
|
font-weight: 600;
|
|
}
|
|
.copy-btn {
|
|
background: none;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
color: var(--fg-muted);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-family: var(--font-sans);
|
|
font-size: 0.75rem;
|
|
transition: all 0.2s;
|
|
}
|
|
.copy-btn:hover {
|
|
background-color: var(--bg-surface-hover);
|
|
color: var(--fg-base);
|
|
}
|
|
.copy-btn.copied {
|
|
background-color: var(--state-completed);
|
|
color: white;
|
|
border-color: var(--state-completed);
|
|
}
|
|
|
|
pre {
|
|
padding: 16px;
|
|
overflow-x: auto;
|
|
margin: 0;
|
|
}
|
|
code {
|
|
font-family: var(--font-mono);
|
|
font-size: 14px;
|
|
line-height: 1.7;
|
|
color: var(--code-fg);
|
|
}
|
|
|
|
/* Code color overrides matching Linear/Vercel standard */
|
|
pre .c { color: var(--code-comment); font-style: italic; }
|
|
pre .k { color: var(--code-keyword); font-weight: 600; }
|
|
pre .s { color: var(--code-string); }
|
|
pre .n { color: var(--code-number); }
|
|
|
|
p code, li code, td code {
|
|
background-color: var(--bg-surface-hover);
|
|
border: 1px solid var(--border);
|
|
color: var(--accent);
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
11. Lists and Checklists
|
|
---------------------------------------------------------------------- */
|
|
ul, ol {
|
|
padding-left: 24px;
|
|
margin-bottom: 20px;
|
|
color: var(--fg-muted);
|
|
}
|
|
li {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.checklist {
|
|
list-style: none;
|
|
padding-left: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
.checklist li {
|
|
position: relative;
|
|
padding-left: 28px;
|
|
margin: 0;
|
|
}
|
|
.checklist li::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
top: 4px;
|
|
width: 18px;
|
|
height: 18px;
|
|
background-color: var(--bg-base);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
}
|
|
.checklist li.ok::before {
|
|
background-color: var(--accent-teal-glow);
|
|
border-color: var(--accent-teal);
|
|
}
|
|
.checklist li.ok::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 6px;
|
|
top: 7px;
|
|
width: 4px;
|
|
height: 8px;
|
|
border: solid var(--accent-teal);
|
|
border-width: 0 2px 2px 0;
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
12. Search Box
|
|
---------------------------------------------------------------------- */
|
|
.search-box-container {
|
|
position: relative;
|
|
}
|
|
.search-box-input {
|
|
width: 100%;
|
|
background-color: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 10px 14px 10px 38px;
|
|
color: var(--fg-base);
|
|
font-family: var(--font-sans);
|
|
font-size: 0.88rem;
|
|
outline: none;
|
|
transition: all 0.2s;
|
|
}
|
|
.search-box-input:focus {
|
|
border-color: var(--border-focus);
|
|
}
|
|
.search-box-container svg {
|
|
position: absolute;
|
|
left: 14px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--fg-subtle);
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
13. Navigation Prev / Next Cards at footer
|
|
---------------------------------------------------------------------- */
|
|
.nav-footer-cards {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
margin-top: 48px;
|
|
padding-top: 32px;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
.nav-card {
|
|
background-color: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
text-decoration: none;
|
|
color: var(--fg-base);
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.nav-card:hover {
|
|
border-color: var(--accent);
|
|
background-color: var(--bg-surface-hover);
|
|
transform: translateY(-2px);
|
|
}
|
|
.nav-card-label {
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--fg-subtle);
|
|
margin-bottom: 4px;
|
|
}
|
|
.nav-card-title {
|
|
font-family: var(--font-display);
|
|
font-weight: 600;
|
|
font-size: 0.95rem;
|
|
}
|
|
.nav-card.prev {
|
|
align-items: flex-start;
|
|
}
|
|
.nav-card.next {
|
|
align-items: flex-end;
|
|
text-align: right;
|
|
}
|
|
|
|
.back-to-top {
|
|
text-align: center;
|
|
margin-top: 24px;
|
|
}
|
|
.back-to-top-link {
|
|
color: var(--fg-subtle);
|
|
text-decoration: none;
|
|
font-size: 0.88rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.back-to-top-link:hover {
|
|
color: var(--fg-base);
|
|
}
|
|
|
|
/* Footer details */
|
|
footer {
|
|
padding-top: 32px;
|
|
margin-top: 32px;
|
|
border-top: 1px solid var(--border);
|
|
color: var(--fg-subtle);
|
|
font-size: 0.82rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
14. Responsive Adaptation (Mobile / Tablet)
|
|
---------------------------------------------------------------------- */
|
|
@media (max-width: 1200px) {
|
|
.app-container {
|
|
grid-template-columns: 260px minmax(0, 1fr);
|
|
}
|
|
.right-sidebar {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.app-container {
|
|
display: block;
|
|
padding: 0 20px;
|
|
}
|
|
.left-sidebar {
|
|
display: none;
|
|
}
|
|
.main-content {
|
|
padding: 32px 0 80px;
|
|
}
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
15. Print Optimization Styling
|
|
---------------------------------------------------------------------- */
|
|
@media print {
|
|
body {
|
|
background-color: #ffffff !important;
|
|
color: #000000 !important;
|
|
display: block !important;
|
|
}
|
|
.left-sidebar, .right-sidebar, #progress-container, .code-header, .copy-btn, .nav-footer-cards, .back-to-top {
|
|
display: none !important;
|
|
}
|
|
.app-container {
|
|
display: block !important;
|
|
padding: 0 !important;
|
|
}
|
|
.main-content {
|
|
margin-left: 0 !important;
|
|
padding: 0 !important;
|
|
max-width: 100% !important;
|
|
}
|
|
.section-card {
|
|
page-break-inside: avoid;
|
|
margin-bottom: 3rem;
|
|
}
|
|
pre, .diagram-panel {
|
|
page-break-inside: avoid;
|
|
}
|
|
}
|
|
|
|
/* Animation helper */
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(4px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Scroll Progress tracker -->
|
|
<div id="progress-container" aria-hidden="true">
|
|
<div id="progress-bar"></div>
|
|
</div>
|
|
|
|
<!-- App Wrapper Grid -->
|
|
<div class="app-container">
|
|
|
|
<!-- 1. Left Sidebar Navigation -->
|
|
<aside class="left-sidebar">
|
|
<a href="./USER_MANUAL.html" class="logo-container">
|
|
<svg width="22" height="22" viewBox="0 0 24 24" class="logo-symbol">
|
|
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
<span>Hermes Skill</span>
|
|
</a>
|
|
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-title">메인 가이드</div>
|
|
<ul class="sidebar-links">
|
|
<li><a href="./USER_MANUAL.html" class="sidebar-link"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg> USER_MANUAL.html <span class="sidebar-badge">사용 설명서</span></a></li>
|
|
<li><a href="./SKILL.md" class="sidebar-link markdown-link"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg> SKILL.md <span class="sidebar-badge">코드 가이드</span></a></li>
|
|
<li><a href="./README.md" class="sidebar-link markdown-link"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg> README.md <span class="sidebar-badge">스킬 요약</span></a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-title">참조 스펙 명세 (References)</div>
|
|
<ul class="sidebar-links">
|
|
<li><a href="./job-protocol.md" class="sidebar-link markdown-link"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg> job-protocol.md</a></li>
|
|
<li><a href="./registry.md" class="sidebar-link markdown-link"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg> registry.md</a></li>
|
|
<li><a href="./mqtt-broker-setup.html" class="sidebar-link active"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg> mqtt-broker-setup.html</a></li>
|
|
</ul>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- 2. Middle Main Column Reading Pane -->
|
|
<main class="main-content">
|
|
|
|
<!-- Title Header -->
|
|
<header>
|
|
<div class="bread-crumb">Hermes / References / mqtt-broker-setup</div>
|
|
<h1>MQTT Broker Setup Guide</h1>
|
|
<p class="subtitle">공개 PoC 브로커에서 내부 TLS 및 ACL 통제 브로커로의 컷오버 셋업 가이드</p>
|
|
</header>
|
|
|
|
<!-- Section 1: PoC vs Production broker -->
|
|
<section id="poc-vs-prod" class="section-card">
|
|
<h2>1. PoC vs Production broker</h2>
|
|
<p>자율 에이전트 위임 스킬(<code>delegate-job</code>)의 모든 연결 설정은 환경변수 및 레지스트리 레코드의 <code>broker</code> 블록 정보로부터 유도됩니다. <strong>코드를 일체 변경하지 않고 설정 조율만으로 자체 사내망 브로커로 안전하게 컷오버(Cut-over)</strong>할 수 있도록 추상화되어 있습니다.</p>
|
|
|
|
<div class="diagram-panel">
|
|
<div class="svg-container">
|
|
<svg class="diagram-svg" viewBox="0 0 850 250" width="100%" height="auto" xmlns="http://www.w3.org/2000/svg">
|
|
<rect width="850" height="250" fill="none" />
|
|
<path d="M 0,0 L 850,0 M 0,50 L 850,50 M 0,100 L 850,100 M 0,150 L 850,150 M 0,200 L 850,200 M 0,0 L 0,250 M 50,0 L 50,250 M 100,0 L 100,250 M 150,0 L 150,250 M 200,0 L 200,250 M 250,0 L 250,250 M 300,0 L 300,250 M 350,0 L 350,250 M 400,0 L 400,250 M 450,0 L 450,250 M 500,0 L 500,250 M 550,0 L 550,250 M 600,0 L 600,250 M 650,0 L 650,250 M 700,0 L 700,250 M 750,0 L 750,250 M 800,0 L 800,250" stroke="var(--border)" stroke-width="0.3" opacity="0.5" />
|
|
<defs>
|
|
<marker id="arrow-setup" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="var(--fg-subtle)" />
|
|
</marker>
|
|
<marker id="arrow-setup-indigo" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#6366f1" />
|
|
</marker>
|
|
<marker id="arrow-setup-teal" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#14b8a6" />
|
|
</marker>
|
|
<linearGradient id="grad-poc" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" stop-color="#94a3b8" />
|
|
<stop offset="100%" stop-color="#475569" />
|
|
</linearGradient>
|
|
<linearGradient id="grad-prod" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" stop-color="#818cf8" />
|
|
<stop offset="100%" stop-color="#4f46e5" />
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
<rect x="20" y="30" width="380" height="190" rx="8" fill="none" stroke="var(--border)" stroke-width="1.5" stroke-dasharray="4 4" />
|
|
<text x="35" y="48" fill="var(--fg-subtle)" font-size="10" font-weight="700" font-family="var(--font-sans)">1. PoC (TEST ENVIRONMENT)</text>
|
|
|
|
<rect x="40" y="70" width="130" height="40" rx="6" class="svg-node-box" />
|
|
<text x="52" y="94" class="svg-txt-main" font-size="10">Publisher (Worker)</text>
|
|
<rect x="40" y="150" width="130" height="40" rx="6" class="svg-node-box" />
|
|
<text x="52" y="174" class="svg-txt-main" font-size="10">Subscriber (Hermes)</text>
|
|
|
|
<rect x="250" y="90" width="130" height="80" rx="8" class="svg-node-box" />
|
|
<rect x="250" y="90" width="4" height="80" rx="1" fill="url(#grad-poc)" />
|
|
<text x="262" y="112" class="svg-txt-main" font-size="11">broker.hivemq.com</text>
|
|
<text x="262" y="128" class="svg-txt-sub" font-size="8">Port: 1883 (Plaintext)</text>
|
|
<text x="262" y="142" class="svg-txt-sub" font-size="8">No Auth, Public Access</text>
|
|
|
|
<path d="M 170,90 L 210,90 L 210,110 L 250,110" class="svg-arrow-line" marker-end="url(#arrow-setup)" />
|
|
<path d="M 250,150 L 210,150 L 210,170 L 170,170" class="svg-arrow-line" marker-end="url(#arrow-setup)" />
|
|
|
|
<rect x="420" y="30" width="410" height="190" rx="8" fill="none" stroke="var(--border)" stroke-width="1.5" />
|
|
<text x="435" y="48" fill="var(--fg-subtle)" font-size="10" font-weight="700" font-family="var(--font-sans)">2. PRODUCTION (SECURE INTRANET)</text>
|
|
|
|
<rect x="440" y="70" width="130" height="40" rx="6" class="svg-node-box" />
|
|
<text x="452" y="94" class="svg-txt-main" font-size="10">Claude Worker</text>
|
|
<rect x="440" y="150" width="130" height="40" rx="6" class="svg-node-box" />
|
|
<text x="452" y="174" class="svg-txt-main" font-size="10">Hermes Client</text>
|
|
|
|
<rect x="660" y="80" width="155" height="100" rx="8" class="svg-node-box" style="stroke: var(--accent)" />
|
|
<rect x="660" y="80" width="4" height="100" rx="1" fill="url(#grad-prod)" />
|
|
<text x="672" y="105" class="svg-txt-main" font-size="11">Mosquitto / EMQX</text>
|
|
<text x="672" y="122" class="svg-txt-sub" font-size="8" fill="var(--accent)" font-weight="600">Port: 8883 (TLS Encrypted)</text>
|
|
<text x="672" y="136" class="svg-txt-sub" font-size="8">ACL Rules & Password Auth</text>
|
|
<text x="672" y="150" class="svg-txt-sub" font-size="8">Bearer auth_token Verification</text>
|
|
|
|
<path d="M 570,90 L 610,90 L 610,110 L 660,110" class="svg-arrow-line accent svg-flow-path" marker-end="url(#arrow-setup-indigo)" />
|
|
<path d="M 660,150 L 610,150 L 610,170 L 570,170" class="svg-arrow-line accent-teal svg-flow-path" marker-end="url(#arrow-setup-teal)" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid-2col" style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;">
|
|
<div style="background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: 8px; padding: 16px;">
|
|
<h4 style="font-family: var(--font-display); font-weight: 700; margin-bottom: 8px; color: var(--fg-base);">PoC 환경 (Public HiveMQ)</h4>
|
|
<p style="font-size: 0.88rem; line-height: 1.5; margin-bottom: 0; color: var(--fg-muted);">
|
|
<strong>장점</strong>: 제로 셋업, 외부 접근성 완비, 빠른 프로토타입 작성 및 timeout/state-machine 로직 결선용에 최적화.<br><br>
|
|
<strong>단점 및 수용 제약</strong>: 암호화 전송 없음(Plaintext), 임의의 제3자가 동일 토픽 메시지를 엿보거나 주입 가능, 민감 정보(패스워드, 소스 경로 등) 적재 절대 금지. 또한 비영속(Non-retained) 모드 구동 시 신호 수신기(Subscriber) 기동 전에 발행된 이벤트 및 재구독(Re-subscribing) 클라이언트의 과거 이벤트 유실을 수용해야 함.
|
|
</p>
|
|
</div>
|
|
<div style="background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: 8px; padding: 16px;">
|
|
<h4 style="font-family: var(--font-display); font-weight: 700; margin-bottom: 8px; color: var(--accent);">Production 환경 (사설 Mosquitto)</h4>
|
|
<p style="font-size: 0.88rem; line-height: 1.5; margin-bottom: 0; color: var(--fg-muted);">
|
|
<strong>장점</strong>: TLS 전송 암호화, ID/Password 기반 클라이언트 인증, 토픽 레벨의 세분화된 ACL 인가 통제(Publish/Subscribe 권한 분리), 영속성 보장(QoS 1 + Retained).<br><br>
|
|
<strong>단점 및 관리 요소</strong>: 전용 사설 인프라 및 자체 CA/인증서 키 체인 수명 관리 셋업 비용 발생.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Section 2: 환경변수로 broker config -->
|
|
<section id="env-config" class="section-card">
|
|
<h2>2. 환경변수로 broker config</h2>
|
|
<p>공통 모듈 <code>mqtt_common.py</code>는 다음 환경변수를 우선 로드하여 접속 설정을 결정합니다. 코드를 일체 변경하지 않고 매개변수 설정만으로도 브로커를 즉각 대체할 수 있도록 추상화되어 있습니다.</p>
|
|
|
|
<div class="table-wrapper">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width:200px">환경변수</th>
|
|
<th>의미</th>
|
|
<th>PoC 기본 설정값</th>
|
|
<th>운영(Production) 구성 예시</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>MQTT_BROKER</code></td>
|
|
<td>브로커 서버 IP / 도메인 호스트명</td>
|
|
<td><code>"broker.hivemq.com"</code></td>
|
|
<td><code>"mqtt.internal.example.com"</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>MQTT_PORT</code></td>
|
|
<td>브로커 접속 포트 번호</td>
|
|
<td><code>1883</code> (Plaintext TCP)</td>
|
|
<td><code>8883</code> (TLS TCP)</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>MQTT_TLS</code></td>
|
|
<td>TLS 암호화 채널 기동 옵션 (<code>1</code>/<code>0</code>)</td>
|
|
<td><code>0</code> (미기동)</td>
|
|
<td><code>1</code> (기동 활성화)</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>MQTT_USERNAME</code></td>
|
|
<td>접속 인증 계정 사용자명</td>
|
|
<td><code>None</code></td>
|
|
<td><code>"hermes-delegator"</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>MQTT_PASSWORD</code></td>
|
|
<td>접속 인증 계정 비밀번호</td>
|
|
<td><code>None</code></td>
|
|
<td><code>"secure_broker_password"</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>MQTT_CA_CERTS</code></td>
|
|
<td>검증용 사설 CA 인증서 묶음 파일 경로</td>
|
|
<td><code>None</code></td>
|
|
<td><code>"/etc/ssl/certs/internal-ca.pem"</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>MQTT_CERTFILE</code></td>
|
|
<td>mTLS 대응용 클라이언트 공개 인증서 경로</td>
|
|
<td><code>None</code></td>
|
|
<td><code>"/path/to/client.crt"</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>MQTT_KEYFILE</code></td>
|
|
<td>mTLS 대응용 클라이언트 개인 키 경로</td>
|
|
<td><code>None</code></td>
|
|
<td><code>"/path/to/client.key"</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>MQTT_CLIENT_ID_PREFIX</code></td>
|
|
<td>식별 접두사 (충돌 방지용 임의 접미사 결합)</td>
|
|
<td><code>"hermes"</code></td>
|
|
<td><code>"hermes-prod"</code></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Section 3: 운영 broker 설정 체크리스트 -->
|
|
<section id="broker-setup" class="section-card">
|
|
<h2>3. 운영 broker 설정 체크리스트 (Mosquitto/EMQX)</h2>
|
|
<p>영속성 구성 및 TLS 암호화 채널, ACL 인가 제어를 위해 구성 정보를 셋업합니다. 다음 체크리스트를 준수하십시오.</p>
|
|
|
|
<ul class="checklist" style="margin-bottom: 24px;">
|
|
<li class="ok"><strong>영속성(Persistence) 보장</strong>: 브로커 재기동 시에도 이전 retained 메시지가 유지되도록 설정</li>
|
|
<li class="ok"><strong>익명 접속 금지</strong>: <code>allow_anonymous false</code> 설정으로 허가된 계정만 연결 허용</li>
|
|
<li class="ok"><strong>전용 포트 매핑</strong>: TLS 전용 <code>8883</code> 포트 리스너 바인딩</li>
|
|
<li class="ok"><strong>인증 및 인가 분리</strong>: 비밀번호 파일 및 ACL 규칙 파일 경로 정의</li>
|
|
</ul>
|
|
|
|
<h3>3.1 Mosquitto 패키지 설치</h3>
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">shell</span>
|
|
<button class="copy-btn" onclick="copyCodeText(this)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
<span>복사</span>
|
|
</button>
|
|
</div>
|
|
<pre><code><span class="c"># macOS 환경인 경우 Homebrew로 설치</span>
|
|
brew install mosquitto
|
|
|
|
<span class="c"># Debian / Ubuntu 리눅스 환경인 경우</span>
|
|
sudo apt-get update && sudo apt-get install -y mosquitto mosquitto-clients
|
|
|
|
<span class="c"># Docker 컨테이너 가상 머신으로 가동하는 경우</span>
|
|
docker run -d --name mosquitto -p 8883:8883 \
|
|
-v "$PWD/mosquitto.conf:/mosquitto/config/mosquitto.conf" \
|
|
-v "$PWD/certs:/mosquitto/certs" \
|
|
-v "$PWD/auth:/mosquitto/auth" \
|
|
eclipse-mosquitto:2</code></pre>
|
|
</div>
|
|
|
|
<h3>3.2 `mosquitto.conf` 주요 설정 명세</h3>
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">conf</span>
|
|
<button class="copy-btn" onclick="copyCodeText(this)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
<span>복사</span>
|
|
</button>
|
|
</div>
|
|
<pre><code>persistence true
|
|
persistence_location /mosquitto/data/
|
|
|
|
password_file /mosquitto/auth/passwd
|
|
acl_file /mosquitto/auth/acl
|
|
allow_anonymous false
|
|
|
|
listener 8883
|
|
cafile /mosquitto/certs/ca.crt
|
|
certfile /mosquitto/certs/server.crt
|
|
keyfile /mosquitto/certs/server.key</code></pre>
|
|
</div>
|
|
|
|
<h3>3.3 사용자 크레덴셜 생성 (`mosquitto_passwd`)</h3>
|
|
<p>인증된 클라이언트만 통신할 수 있도록 사용자 해시 패스워드 파일을 생성합니다.</p>
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">shell</span>
|
|
<button class="copy-btn" onclick="copyCodeText(this)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
<span>복사</span>
|
|
</button>
|
|
</div>
|
|
<pre><code><span class="c"># 최초 사용자(hermes) 등록 (-c 옵션은 기존 파일 초기화 및 재생성하므로 최초 1회만 사용)</span>
|
|
mosquitto_passwd -c /mosquitto/auth/passwd hermes
|
|
|
|
<span class="c"># 추가 에이전트 전용 계정 등록 (-c를 붙이면 기존 패스워드 목록이 휘발되므로 제외)</span>
|
|
mosquitto_passwd /mosquitto/auth/passwd claude-worker</code></pre>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Section 4: TLS / 인증서 설정 -->
|
|
<section id="tls-setup" class="section-card">
|
|
<h2>4. TLS / 인증서 설정</h2>
|
|
<p>전송 구간 암호화 및 도청 방지를 위해 TLS 인증체계를 갖춥니다. 사설 환경 구축 시 다음 명령어를 참조하여 인증서 세트를 준비하십시오.</p>
|
|
|
|
<h3>4.1 간이 검증용 자체 서명 인증서 세트 (단일 호스트 테스트용)</h3>
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">shell</span>
|
|
<button class="copy-btn" onclick="copyCodeText(this)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
<span>복사</span>
|
|
</button>
|
|
</div>
|
|
<pre><code>mkdir -p certs && cd certs
|
|
openssl req -x509 -newkey rsa:2048 -nodes -days 825 \
|
|
-keyout server.key -out server.crt \
|
|
-subj "/CN=mqtt.internal"
|
|
cp server.crt ca.crt <span class="c"># 클라이언트는 동일 인증서를 루트 CA 신뢰용 인증서로 복사하여 참조</span></code></pre>
|
|
</div>
|
|
|
|
<h3>4.2 사내 전용 루트 CA 기반 발급 구조 (권장 체계)</h3>
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">shell</span>
|
|
<button class="copy-btn" onclick="copyCodeText(this)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
<span>복사</span>
|
|
</button>
|
|
</div>
|
|
<pre><code><span class="c"># 1) 사내 최상위 루트 CA 키 및 인증서 생성 (10년 보존 기한)</span>
|
|
openssl genrsa -out ca.key 4096
|
|
openssl req -x509 -new -nodes -key ca.key -days 3650 -out ca.crt -subj "/CN=Hermes-CA"
|
|
|
|
<span class="c"># 2) 브로커용 개인키 및 CSR 발급 신청 파일 생성</span>
|
|
openssl genrsa -out server.key 2048
|
|
openssl req -new -key server.key -out server.csr -subj "/CN=mqtt.internal"
|
|
|
|
<span class="c"># 3) 루트 CA 개인키 서명을 통해 최종 브로커용 인증서 생성 (825일 유효)</span>
|
|
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
|
|
-out server.crt -days 825</code></pre>
|
|
</div>
|
|
<p style="margin-top: 12px; font-size: 0.9rem;">클라이언트는 환경변수 <code>MQTT_CA_CERTS=/path/to/ca.crt</code>를 설정함으로써 해당 사설 CA를 신뢰하게 됩니다.</p>
|
|
</section>
|
|
|
|
<!-- Section 5: ACL Policies -->
|
|
<section id="acl-policy" class="section-card">
|
|
<h2>5. ACL (claude worker / Hermes reader 권한 분리)</h2>
|
|
<p>최소 권한의 법칙(Least Privilege)에 입각하여 퍼블리셔(에이전트 Worker)와 서브스크라이버(Hermes 모니터)의 토픽 읽기/쓰기 권한을 명확히 분리합니다. 이를 통해 타사 또는 다른 권한 영역의 에이전트가 허가되지 않은 작업 이벤트를 스푸핑하거나 발행하는 것을 완벽히 방지합니다.</p>
|
|
|
|
<div class="diagram-panel" style="margin: 20px 0;">
|
|
<div class="svg-container">
|
|
<svg viewBox="0 0 850 180" width="100%" height="auto" xmlns="http://www.w3.org/2000/svg">
|
|
<rect width="850" height="180" fill="none" />
|
|
<path d="M 0,0 L 850,0 M 0,30 L 850,30 M 0,60 L 850,60 M 0,90 L 850,90 M 0,120 L 850,120 M 0,150 L 850,150 M 0,0 L 0,180 M 50,0 L 50,180 M 100,0 L 100,180 M 150,0 L 150,180 M 200,0 L 200,180 M 250,0 L 250,180 M 300,0 L 300,180 M 350,0 L 350,180 M 400,0 L 400,180 M 450,0 L 450,180 M 500,0 L 500,180 M 550,0 L 550,180 M 600,0 L 600,180 M 650,0 L 650,180 M 700,0 L 700,180 M 750,0 L 750,180 M 800,0 L 800,180" stroke="var(--border)" stroke-width="0.3" opacity="0.5" />
|
|
|
|
<defs>
|
|
<marker id="arrow-acl" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="var(--fg-subtle)" />
|
|
</marker>
|
|
<marker id="arrow-acl-green" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#059669" />
|
|
</marker>
|
|
<marker id="arrow-acl-red" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#dc2626" />
|
|
</marker>
|
|
</defs>
|
|
|
|
<rect x="30" y="30" width="160" height="50" rx="6" class="svg-node-box" />
|
|
<text x="45" y="55" class="svg-txt-main" font-size="11" font-weight="600">user: claude-worker</text>
|
|
<text x="45" y="70" class="svg-txt-sub" font-size="8">Agent Publisher</text>
|
|
|
|
<rect x="320" y="50" width="220" height="80" rx="8" class="svg-node-box" />
|
|
<text x="335" y="75" class="svg-txt-main" font-size="11">topic: python/mqtt/jobs/+/events</text>
|
|
<text x="335" y="95" class="svg-txt-sub" font-size="9" fill="var(--accent)" font-weight="600">ACL Rules Enforcement</text>
|
|
<text x="335" y="112" class="svg-txt-sub" font-size="8">Wildcard '+' maps to Job ID</text>
|
|
|
|
<rect x="650" y="100" width="160" height="50" rx="6" class="svg-node-box" />
|
|
<text x="665" y="125" class="svg-txt-main" font-size="11" font-weight="600">user: hermes</text>
|
|
<text x="665" y="140" class="svg-txt-sub" font-size="8">Observer/Subscriber</text>
|
|
|
|
<path d="M 190,55 L 255,55 L 255,80 L 320,80" class="svg-arrow-line" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow-acl-green)" />
|
|
<text x="200" y="48" fill="#059669" font-size="9" font-weight="600">topic write [OK]</text>
|
|
|
|
<path d="M 320,100 L 255,100 L 255,120 L 190,120" class="svg-arrow-line" stroke="#dc2626" stroke-width="1.5" marker-end="url(#arrow-acl-red)" />
|
|
<text x="200" y="140" fill="#dc2626" font-size="9" font-weight="600">topic read [DENIED]</text>
|
|
|
|
<path d="M 540,90 L 595,90 L 595,125 L 650,125" class="svg-arrow-line" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow-acl-green)" />
|
|
<text x="560" y="82" fill="#059669" font-size="9" font-weight="600">topic read [OK]</text>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>5.1 ACL 규칙 파일 명세 (`/mosquitto/auth/acl`)</h3>
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">acl</span>
|
|
<button class="copy-btn" onclick="copyCodeText(this)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
<span>복사</span>
|
|
</button>
|
|
</div>
|
|
<pre><code><span class="c"># claude-worker: 지정한 JID의 하위 이벤트 토픽에 메시지를 '발행'할 권한만 소유</span>
|
|
user claude-worker
|
|
topic write python/mqtt/jobs/+/events
|
|
|
|
<span class="c"># hermes: 모든 작업 식별자 토픽으로부터 이벤트를 '구독'할 권한만 소유</span>
|
|
user hermes
|
|
topic read python/mqtt/jobs/+/events
|
|
|
|
<span class="c"># 호환성 검증용 샘플 토픽 권한 공유</span>
|
|
pattern readwrite python/mqtt/sample</code></pre>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Section 6: Cut-over 절차 + precedence 함정 + 검증 -->
|
|
<section id="cutover-verify" class="section-card">
|
|
<h2>6. Cut-over 절차 + precedence 함정 + 검증</h2>
|
|
<p>환경 구성 이관 준비가 완료되었다면, 오직 주입 설정 정보만으로 올바르게 컷오버가 체결되는지 점검합니다.</p>
|
|
|
|
<div class="callout danger" style="margin-bottom: 24px;">
|
|
<div class="callout-icon">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
|
</div>
|
|
<div class="callout-body">
|
|
<h4>우선순위 역전 장애 (Broker Precedence Pitfall) 주의</h4>
|
|
<p>
|
|
공통 모듈 <code>broker_config_from_job()</code>는 환경 변수보다 <strong>Job 레지스트리 레코드 내부의 <code>broker.*</code> 설정값</strong>을 최종 우선(Overriding)하여 병합합니다.<br><br>
|
|
쉘 터미널에 신규 프로덕션 브로커 환경변수(<code>MQTT_BROKER=mqtt.internal</code> 등)를 정상 주입하였더라도, <strong>과거에 이미 생성되어 대기 중이던 레지스트리 JSON 파일</strong>이 있다면 기존 PoC 주소(<code>broker.hivemq.com</code>)가 우선 적용되게 됩니다.
|
|
반드시 신규 작업을 다시 등록(register)하거나, 레코드 JSON 파일(<code>.hermes/jobs/<id>.json</code>)을 수동 편집하여 <code>broker.host</code> 항목을 수동 업데이트한 후 실행하십시오.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>6.1 컷오버 통합 검증 명령어 시나리오</h3>
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">bash</span>
|
|
<button class="copy-btn" onclick="copyCodeText(this)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
<span>복사</span>
|
|
</button>
|
|
</div>
|
|
<pre><code><span class="c"># 1) 쉘 환경에 신규 브로커 접속 매개변수 바인딩</span>
|
|
export MQTT_BROKER=mqtt.internal
|
|
export MQTT_PORT=8883
|
|
export MQTT_TLS=1
|
|
export MQTT_CA_CERTS=$PWD/certs/ca.crt
|
|
export MQTT_USERNAME=hermes
|
|
export MQTT_PASSWORD=secure_hermes_password
|
|
|
|
<span class="c"># 2) mosquitto CLI 유틸리티를 활용한 선 구독 점검</span>
|
|
mosquitto_sub -h "$MQTT_BROKER" -p 8883 --cafile "$MQTT_CA_CERTS" \
|
|
-u hermes -P "$MQTT_PASSWORD" -t 'python/mqtt/jobs/+/events' -v &
|
|
|
|
<span class="c"># 3) 작업 위임 스크립트를 변경 없이 실행</span>
|
|
PY=.venv/bin/python
|
|
JID=$($PY scripts/registry.py register --prompt "broker cutover smoke test")
|
|
$PY scripts/job_subscriber.py --job "$JID" --timeout 30 &
|
|
sleep 3
|
|
$PY scripts/publish_event.py --job "$JID" --event started
|
|
$PY scripts/publish_event.py --job "$JID" --event completed</code></pre>
|
|
</div>
|
|
|
|
<h3>6.2 성공 판정 체크리스트</h3>
|
|
<ul class="checklist">
|
|
<li class="ok"><code>job_subscriber.py</code>가 <code>completed</code> 최종 수신 감지 즉시 exit code 0으로 반환 종료</li>
|
|
<li class="ok"><code>mosquitto_sub</code> 백그라운드 수신 터미널 창에 발행된 JSON 이벤트 페이로드가 정상 검출됨</li>
|
|
<li class="ok">인가되지 않은(허가 받지 않은) 크레덴셜 계정으로 publish 시도 시 브로커 ACL에 의해 즉각 거절 차단</li>
|
|
<li class="ok">작업 종료 시그널 발송 완료 후 늦게 접속한(late joined) 신규 구독자도 retained terminal 이벤트 정상 수신</li>
|
|
</ul>
|
|
|
|
<p style="margin-top: 20px;">
|
|
검증이 완료되면, <code>publish_event.py</code> 및 에이전트 구동 시 로컬 Registry 정보를 통해 신규 브로커 정보로 자동 접속할 수 있도록 각 작업 레코드 JSON의 <code>broker</code> 블록을 다음과 같이 지속(Persist) 정의해 주십시오.
|
|
</p>
|
|
|
|
<div class="code-container">
|
|
<div class="code-header">
|
|
<span class="code-lang-label">json</span>
|
|
<button class="copy-btn" onclick="copyCodeText(this)">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
|
<span>복사</span>
|
|
</button>
|
|
</div>
|
|
<pre><code>"broker": {
|
|
"host": "mqtt.internal",
|
|
"port": 8883,
|
|
"tls": true,
|
|
"username": "claude-worker",
|
|
"password": "…"
|
|
}</code></pre>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Bottom Nav Cards -->
|
|
<nav class="nav-footer-cards" aria-label="Documentation navigation">
|
|
<a href="./USER_MANUAL.html" class="nav-card prev">
|
|
<span class="nav-card-label">이전 문서</span>
|
|
<span class="nav-card-title">← USER_MANUAL.html 사용 설명서</span>
|
|
</a>
|
|
<a href="./registry.md" class="nav-card next">
|
|
<span class="nav-card-label">다음 문서</span>
|
|
<span class="nav-card-title">Registry 데이터 사양 명세 →</span>
|
|
</a>
|
|
</nav>
|
|
|
|
<div class="back-to-top">
|
|
<a href="#" class="back-to-top-link">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"></polyline></svg>
|
|
맨 위로 이동
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<footer>
|
|
<div>최종 검증 완료일: 2026-06-19</div>
|
|
<div>작업 사양: PoC to Production Cut-over</div>
|
|
<div>인프라: Eclipse Mosquitto v2.x</div>
|
|
</footer>
|
|
|
|
</main>
|
|
|
|
<!-- 3. Right Sidebar outlining document layout -->
|
|
<aside class="right-sidebar">
|
|
<div class="sidebar-title" style="margin-bottom: 12px">본문 구성</div>
|
|
<nav aria-label="Table of contents">
|
|
<a href="#poc-vs-prod" class="toc-link">01. PoC vs Production broker</a>
|
|
<a href="#env-config" class="toc-link">02. 환경변수로 broker config</a>
|
|
<a href="#broker-setup" class="toc-link">03. 운영 broker 설정 체크리스트</a>
|
|
<a href="#tls-setup" class="toc-link">04. TLS / 인증서 설정</a>
|
|
<a href="#acl-policy" class="toc-link">05. ACL 권한 분리 정책</a>
|
|
<a href="#cutover-verify" class="toc-link">06. Cut-over 절차 & 검증</a>
|
|
</nav>
|
|
</aside>
|
|
|
|
</div>
|
|
|
|
<!-- Interactive JavaScript Logic -->
|
|
<script>
|
|
// 1. Clipboard Copy button triggers
|
|
function copyCodeText(btn) {
|
|
const container = btn.closest('.code-container');
|
|
const codeElement = container.querySelector('pre code');
|
|
navigator.clipboard.writeText(codeElement.innerText).then(() => {
|
|
const textSpan = btn.querySelector('span');
|
|
const originalText = textSpan.innerText;
|
|
btn.classList.add('copied');
|
|
textSpan.innerText = '복사됨!';
|
|
setTimeout(() => {
|
|
btn.classList.remove('copied');
|
|
textSpan.innerText = originalText;
|
|
}, 1500);
|
|
});
|
|
}
|
|
|
|
// 2. Scroll spy outline navigation highlighter
|
|
const observerOptions = {
|
|
root: null,
|
|
rootMargin: '0px 0px -60% 0px',
|
|
threshold: 0
|
|
};
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const id = entry.target.getAttribute('id');
|
|
document.querySelectorAll('.toc-link').forEach(link => {
|
|
if (link.getAttribute('href') === `#${id}`) {
|
|
link.classList.add('active');
|
|
} else {
|
|
link.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}, observerOptions);
|
|
|
|
document.querySelectorAll('.section-card').forEach(sec => {
|
|
observer.observe(sec);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|