NGINX vhost templates
Use this page to create new NGINX vhosts on web.core.lef (and to keep a second ingress proxy consistent).
These templates are derived from the current listener-separated setup (see NGINX ingress (public vs internal)).
How vhosts are organized (current)
Section titled “How vhosts are organized (current)”- Public vhosts bind only to the ingress VIPs (
192.168.20.112–192.168.20.120). - VPN-only vhosts (public DNS, VPN-only) bind only to
192.168.20.111. - Internal vhosts bind only to
192.168.20.2. - Enabled vhosts live under:
/etc/nginx/sites-enabled/public//etc/nginx/sites-enabled/vpn//etc/nginx/sites-enabled/internal/
- Canonical upstream groups live under
/etc/nginx/upstreams-enabled/. - Host allow/deny rules are vhost-specific (use for internal-only services).
When adding a new vhost, update these too
Section titled “When adding a new vhost, update these too”- Known-domain map (public
:80redirects + internal:80behavior): add<listener-ip>:<hostname> 1;to/etc/nginx/maps/known_domains.map(or regenerate viagenerate-known-domains-map.sh). - Upstream definition: add/update the relevant upstream in
/etc/nginx/upstreams-enabled/<zone>.conf. - Certificates
- Public: ensure the ACME/Let’s Encrypt cert exists for the hostname (paths typically under
/etc/letsencrypt/live/<hostname>/). - Internal: use the appropriate wildcard cert/key under
/etc/nginx/ssl/when possible.
- Public: ensure the ACME/Let’s Encrypt cert exists for the hostname (paths typically under
Reverse proxy vhosts (normalize to one shape)
Section titled “Reverse proxy vhosts (normalize to one shape)”Most vhosts on web.core.lef are reverse proxies. Keep them consistent by default:
- Always include
security-headers.conf. - Prefer the shared snippets over inline directive duplication:
proxy-base.confproxy-websockets.conf(when needed)proxy-nocache.conf(when appropriate)proxy-rate-limit.conf/proxy-retry-fallback.conf(optional hardening)
- Use a dedicated “restricted” vhost shape when a public hostname must be VPN/LAN-only (enforced via allow/deny).
- Use a “streaming” vhost shape when buffering breaks the app (
proxy_buffering off).
VPN-only (public DNS) vhosts use the same shapes as public vhosts, but:
- bind to the VPN-only VIP (
192.168.20.111) - live under
/etc/nginx/sites-enabled/vpn/
Thinkwise vhosts (normalize to one shape)
Section titled “Thinkwise vhosts (normalize to one shape)”For Thinkwise GUI vhosts (static GUI + Indicium API), keep the server blocks structurally identical across hostnames:
- Use the same static asset caching block.
- Use a single API prefix pattern (default:
/api/) and preferproxy_pass http://<upstream>/;to strip the prefix. - Keep
cache-control: no-storefor API responses.
If a vhost uses a different API prefix (example: /indicium/), update only the API location block and keep everything else consistent.
Current vhosts by pattern (observed)
Section titled “Current vhosts by pattern (observed)”This grouping is based on the current listener-separated config on web.core.lef:
- Thinkwise GUI + API (public):
concepts.lef.software,credit-hub.lef.software,experience.lef.software,sapore.lef.software,tokiocred.lef.software,unimed.lef.software- Exception:
my.lef.digitaluses a different API prefix (/indicium/) but should follow the same overall shape.
- Exception:
- Reverse proxy (public; standard):
collab.lef.digital,wf.lef.digital,wiki.lef.digital,analytics.coragem.app,proxy.coragem.app,s3.coragem.app,vault.lef.digital - Reverse proxy (public; restricted):
registry.coragem.app - Reverse proxy (public; streaming):
report.coragem.app - Internal Thinkwise GUI + API:
pivot.dev.lef,solution.dev.lef,tokio.dev.lef,trainee.core.lef - Internal reverse proxy:
ca.app.lef,draw.app.lef,s3.app.lef,uptime.app.lef- Exception:
ca.app.lefproxies to an HTTPS upstream and disables upstream TLS verification.
- Exception:
- Internal reverse proxy (streaming):
report.app.lef - Exceptions (path routing):
hook.lef.software,io-trg.lef.digital - VPN-only vhosts: see the
sites-enabled/vpn/listener VIP (192.168.20.111) and keep the list in sync with the firewall inventory.
Templates
Section titled “Templates”# Public HTTPS reverse proxy (template)
#
# - Binds ONLY to the intended public VIP.
# - Requires a matching entry in `maps/known_domains.map` for `<vip-ip>:<hostname>`.
#
# Replace placeholders:
# - <vip-ip> (example: 192.168.20.112)
# - <hostname> (example: wiki.lef.digital)
# - <access-log-name> (example: wiki-lef-digital)
# - <upstream-name> (example: wiki_lef_digital)
server {
listen <vip-ip>:443 ssl http2;
server_name <hostname>;
access_log /var/log/nginx/vhosts/<access-log-name>.access main;
error_log /var/log/nginx/vhosts/<access-log-name>.error warn;
include /etc/nginx/snippets/security-headers.conf;
location / {
include /etc/nginx/snippets/proxy-base.conf;
include /etc/nginx/snippets/proxy-websockets.conf;
include /etc/nginx/snippets/proxy-nocache.conf;
proxy_pass http://<upstream-name>;
}
ssl_certificate /etc/letsencrypt/live/<hostname>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<hostname>/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}# Public HTTPS reverse proxy (restricted; template)
#
# Use this for public hostnames that should only be reachable from VPN/LAN
# (enforced by NGINX allow/deny).
#
# Replace placeholders:
# - <vip-ip> (example: 192.168.20.113)
# - <hostname> (example: registry.coragem.app)
# - <access-log-name> (example: registry-coragem-app)
# - <upstream-name> (example: registry_coragem_app)
# - <max-body-size> (example: 3G)
# - <allow-cidr> (repeat as needed)
server {
listen <vip-ip>:443 ssl http2;
server_name <hostname>;
# Restrict access (example; adjust to policy)
allow <allow-cidr>;
deny all;
# Large uploads (optional; example: Docker registry)
client_max_body_size <max-body-size>;
access_log /var/log/nginx/vhosts/<access-log-name>.access main;
error_log /var/log/nginx/vhosts/<access-log-name>.error warn;
include /etc/nginx/snippets/security-headers.conf;
location / {
include /etc/nginx/snippets/proxy-base.conf;
include /etc/nginx/snippets/proxy-websockets.conf;
include /etc/nginx/snippets/proxy-nocache.conf;
# Optional hardening
# include /etc/nginx/snippets/proxy-rate-limit.conf;
# include /etc/nginx/snippets/proxy-retry-fallback.conf;
proxy_pass http://<upstream-name>;
}
ssl_certificate /etc/letsencrypt/live/<hostname>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<hostname>/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}# Public HTTPS reverse proxy (streaming/websocket; template)
#
# Use this for services that require long-lived responses or websocket-style
# behavior and do not work well with proxy buffering.
#
# Replace placeholders:
# - <vip-ip> (example: 192.168.20.113)
# - <hostname> (example: report.coragem.app)
# - <access-log-name> (example: report-coragem-app)
# - <upstream-name> (example: report_coragem_app)
server {
listen <vip-ip>:443 ssl http2;
server_name <hostname>;
access_log /var/log/nginx/vhosts/<access-log-name>.access main;
error_log /var/log/nginx/vhosts/<access-log-name>.error warn;
include /etc/nginx/snippets/security-headers.conf;
location / {
include /etc/nginx/snippets/proxy-base.conf;
include /etc/nginx/snippets/proxy-websockets.conf;
proxy_buffering off;
proxy_request_buffering off;
proxy_pass http://<upstream-name>;
}
ssl_certificate /etc/letsencrypt/live/<hostname>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<hostname>/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}# Public HTTPS path router (template; exception)
#
# Use this when a single public hostname routes different path prefixes to
# different internal upstreams.
#
# Replace placeholders:
# - <vip-ip> (example: 192.168.20.117)
# - <hostname> (example: hook.lef.software)
# - <access-log-name> (example: hook-lef-software)
# - <path-prefix> (example: tokio)
# - <upstream-name> (example: tokio_dev_lef)
# - <fallback-url> (example: https://lef.tec.br)
server {
listen <vip-ip>:443 ssl http2;
server_name <hostname>;
access_log /var/log/nginx/vhosts/<access-log-name>.access main;
error_log /var/log/nginx/vhosts/<access-log-name>.error warn;
include /etc/nginx/snippets/security-headers.conf;
# Route 1 (repeat as needed)
location /<path-prefix>/ {
include /etc/nginx/snippets/proxy-base.conf;
include /etc/nginx/snippets/proxy-websockets.conf;
# Strip the prefix before proxying:
rewrite ^/<path-prefix>(/.*)$ $1 break;
proxy_pass http://<upstream-name>;
}
# Default behavior
location / {
return 301 <fallback-url>;
}
# Optional hard stop if a request arrives with an unexpected Host header
# (usually redundant when using explicit `server_name`, but sometimes kept
# as a guardrail).
# if ($host !~ ^hook\.lef\.software$) { return 444; }
ssl_certificate /etc/letsencrypt/live/<hostname>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<hostname>/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}# Public HTTPS Thinkwise GUI + API proxy (template)
#
# Replace placeholders:
# - <vip-ip> (example: 192.168.20.117)
# - <hostname> (example: concepts.lef.software)
# - <web-root> (example: /srv/www/concepts.lef.software)
# - <access-log-name> (example: concepts-lef-software)
# - <upstream-name> (example: concepts_lef_software)
#
# Notes:
# - Static GUI is served from disk.
# - API traffic is proxied to an upstream (Indicium).
# - If your app uses a different API prefix (example: `/indicium/`), adjust the
# `location` accordingly.
server {
listen <vip-ip>:443 ssl http2;
server_name <hostname>;
root <web-root>;
index index.html index.htm;
access_log /var/log/nginx/vhosts/<access-log-name>.access main;
error_log /var/log/nginx/vhosts/<access-log-name>.error warn;
include /etc/nginx/snippets/security-headers.conf;
location ~* \.(?:css|js|mjs|wasm|woff2?|ttf|otf|eot|svg|ico|png|jpe?g|gif)$ {
access_log off;
expires 30d;
add_header cache-control "public, max-age=2592000, immutable";
try_files $uri =404;
}
location / {
try_files $uri $uri/ =404;
}
# Indicium API (example prefix; adjust to your app)
location ^~ /api/ {
include /etc/nginx/snippets/proxy-base.conf;
include /etc/nginx/snippets/proxy-websockets.conf;
add_header cache-control "no-store";
# Recommended: strip the `/api/` prefix.
# Example: `/api/foo` -> upstream `/foo`.
proxy_pass http://<upstream-name>/;
# Optional: some clients expect JSON from the API.
# proxy_set_header Accept application/json;
# Alternative: keep the `/api/` prefix on the upstream.
# proxy_pass http://<upstream-name>;
}
location ~* \.json$ {
add_header cache-control "no-store";
try_files $uri =404;
}
ssl_certificate /etc/letsencrypt/live/<hostname>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<hostname>/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}# Internal HTTPS Thinkwise GUI + API proxy (template)
#
# Replace placeholders:
# - <internal-ip> (example: 192.168.20.2)
# - <hostname> (example: pivot.dev.lef)
# - <web-root> (example: /srv/www/pivot.dev.lef)
# - <access-log-name> (example: dev-lef-pivot)
# - <upstream-name> (example: pivot_dev_lef)
# - <wildcard-cert> (example: /etc/nginx/ssl/dev-lef-wildcard.cert.pem)
# - <wildcard-key> (example: /etc/nginx/ssl/dev-lef-wildcard.key.pem)
#
# Notes:
# - Listener separation: internal vhosts MUST bind only to the internal listener IP.
# - If your app uses a different API prefix (example: `/indicium/`), adjust the
# `location` accordingly.
server {
listen <internal-ip>:443 ssl http2;
server_name <hostname>;
root <web-root>;
index index.html index.htm;
access_log /var/log/nginx/vhosts/<access-log-name>.access main;
error_log /var/log/nginx/vhosts/<access-log-name>.error warn;
include /etc/nginx/snippets/security-headers.conf;
location ~* \.(?:css|js|mjs|wasm|woff2?|ttf|otf|eot|svg|ico|png|jpe?g|gif)$ {
access_log off;
expires 30d;
add_header cache-control "public, max-age=2592000, immutable";
try_files $uri =404;
}
location / {
try_files $uri $uri/ =404;
}
# Indicium API (example prefix; adjust to your app)
location ^~ /api/ {
include /etc/nginx/snippets/proxy-base.conf;
include /etc/nginx/snippets/proxy-websockets.conf;
add_header cache-control "no-store";
# Recommended: strip the `/api/` prefix.
proxy_pass http://<upstream-name>/;
# Optional: some clients expect JSON from the API.
# proxy_set_header Accept application/json;
}
location ~* \.json$ {
add_header cache-control "no-store";
try_files $uri =404;
}
ssl_certificate <wildcard-cert>;
ssl_certificate_key <wildcard-key>;
include /etc/nginx/snippets/ssl-internal-params.conf;
}# Internal HTTPS reverse proxy (template)
#
# - Binds ONLY to the internal listener IP (Core LAN IP).
# - Use allow/deny if this should be restricted to VPN/LAN ranges.
#
# Replace placeholders:
# - <internal-ip> (example: 192.168.20.2)
# - <hostname> (example: uptime.app.lef)
# - <upstream-name> (example: uptime_app_lef)
# - <zone-wildcard-cert> (example: /etc/nginx/ssl/app-lef-wildcard.cert.pem)
# - <zone-wildcard-key> (example: /etc/nginx/ssl/app-lef-wildcard.key.pem)
server {
listen <internal-ip>:443 ssl http2;
server_name <hostname>;
# Example restrictions (adjust to policy)
allow 192.168.20.0/24;
allow 10.0.0.0/8;
deny all;
include /etc/nginx/snippets/security-headers.conf;
location / {
include /etc/nginx/snippets/proxy-base.conf;
include /etc/nginx/snippets/proxy-websockets.conf;
include /etc/nginx/snippets/proxy-nocache.conf;
proxy_pass http://<upstream-name>;
}
ssl_certificate <zone-wildcard-cert>;
ssl_certificate_key <zone-wildcard-key>;
include /etc/nginx/snippets/ssl-internal-params.conf;
}# Internal HTTPS reverse proxy (streaming/websocket; template)
#
# Replace placeholders:
# - <internal-ip> (example: 192.168.20.2)
# - <hostname> (example: report.app.lef)
# - <upstream-name> (example: report_app_lef)
# - <wildcard-cert> (example: /etc/nginx/ssl/app-lef-wildcard.cert.pem)
# - <wildcard-key> (example: /etc/nginx/ssl/app-lef-wildcard.key.pem)
#
# Notes:
# - Listener separation: internal vhosts MUST bind only to the internal listener IP.
server {
listen <internal-ip>:443 ssl http2;
server_name <hostname>;
# Restrict access (example; adjust to policy)
allow 192.168.20.0/24;
allow 10.0.0.0/8;
deny all;
include /etc/nginx/snippets/security-headers.conf;
location / {
include /etc/nginx/snippets/proxy-base.conf;
include /etc/nginx/snippets/proxy-websockets.conf;
proxy_buffering off;
proxy_request_buffering off;
proxy_pass http://<upstream-name>;
}
ssl_certificate <wildcard-cert>;
ssl_certificate_key <wildcard-key>;
include /etc/nginx/snippets/ssl-internal-params.conf;
}# Internal HTTPS static SPA (template)
#
# Replace placeholders:
# - <internal-ip> (example: 192.168.20.2)
# - <hostname> (example: main.docs.lef)
# - <web-root> (example: /srv/www/main.docs.lef)
# - <wildcard-cert> (example: /etc/nginx/ssl/docs-lef-wildcard.cert.pem)
# - <wildcard-key> (example: /etc/nginx/ssl/docs-lef-wildcard.key.pem)
server {
listen <internal-ip>:443 ssl http2;
server_name <hostname>;
root <web-root>;
index index.html index.htm;
include /etc/nginx/snippets/security-headers.conf;
location = /index.html {
add_header cache-control "no-store, no-cache, must-revalidate" always;
}
location / {
try_files $uri $uri/ /index.html;
add_header cache-control "no-store, no-cache, must-revalidate" always;
}
location ~* \.(?:css|js|mjs|png|jpg|jpeg|gif|svg|ico|webp|woff2?|ttf|otf)$ {
add_header cache-control "public, max-age=31536000, immutable" always;
}
ssl_certificate <wildcard-cert>;
ssl_certificate_key <wildcard-key>;
include /etc/nginx/snippets/ssl-internal-params.conf;
}Validation & safe reload
Section titled “Validation & safe reload”On the server:
sudo nginx -t
sudo systemctl reload nginx
sudo ss -lntp | grep nginxThen validate by targeting the specific listener IP (VIP or internal IP) and host header:
curl -I http://<listener-ip>/ -H 'Host: <hostname>'
curl -Ik https://<hostname>/ --resolve <hostname>:443:<listener-ip>