자체 도메인과 퍼스트 파티 프록시
웹사이트의 서브도메인으로 위젯 파일, 채팅 연결, 오프라인 양식과 Smart Answer를 제공하는 방법: 프록시 키, 실제 방문자 주소, nginx·Apache·Caddy 템플릿.
자체 도메인을 사용하면 채팅이 chat.your-company.com 같은 웹사이트의 서브도메인을 통해 실행됩니다. 서버는 위젯의 요청을 받아 리버스 프록시로 reactive.chat에 전달합니다. 이 페이지는 해당 서버를 운영하는 사람을 위한 안내입니다. 편집자용 간단한 안내는 자체 도메인(퍼스트 파티)에 있습니다.
서브도메인을 통해 실행되는 항목
- 위젯 파일 –
loader.js, 이후 로드되는 코어(core.<hash>.js)와 기타 모듈입니다. - 채팅 연결 –
/rc_wss/의 WebSocket,/rc_wss/h/의 HTTP 대체 연결,/rc_wss/upload/의 파일 업로드입니다. - 오프라인 양식 – 채팅 연결이 성립하지 않을 때의
POST /api/offline/<SITE_KEY>입니다. - Smart Answer – 웹사이트 검색의 AI 답변을 사용하는 경우
/api/suche/입니다.
얻는 이점:
- 타사 도메인으로 요청하지 않습니다. 이 경로에서 방문자의 브라우저는 운영자의 서브도메인과만 통신합니다.
- Content Security Policy에 주소 하나를 사용합니다.
script-src와connect-src에 reactive.chat 주소 대신 서브도메인을 넣습니다(자세한 내용은 보안과 CSP). - 실제 방문자 주소가 전달됩니다. 프록시가 비밀 키로 보호되는 전용 헤더로 주소를 알립니다. 빈도 제한과 차단은 프록시가 아닌 방문자별로 집계됩니다.
자체 도메인이 의미하지 않는 사항:
- 자체 호스팅은 아닙니다. 대화, 첨부 파일과 설정은 계속 reactive.chat이 처리하고 저장합니다.
- 동의 여부를 판단하지 않습니다. 자체 도메인을 사용해도 데이터 처리 주체는 바뀌지 않습니다.
- 프록시에서 트래픽을 볼 수 있습니다. TLS는 프록시에서 종료됩니다. 요청 본문을 기록하지 않고
/rc_wss/h/를 접근 로그에서 제외합니다. 이 요청은 쿼리에 단기 연결 토큰을 포함합니다.
요구 사항
- HTTPS 443 포트로 공개 접근할 수 있고 외부 HTTPS 연결을 열 수 있는 서버 또는 리버스 프록시입니다. nginx(ssl 모듈 포함), Apache 2.4.47 이상, Caddy 2용 템플릿을 제공합니다.
- Apache에는
mod_ssl,mod_proxy,mod_proxy_http,mod_proxy_wstunnel,mod_headers,mod_setenvif모듈이 필요합니다. - 채팅 전용 서브도메인. 채팅은 호스트 전체를 사용하므로
www.your-company.com/chat/같은 경로는 작동하지 않습니다. 이름만 입력하고https://, 포트와 경로는 넣지 않습니다. IP 주소,.local,.lan,.internal같은 내부 도메인,reactive.chat하위 이름은 허용하지 않습니다. - 웹사이트와 같은 도메인. 삽입된 웹사이트의 도메인에 속한 서브도메인일 때만 퍼스트 파티입니다.
- 공인 주소만 있는 DNS(A 및/또는 AAAA).
- 서브도메인의 TLS 인증서(nginx/Apache 템플릿은 certbot 경로를 전제로 하며 Caddy는 직접 발급받음).
- 프로젝트 수정 권한.
설정
- 계정에서 프로젝트의 삽입 → 자체 도메인 설정을 엽니다.
- 1. 호스트에
chat.your-company.com같은 이름을 입력하고 저장합니다. - 2. 프록시 키에서 키 생성을 클릭합니다. 키는
rcpx_로 시작하며 정확히 한 번만 표시됩니다. 해시만 저장합니다. 키는 프록시 설정에만 넣고 웹페이지, 브라우저 스크립트나 저장소에 넣어서는 안 됩니다. - 3. 서버 설정에서 서버에 맞는 템플릿을 가져옵니다. 키 생성 직후에는 키가 포함되어 있으며 나중에는
__RC_PROXY_KEY__가 표시됩니다. 설정을 검사한 뒤(nginx -t또는apachectl configtest) 서버 설정을 다시 로드합니다. - 5. 검사에서 지금 검사를 클릭합니다(10초에 최대 한 번). 메시지는 생성되지 않습니다.
- 다섯 검사 단계가 모두 “정상”일 때만 삽입 스니펫을 4. 삽입 스니펫의 것으로 교체합니다.
<script src="https://chat.your-company.com/loader.js"
data-rc-site-key="SITE_KEY"
data-rc-ws-url="wss://chat.your-company.com/rc_wss/"
async></script>
위젯은 src에서 파일 위치를, data-rc-ws-url에서 채팅 연결을 가져옵니다. 여기에서 오프라인 양식, 업로드와 HTTP 대체 연결 주소를 도출합니다. 다른 속성은 그대로 유지합니다(script 태그 참조).
프록시가 전달해야 하는 항목
/rc_wss/→ 채팅 서버https://ws.reactive.chat/rc_wss/, WebSocket 업그레이드, 긴 시간 제한(템플릿: 3600초), 버퍼링 없음, 최소 32 MB의 본문 제한(업로드)이 필요합니다./api/offline/→https://ws.reactive.chat/api/offline/(작은 본문, 템플릿: 32 kB, 30초)./api/suche/→https://ws.reactive.chat/api/suche/, 버퍼링 없음(응답이 Server-Sent Events 스트림), 본문 최대 8 kB, 120초.- 그 밖의 모든 경로 → 배포 네트워크
https://cdn.reactive.chat/.
모든 경로에 적용됩니다.
- Host 헤더와 SNI는 대상 이름이어야 하며 운영자의 서브도메인이 아닙니다. 알 수 없는 호스트에는 HTTP 421로 응답합니다.
- 대상 인증서를 검증합니다.
- 쿠키와 인증 정보는 전달하지 않습니다. 브라우저의
Cookie와Authorization을 전달하지 않고Set-Cookie도 반환하지 않습니다. - 리디렉션은 사용하지 않습니다. 캐시는 필요하지 않습니다. 설정한다면
loader.js의 기간은 짧게 유지합니다. 파일 이름에 버전 번호가 없기 때문입니다. 코어에는 있습니다(아키텍처 참조).
/rc_wss/, /api/offline/, /api/suche/에서는 프록시가 모든 요청에 헤더 두 개를 설정합니다.
- X-RC-Proxy-Key – 운영자의
rcpx_…키입니다. - X-RC-Client-IP – 프록시에서 확인한 방문자 주소입니다.
“설정”은 브라우저가 이 이름으로 보낸 내용을 덮어쓴다는 뜻입니다. 배포 네트워크로 가는 경로에서는 둘 다 지웁니다.
로드 밸런서나 CDN 뒤에서는 프록시가 처음에 해당 장치의 주소를 봅니다. 실제 주소를 사용하도록 설정합니다. nginx real_ip(set_real_ip_from, real_ip_header), Apache mod_remoteip(RemoteIPHeader, RemoteIPTrustedProxy), Caddy trusted_proxies와 {remote_host} 대신 {client_ip}입니다. 검사는 이 경우를 감지하지 못하며 유효한 주소가 설정되었는지만 확인합니다.
nginx 템플릿
계정 페이지는 호스트, 대상 주소와 키를 미리 채우므로 그곳에서 복사하는 것이 가장 좋습니다. 여기 버전은 참고용이며 자리 표시자는 chat.your-company.com, SITE_KEY, __RC_PROXY_KEY__입니다. RHEL/CentOS의 CA 번들은 /etc/pki/tls/certs/ca-bundle.crt에 있습니다.
server {
listen 80;
listen [::]:80;
server_name chat.your-company.com;
location /.well-known/acme-challenge/ { root /var/www/html; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name chat.your-company.com;
ssl_certificate /etc/letsencrypt/live/chat.your-company.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/chat.your-company.com/privkey.pem;
# HTTP fallback (SSE / long polling): token in the query, do not log
location /rc_wss/h/ {
access_log off;
proxy_pass https://ws.reactive.chat/rc_wss/h/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host ws.reactive.chat;
proxy_ssl_server_name on;
proxy_ssl_name ws.reactive.chat;
proxy_ssl_verify on;
proxy_ssl_verify_depth 3;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_set_header X-RC-Proxy-Key "__RC_PROXY_KEY__";
proxy_set_header X-RC-Client-IP $remote_addr;
proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
proxy_cache off;
}
# Chat connection (WebSocket) and uploads
location /rc_wss/ {
proxy_pass https://ws.reactive.chat/rc_wss/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host ws.reactive.chat;
proxy_ssl_server_name on;
proxy_ssl_name ws.reactive.chat;
proxy_ssl_verify on;
proxy_ssl_verify_depth 3;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_set_header X-RC-Proxy-Key "__RC_PROXY_KEY__";
proxy_set_header X-RC-Client-IP $remote_addr;
proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
proxy_cache off;
client_max_body_size 32m;
proxy_request_buffering off;
}
# Offline form
location /api/offline/ {
proxy_pass https://ws.reactive.chat/api/offline/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host ws.reactive.chat;
proxy_ssl_server_name on;
proxy_ssl_name ws.reactive.chat;
proxy_ssl_verify on;
proxy_ssl_verify_depth 3;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_set_header X-RC-Proxy-Key "__RC_PROXY_KEY__";
proxy_set_header X-RC-Client-IP $remote_addr;
proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_hide_header Set-Cookie;
client_max_body_size 32k;
proxy_read_timeout 30s;
}
# Smart Answer (website search), answer as a stream
location /api/suche/ {
proxy_pass https://ws.reactive.chat/api/suche/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host ws.reactive.chat;
proxy_ssl_server_name on;
proxy_ssl_name ws.reactive.chat;
proxy_ssl_verify on;
proxy_ssl_verify_depth 3;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_set_header X-RC-Proxy-Key "__RC_PROXY_KEY__";
proxy_set_header X-RC-Client-IP $remote_addr;
proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_hide_header Set-Cookie;
client_max_body_size 8k;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 120s;
}
# Widget files
location / {
proxy_pass https://cdn.reactive.chat/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host cdn.reactive.chat;
proxy_ssl_server_name on;
proxy_ssl_name cdn.reactive.chat;
proxy_ssl_verify on;
proxy_ssl_verify_depth 3;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_set_header X-RC-Proxy-Key "";
proxy_set_header X-RC-Client-IP "";
proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_hide_header Set-Cookie;
}
}
Apache 템플릿(2.4.47 이상)
443 포트의 VirtualHost만 있습니다. 80 포트에서의 리디렉션과 인증서는 다른 호스트처럼 설정합니다. SSLProxyVerifyDepth 5가 필요합니다. 없으면 Apache가 대상 체인을 한 단계만 검사하고 “Certificate Chain too long”을 보고합니다.
<VirtualHost *:443>
ServerName chat.your-company.com
SetEnvIf Request_URI "^/rc_wss/h/" rc_no_log
CustomLog ${APACHE_LOG_DIR}/chat.your-company.com_access.log combined env=!rc_no_log
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/chat.your-company.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/chat.your-company.com/privkey.pem
SSLProxyEngine on
SSLProxyVerify require
SSLProxyVerifyDepth 5
SSLProxyCheckPeerName on
SSLProxyCACertificateFile /etc/ssl/certs/ca-certificates.crt
ProxyRequests Off
ProxyPreserveHost Off
ProxyAddHeaders Off
RequestHeader unset X-RC-Proxy-Key early
RequestHeader unset X-RC-Client-IP early
RequestHeader unset Cookie early
RequestHeader unset Authorization early
ProxyPass "/rc_wss/" "https://ws.reactive.chat/rc_wss/" upgrade=websocket timeout=3600 flushpackets=on
ProxyPass "/api/offline/" "https://ws.reactive.chat/api/offline/" timeout=30
ProxyPass "/api/suche/" "https://ws.reactive.chat/api/suche/" timeout=120 flushpackets=on
ProxyPass "/" "https://cdn.reactive.chat/"
<Location "/rc_wss/">
RequestHeader set X-RC-Proxy-Key "__RC_PROXY_KEY__"
RequestHeader set X-RC-Client-IP "expr=%{REMOTE_ADDR}"
</Location>
<Location "/api/offline/">
RequestHeader set X-RC-Proxy-Key "__RC_PROXY_KEY__"
RequestHeader set X-RC-Client-IP "expr=%{REMOTE_ADDR}"
Header unset Set-Cookie
</Location>
<Location "/api/suche/">
RequestHeader set X-RC-Proxy-Key "__RC_PROXY_KEY__"
RequestHeader set X-RC-Client-IP "expr=%{REMOTE_ADDR}"
Header unset Set-Cookie
</Location>
</VirtualHost>
Apache 2.4.47 이전 버전은 upgrade=websocket을 지원하지 않습니다. 이 경우 첫 번째 ProxyPass 줄을 다음으로 대체합니다.
RewriteEngine on
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule ^/rc_wss/(.*) "wss://ws.reactive.chat/rc_wss/$1" [P,L]
ProxyPass "/rc_wss/" "https://ws.reactive.chat/rc_wss/" timeout=3600 flushpackets=on
Caddy 2 템플릿
값이 있는 header_up은 브라우저가 이 이름으로 보낸 내용을 대체합니다. 접근 로그를 켜면 /rc_wss/h/*를 제외합니다(log_skip, Caddy 2.8 이전에는 skip_log).
chat.your-company.com {
handle /rc_wss/* {
reverse_proxy https://ws.reactive.chat {
header_up Host ws.reactive.chat
header_up X-RC-Proxy-Key "__RC_PROXY_KEY__"
header_up X-RC-Client-IP {remote_host}
header_up -Cookie
header_up -Authorization
transport http {
tls_server_name ws.reactive.chat
}
flush_interval -1
}
}
handle /api/offline/* {
reverse_proxy https://ws.reactive.chat {
header_up Host ws.reactive.chat
header_up X-RC-Proxy-Key "__RC_PROXY_KEY__"
header_up X-RC-Client-IP {remote_host}
header_up -Cookie
header_up -Authorization
header_down -Set-Cookie
transport http {
tls_server_name ws.reactive.chat
}
}
}
handle /api/suche/* {
reverse_proxy https://ws.reactive.chat {
header_up Host ws.reactive.chat
header_up X-RC-Proxy-Key "__RC_PROXY_KEY__"
header_up X-RC-Client-IP {remote_host}
header_up -Cookie
header_up -Authorization
header_down -Set-Cookie
transport http {
tls_server_name ws.reactive.chat
}
flush_interval -1
}
}
handle {
reverse_proxy https://cdn.reactive.chat {
header_up Host cdn.reactive.chat
header_up -X-RC-Proxy-Key
header_up -X-RC-Client-IP
header_up -Cookie
header_up -Authorization
header_down -Set-Cookie
transport http {
tls_server_name cdn.reactive.chat
}
}
}
}
다섯 검사 단계
각 단계는 “정상”, “오류”, “안내”, “검사하지 않음” 중 하나입니다. 다섯 번 모두 “정상”이어야 통과입니다. 검사는 HTTPS 443 포트만 사용하고 리디렉션을 따르지 않으며 단계당 5초입니다.
- 이름 확인 – A/AAAA가 있고 모든 주소가 공인 주소입니다. 이후 단계는 정확히 이 주소에 연결합니다.
- 자체 도메인의 loader.js – 파일을 로드해 당사 파일과 비교합니다. 연결, 응답이나 유효한 인증서가 없으면 나머지 단계를 건너뜁니다. 리디렉션은 오류이며 다른 파일은 캐시나
/의 잘못된 대상을 나타냅니다. - 키와 방문자 주소 –
/rc_wss/eigene-domain-probe를 호출합니다. 가능한 오류는X-RC-Proxy-Key누락, 알 수 없는 키(생성 직후 최대 30초 지연),X-RC-Client-IP누락 또는 유효하지 않음, 다른 프로젝트의 키입니다. 키가 유효하지만 다른 호스트에 등록되어 있으면 안내입니다. - WebSocket –
wss://chat.your-company.com/rc_wss/는 HTTP 101과rc.v1하위 프로토콜로 응답해야 합니다. - 오프라인 양식 –
GET /api/offline/SITE_KEY는 405로 응답해야 합니다(POST만 허용하는 엔드포인트이므로 메시지를 만들지 않고 검사 가능).
3단계의 검사 주소를 직접 호출할 수도 있습니다.
curl -s https://chat.your-company.com/rc_wss/eigene-domain-probe
올바르게 설정된 경우:
{"rc":"eigene-domain-probe","v":1,"schluessel":"gueltig","client_ip":"gesetzt","site":"SITE_KEY","host":"chat.your-company.com"}
- schluessel –
gueltig(유효),unbekannt(알 수 없음),fehlt(없음). - client_ip –
gesetzt(설정됨),fehlt(없음),ungueltig(유효하지 않음). - site, host – 유효한 키가 있을 때만 포함됩니다. 응답은 주소를 반환하지 않습니다.
프록시 키
- 교체: 새 키 생성은 즉시 새 키를 발급하며 이전 키는 24시간 동안 유효합니다. 유효한 키는 최대 두 개입니다. 24시간 안에 다시 교체하면 가장 오래된 키가 즉시 무효화됩니다.
- 제거: 키 제거는 키를 즉시 무효화하며 교체 후 유효한 이전 키도 포함합니다. 자체 도메인 제거는 호스트와 키를 함께 제거합니다. 먼저 웹사이트의 스니펫을 되돌립니다.
- 키가 없거나 잘못된 경우: 채팅은 계속되지만 두 헤더는 무시됩니다. 모든 방문자가 프록시 주소로 표시되며 악용 방지가 주소별로 집계하므로 프록시 전체와 모든 방문자를 동시에 빈도 제한할 수 있습니다.
- 키당 프로젝트 하나: 다른 프로젝트의 사이트 키를 가진 위젯이 유효한 키의 프록시로 연결하면 거부됩니다. 추가 프로젝트에는 자체 키가 있는 별도 서브도메인이 필요합니다.
- 키 유출: 키를 아는 사람은 프로젝트에 임의의 방문자 주소를 제시할 수 있습니다. 제거하고 새로 생성해 입력합니다.
문제 해결
- HTTP 421(Misdirected Request): 프록시가 대상 이름 대신 자체 이름을 호스트로 보냅니다. nginx:
proxy_set_header Host와proxy_ssl_server_name on/proxy_ssl_name. Apache:ProxyPreserveHost Off. Caddy:header_up Host와tls_server_name. - HTTP 502 / 대상 TLS 오류: Apache의
SSLProxyVerifyDepth 5누락, nginx의proxy_ssl_trusted_certificate경로 오류 또는proxy_ssl_server_name on누락, 외부 HTTPS 차단입니다. - 업로드 시 HTTP 413: 별도 설정이 없으면 nginx는 1 MB만 허용합니다.
/rc_wss/섹션에client_max_body_size 32m을 설정하고 앞단의 로드 밸런서에도 최소 같은 한도를 적용합니다. - WebSocket 업그레이드 없음:
Upgrade/Connection을 전달하지 않습니다. 채팅은 HTTP 대체 연결로 계속되지만 WebSocket 시도가 두 번 실패한 뒤에 연결됩니다. - 오프라인 양식의 401:
/api/offline/앞에 비밀번호 보호가 있습니다. - 출처 관련 안내가 있는 403: 프로젝트 허용된 출처에 웹사이트 도메인이 없습니다(채팅 서브도메인이 아님).
- 403 “reactive.chat에서 온 응답이 아닙니다”: 대상 앞의 방화벽이나 접근 규칙이 요청을 거부합니다.
- DNS가 사설 주소를 가리킴(
10.0.0.0/8,192.168.0.0/16,100.64.0.0/10,fc00::/7등): 공개 레코드는 공인 주소여야 합니다.
최종 업데이트: