v8.2.2: Add security headers and HTTP/2 support for Nginx configuration in models, templates, and JS

- Introduced `NginxProxyPassSecurity` model for managing security headers and server token settings.
- Enhanced templates and JavaScript to support `hide_server_tokens`, `security_headers`, and `hsts` options.
- Updated Ansible and migration files to align with new security and HTTP/2 features.
master v8.2.2
Aidaho 2025-07-28 11:57:22 +03:00
parent 9755fe1513
commit 20157572c6
16 changed files with 142 additions and 28 deletions

View File

@ -0,0 +1,22 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect, Version
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
try:
Version.update(version='8.2.2').execute()
except Exception as e:
print(f"Error updating version: {str(e)}")
raise e
def down():
"""Roll back the migration."""
try:
Version.update(version='8.2.1').execute()
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -475,6 +475,7 @@ class HaproxyConfigRequest(BaseModel):
backends: Optional[str] = None
circuit_breaking: Optional[HaproxyCircuitBreaking] = None
action: Optional[Literal['save', 'test', 'reload', 'restart']] = "save"
http2: Optional[bool] = False
class HaproxyUserListUser(BaseModel):
@ -645,6 +646,11 @@ class NginxLocationRequest(BaseModel):
return values
class NginxProxyPassSecurity(BaseModel):
security_headers: bool = False
hide_server_tokens: bool = False
class NginxProxyPassRequest(BaseModel):
locations: List[NginxLocationRequest]
name: Union[IPvAnyAddress, DomainName]
@ -655,9 +661,11 @@ class NginxProxyPassRequest(BaseModel):
ssl_crt: Optional[str] = None
ssl_key: Optional[str] = None
ssl_offloading: Optional[bool] = False
hsts: Optional[bool] = False
http2: Optional[bool] = False
action: Optional[Literal['save', 'test', 'reload', 'restart']] = 'reload'
compression: bool = False
compression_level: Annotated[int, Gt(0), Le(10)] = 6
compression_min_length: Optional[int] = 1024
compression_types: Optional[str] = 'text/plain text/css application/json application/javascript text/xml'
security: Optional[NginxProxyPassSecurity]

View File

@ -1,7 +1,7 @@
{{ config.type }} {{ config.name }}
{% if config.binds != 'None' -%}
{% for bind in config.binds -%}
bind {{ bind.ip }}:{{ bind.port }} {% if config.ssl != 'None' and config.mode == 'http' and config.ssl.cert %} ssl crt {{cert_path}}/{{ config.ssl.cert }}{% endif %}
bind {{ bind.ip }}:{{ bind.port }} {% if config.ssl != 'None' and config.mode == 'http' and config.ssl.cert %} ssl crt {{cert_path}}/{{ config.ssl.cert }} {% if config.http2 %}alpn h2,http/1.1{% endif %}{% endif %}
{% endfor %}
{% endif %}
@ -35,6 +35,14 @@
tcp-request connection reject if { src -f {{ service_dir }}/black/{{ config.blacklist }} }
{% endif %}
{% if config.ddos -%}
{{ ddos }}
acl abuse sc1_http_req_rate({{ config.name }}) ge 100
acl flag_abuser sc1_inc_gpc0({{ config.name }})
tcp-request content reject if abuse flag_abuser
# End config for DDOS
{% endif -%}
{% if config.acls != 'None' -%}
{% for acl in config.acls -%}
{% if acl.acl_if in (1, 2) -%}
@ -72,14 +80,6 @@
{{ ssl_offloading }}
{% endif -%}
{% if config.ddos -%}
{{ ddos }}
acl abuse sc1_http_req_rate({{ config.name }}) ge 100
acl flag_abuser sc1_inc_gpc0({{ config.name }})
tcp-request content reject if abuse flag_abuser
# End config for DDOS
{% endif -%}
{% if config.waf -%}
filter spoe engine modsecurity config {{ service_dir }}/waf.conf
http-request deny if { var(txn.modsec.code) -m int gt 0 }
@ -102,7 +102,7 @@
{% if config.ssl != 'None' and config.mode == 'http' -%}
{%- if config.ssl.ssl_check_backend -%}
{%- set ssl_check_option = 'ssl verify' -%}
{%- set ssl_check_option = 'ssl verify required' -%}
{%- else -%}
{%- set ssl_check_option = 'ssl verify none' -%}
{%- endif -%}
@ -130,12 +130,12 @@
default-server observe {{ config.circuit_breaking.observe }} error-limit {{ config.circuit_breaking.error_limit }} on-error {{ config.circuit_breaking.on_error }}
{% endif -%}
{% if config.backend_servers != 'None' and config.servers_template == 'None' -%}
{% for backend in config.backend_servers -%}
{%- if config.backend_servers != 'None' and config.servers_template == 'None' -%}
{%- for backend in config.backend_servers -%}
server {{ backend.server }} {{ backend.server }}:{{ backend.port }} port {{ backend.port_check }} {{ check_option }} {{ ssl_check_option }} maxconn {{ backend.maxconn }}{% if backend.send_proxy %} send-proxy{% endif %}{% if backend.backup %} backup {% endif %}
{% endfor -%}
{% endif -%}
{%- endif -%}
{% if config.servers_template != 'None' -%}
server-template {{ config.servers_template.prefix }} {{ config.servers_template.count }} {{ config.servers_template.servers }}: {{ config.servers_template.port }} {{ check_option }}

View File

@ -4,7 +4,7 @@ server {
listen {{ config.port }} ssl{% if config.http2 %} http2{% endif %};
ssl_certificate {{ config.ssl_crt }};
ssl_certificate_key {{ config.ssl_key }};
{% if nginx_proxy.security.hsts %}
{% if config.hsts %}
add_header Strict-Transport-Security "max-age={{ nginx_proxy.security.hsts_max_age }}; includeSubDomains" always;
{% endif %}
{% else %}
@ -29,11 +29,11 @@ server {
{% endif -%}
{% if nginx_proxy.security.hide_server_tokens %}
{% if config.security.hide_server_tokens %}
server_tokens off;
{% endif %}
{% if nginx_proxy.security.security_headers %}
{% if config.security.security_headers %}
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
@ -150,7 +150,7 @@ proxy_cache_path {{ nginx_proxy.caching.zones[0].path }}
inactive={{ nginx_proxy.caching.zones[0].inactive }};
{% endif %}
{% if config.ssl_offloading -%}
{% if config.ssl_offloading and config.scheme == 'https' -%}
# HTTP to HTTPS redirect
server {
listen 80;

View File

@ -9,10 +9,7 @@ nginx_proxy:
# Security configurations
security:
hide_server_tokens: true # Hide Nginx version in headers
hide_backend_headers: true # Hide backend server headers (e.g., X-Powered-By)
security_headers: true # Enable security headers (X-Content-Type-Options, etc.)
hsts: true # Enable HTTP Strict Transport Security
hsts_max_age: 15768000 # HSTS duration in seconds (6 months)
content_security_policy: "default-src 'self'" # Content Security Policy rules

View File

@ -214,6 +214,7 @@ function getNginxFormData($form, form_name) {
indexed_array['locations'] = [];
indexed_array['backend_servers'] = [];
indexed_array['name_aliases'] = [];
indexed_array['security'] = {};
let headers = [];
$.map(unindexed_array, function (n, i) {
@ -244,6 +245,12 @@ function getNginxFormData($form, form_name) {
} else {
indexed_array['ssl_offloading'] = false;
}
} else if (n['name'] === 'hsts') {
if ($('input[name="hsts"]').is(':checked')) {
indexed_array['hsts'] = true;
} else {
indexed_array['hsts'] = false;
}
} else if (n['name'] === 'http2') {
if ($('input[name="http2"]').is(':checked')) {
indexed_array['http2'] = true;
@ -261,6 +268,15 @@ function getNginxFormData($form, form_name) {
}
indexed_array['name_aliases'].push(name);
});
let hide_server_tokens = false;
let security_headers = false;
if ($('#hide_server_tokens').is(':checked')) {
hide_server_tokens = true;
}
if ($('#security_headers').is(':checked')) {
security_headers = true;
}
indexed_array['security'] = {'hide_server_tokens': hide_server_tokens, 'security_headers': security_headers};
$('#'+form_name+' span[name="add_servers"] p').each(function (){
let server = $(this).children("input[name='servers']").val();
if (server === undefined || server === '') {
@ -274,7 +290,7 @@ function getNginxFormData($form, form_name) {
});
let elementsForDelete = [
'servers', 'server_port', 'max_fails', 'fail_timeout', 'proxy_connect_timeout', 'proxy_read_timeout', 'proxy_send_timeout',
'headers_res', 'header_name', 'header_value', 'upstream', 'server', 'name_alias'
'headers_res', 'header_name', 'header_value', 'upstream', 'server', 'name_alias', 'hide_server_tokens', 'security_headers'
]
for (let element of elementsForDelete) {
delete indexed_array[element]

View File

@ -105,6 +105,14 @@ function openSection(section) {
} else {
$('#'+section_type+'_whitelist_checkbox').prop("checked", false);
}
if (data.ssl) {
if ($("#https-" + section_type).is(':checked')) {
$("#https-hide-" + section_type).show("fast");
$("#path-cert-" + section_type).val(data.ssl['cert']);
} else {
$("#https-hide-" + section_type).hide("fast");
}
}
}
if (section_type === 'listen' || section_type === 'backend') {
if (data.config.backend_servers) {
@ -460,12 +468,10 @@ function getFormData($form, form_name) {
}
} else if (n['name'] === 'ssl') {
if ($('input[name="ssl"]').is(':checked')) {
let cert = $('input[name="cert"]').val();
let ssl_check_backend = true;
if ($('input[name="ssl-check"]').is(':checked')) {
ssl_check_backend = 0;
} else {
ssl_check_backend = true;
let cert = $form.find('input[name="cert"]').val();
if ($form.find('input[name="ssl-check"]').is(':checked')) {
ssl_check_backend = false;
}
indexed_array['ssl'] = {cert, ssl_check_backend};
}
@ -509,6 +515,10 @@ function getFormData($form, form_name) {
if ($('input[name="cache"]').is(':checked')) {
indexed_array['cache'] = true;
}
} else if (n['name'] === 'http2') {
if ($('input[name="http2"]').is(':checked')) {
indexed_array['http2'] = true;
}
} else if (n['name'] === 'circuit_breaking') {
if ($('input[name="circuit_breaking"]').is(':checked')) {
let observe = $('select[name="circuit_breaking_observe"] option:selected').val();
@ -805,6 +815,23 @@ function openNginxSection(section) {
$("#scheme").selectmenu();
$("#scheme").selectmenu('refresh');
}
if (data.security) {
if (data.security.hide_server_tokens) {
$('#hide_server_tokens').prop("checked", true);
} else {
$('#hide_server_tokens').prop("checked", false);
}
if (data.security.security_headers) {
$('#security_headers').prop("checked", true);
} else {
$('#security_headers').prop("checked", false);
}
}
if (data.hsts) {
$('#hsts').prop("checked", true);
} else {
$('#hsts').prop("checked", false);
}
}
$(section_id + ' select[name="server"]').selectmenu();
$(section_id + ' select[name="server"]').selectmenu('refresh');

View File

@ -48,6 +48,7 @@
<div id="https-hide-frontend" style="display: none;">
<br /><span class="tooltip tooltipTop">{{lang.words.enter2|title()}} {{lang.words.name}} {{lang.words.of}} pem {{lang.words.file2}}, {{lang.add_page.desc.press_down}}:</span><br />
{{ input('path-cert-frontend', name="cert", placeholder="some_cert.pem", size='39') }}
<p>{{ checkbox('frontend-http2', name='http2', title=lang.add_page.desc.enable_http2, desc='Enable HTTP2') }}</p>
</div>
</td>
</tr>

View File

@ -50,6 +50,7 @@
<br /><span class="tooltip tooltipTop">{{lang.words.enter2|title()}} {{lang.words.name}} {{lang.words.of}} pem {{lang.words.file2}}, {{lang.add_page.desc.press_down}}:</span><br />
{{ input('path-cert-listen', name="cert", placeholder="some_cert.pem", size='39') }}<br />
<label for="ssl-check-listen" style="margin-top: 5px;">{{lang.add_page.buttons.disable_ssl_verify}}</label><input type="checkbox" id="ssl-check-listen" name="ssl-check" value="ssl-check" checked>
<p>{{ checkbox('listen-http2', name='http2', title=lang.add_page.desc.enable_http2, desc='Enable HTTP2') }}</p>
</div>
</td>
</tr>

View File

@ -58,7 +58,9 @@ Timeouts: Timeouts (proxy_connect_timeout).
<br>
<br>
<div id="hide-scheme" style="display: none;">
{{ checkbox('ssl_offloading', title=lang.add_page.desc.http_https, desc='HTTP->HTTPS') }}<br>
{{ checkbox('ssl_offloading', title=lang.add_page.desc.http_https, desc='HTTP->HTTPS') }}
{{ checkbox('hsts', title=lang.words.enable|title() + ' HSTS', desc='HSTS') }}
<br>
<span class="tooltip tooltipTop">{{lang.words.enter2|title()}} {{lang.words.name}} {{lang.words.of}} {{lang.words.file2}}, {{lang.add_page.desc.press_down}}:</span><br />
{{ input('ssl_key', placeholder='cert.key') }}
{{ input('ssl_crt', placeholder='cert.crt') }}
@ -111,6 +113,16 @@ Timeouts: Timeouts (proxy_connect_timeout).
</p>
</td>
</tr>
<tr class="addName">
<td class="addName">{{ lang.words.security|title() }}:</td>
<td class="addOption">
<div id="security-options">
{% set enable_sec_headers = lang.words.enable|title() + ' ' + lang.words.security + ' ' + lang.words.headers %}
<p>{{ checkbox('hide_server_tokens', title=lang.add_page.desc.hide_server_tokens, desc=lang.phrases.hide_server_tokens, checked='checked') }}</p>
<p>{{ checkbox('security_headers', title=lang.add_page.desc.security_headers, desc=lang.phrases.security_headers, checked='checked') }}</p>
</div>
</td>
</tr>
<tr>
<td class="addName">{{ lang.words.compression|title() }}:</td>
<td class="addOption">

View File

@ -343,6 +343,8 @@
"ssh_passphrase": "SSH key passphrase",
"check_interval": "Check interval.",
"check_interval_title": "Check interval. In seconds.",
"hide_server_tokens": "Hide server tokens",
"security_headers": "Enable security headers",
}
%}
{% set roles = {
@ -354,6 +356,8 @@
%}
{% set add_page = {
"desc": {
"security_headers": "Enable security headers (X-Content-Type-Options, etc.)",
"hide_server_tokens": "Hide Nginx version in headers",
"port_check": "A basic TCP-layer health check tries to connect to the server's TCP port. The check is valid when the server answers with a SYN/ACK packet.",
"maxconn": "The total number of connections allowed, process-wide. This stops the process from accepting too many connections at once, which safeguards it from running out of memory.",
"server_template": "Create the list of servers from the template",
@ -975,5 +979,6 @@
"types": "types",
"min": "minimum",
"length": "length",
"security": "security",
}
%}

View File

@ -330,6 +330,8 @@
"ssh_passphrase": "Frase de paso para la clave SSH",
"check_interval": "Intervalo de comprobación.",
"check_interval_title": "Intervalo de comprobación. En segundos.",
"hide_server_tokens": "Ocultar tokens del servidor",
"security_headers": "Habilitar encabezados de seguridad",
}
%}
{% set roles = {
@ -341,6 +343,8 @@
%}
{% set add_page = {
"desc": {
"security_headers": "Habilitar encabezados de seguridad (X-Content-Type-Options, etc.)",
"hide_server_tokens": "Ocultar la versión de Nginx en los encabezados",
"port_check": "Una verificación básica a nivel TCP intenta conectarse al puerto TCP del servidor. La verificación es válida cuando el servidor responde con un paquete SYN/ACK.",
"maxconn": "Número total de conexiones permitidas a nivel de proceso. Esto evita aceptar demasiadas conexiones a la vez, protegiendo de quedarse sin memoria.",
"server_template": "Crear la lista de servidores desde la plantilla",
@ -959,6 +963,7 @@
"light": "claro",
"types": "tipos",
"min": "mínimo",
"length": "longitud"
"length": "longitud",
"security": "seguridad",
}
%}

View File

@ -330,6 +330,8 @@
"ssh_passphrase": "Phrase secrète de la clé SSH",
"check_interval": "Vérifiez l'intervalle",
"check_interval_title": "Vérifiez l'intervalle. En secondes.",
"hide_server_tokens": "Masquer les jetons du serveur",
"security_headers": "Activer les en-têtes de sécurité",
}
%}
{% set roles = {
@ -341,6 +343,8 @@
%}
{% set add_page = {
"desc": {
"security_headers": "Activer les en-têtes de sécurité (X-Content-Type-Options, etc.)",
"hide_server_tokens": "Masquer la version Nginx dans les en-têtes",
"port_check": "Un contrôle de base de la couche TCP tente de se connecter au port TCP du serveur. Le contrôle est valide lorsque le serveur répond par un paquet SYN/ACK.",
"maxconn": "Le nombre total de connexions autorisées, à l\'échelle du processus. Cela empêche le processus d\'accepter trop de connexions à la fois, ce qui lui évite de manquer de mémoire.",
"server_template": "Créez la liste des serveurs à partir du modèle",
@ -961,5 +965,6 @@
"types": "types",
"min": "minimum",
"length": "longueur",
"security": "sécurité",
}
%}

View File

@ -330,6 +330,8 @@
"ssh_passphrase": "Senha da chave SSH",
"check_interval": "Verifique o intervalo",
"check_interval_title": "Verifique o intervalo. Em segundos.",
"hide_server_tokens": "Ocultar tokens do servidor",
"security_headers": "Habilitar cabeçalhos de segurança",
}
%}
{% set roles = {
@ -341,6 +343,8 @@
%}
{% set add_page = {
"desc": {
"security_headers": "Habilitar cabeçalhos de segurança (X-Content-Type-Options, etc.)",
"hide_server_tokens": "Ocultar versão do Nginx nos cabeçalhos",
"port_check": "a verificação de integridade básica do nivel TCP tenta se conectar à porta TCP do servidor. A verificação é válida quando o servidor responde com um pacote SYN/ACK",
"maxconn": "número total de conexões permitidas. Isso impede que o processo aceite muitas conexões de uma só vez, o que o protege de ficar sem memória.",
"server_template": "Criar uma lista de servidores de um modelo",
@ -960,5 +964,6 @@
"types": "tipos",
"min": "mínima",
"length": "comprimento",
"security": "segurança",
}
%}

View File

@ -343,6 +343,8 @@
"ssh_passphrase": "Ключевая фраза SSH",
"check_interval": "Интервал проверки",
"check_interval_title": "Интервал проверки. В секундах.",
"hide_server_tokens": "Скрыть токены сервера",
"security_headers": "Включить заголовки безопасности",
}
%}
{% set roles = {
@ -354,6 +356,8 @@
%}
{% set add_page = {
"desc": {
"security_headers": "Включить заголовки безопасности (X-Content-Type-Options и т. д.)",
"hide_server_tokens": "Скрыть версию Nginx в заголовках",
"port_check": "Базовая проверка работоспособности на уровне TCP пытается подключиться к TCP-порту сервера. Проверка действительна, когда сервер отвечает пакетом SYN/ACK.",
"maxconn": "Общее количество разрешенных подключений для всего процесса. Это не позволяет процессу одновременно принимать слишком много подключений, что защищает его от нехватки памяти.",
"server_template": "Создать список серверов из шаблона",
@ -975,5 +979,6 @@
"types": "типы",
"min": "минимальная",
"length": "длина",
"security": "безопасность",
}
%}

View File

@ -330,6 +330,8 @@
"ssh_passphrase": "SSH 密钥密码",
"check_interval": "检查间隔。",
"check_interval_title": "检查间隔(以秒为单位)。",
"hide_server_tokens": "隐藏服务器令牌",
"security_headers": "启用安全标头",
}
%}
{% set roles = {
@ -341,6 +343,8 @@
%}
{% set add_page = {
"desc": {
"security_headers": "启用安全标头X-Content-Type-Options 等)",
"hide_server_tokens": "在标头中隐藏 Nginx 版本",
"port_check": "基本的 TCP 层健康检查尝试连接到服务器的 TCP 端口。当服务器返回 SYN/ACK 数据包时,检查被认为是成功的。",
"maxconn": "进程允许的总连接数。这可以防止进程接受过多连接,从而防止内存耗尽。",
"server_template": "根据模板创建服务器列表。",
@ -960,5 +964,6 @@
"types": "类型",
"min": "最小值",
"length": "长度",
"security": "安全",
}
%}