{{ $cfg := .Cfg }}{{ $healthzURI := .HealthzURI }}{{ $backends := .Backends }}{{ $proxyHeaders := .ProxySetHeaders }} daemon off; worker_processes {{ $cfg.WorkerProcesses }}; pid /run/nginx.pid; {{ if ne .MaxOpenFiles 0 }} worker_rlimit_nofile {{ .MaxOpenFiles }}; {{ end}} events { multi_accept on; worker_connections {{ $cfg.MaxWorkerConnections }}; use epoll; } http { {{/* we use the value of the header X-Forwarded-For to be able to use the geo_ip module */}} {{ if $cfg.UseProxyProtocol }} set_real_ip_from {{ $cfg.ProxyRealIPCIDR }}; real_ip_header proxy_protocol; {{ else }} real_ip_header X-Forwarded-For; set_real_ip_from 0.0.0.0/0; {{ end }} real_ip_recursive on; {{/* databases used to determine the country depending on the client IP address */}} {{/* http://nginx.org/en/docs/http/ngx_http_geoip_module.html */}} {{/* this is require to calculate traffic for individual country using GeoIP in the status page */}} geoip_country /etc/nginx/GeoIP.dat; geoip_city /etc/nginx/GeoLiteCity.dat; geoip_proxy_recursive on; {{ if $cfg.EnableVtsStatus }} vhost_traffic_status_zone shared:vhost_traffic_status:{{ $cfg.VtsStatusZoneSize }}; vhost_traffic_status_filter_by_set_key $geoip_country_code country::*; {{ end }} # lua section to return proper error codes when custom pages are used lua_package_path '.?.lua;./etc/nginx/lua/?.lua;/etc/nginx/lua/vendor/lua-resty-http/lib/?.lua;'; init_by_lua_block { require("error_page") } sendfile on; aio threads; tcp_nopush on; tcp_nodelay on; log_subrequest on; reset_timedout_connection on; keepalive_timeout {{ $cfg.KeepAlive }}s; client_header_buffer_size {{ $cfg.ClientHeaderBufferSize }}; large_client_header_buffers {{ $cfg.LargeClientHeaderBuffers }}; types_hash_max_size 2048; server_names_hash_max_size {{ $cfg.ServerNameHashMaxSize }}; server_names_hash_bucket_size {{ $cfg.ServerNameHashBucketSize }}; map_hash_bucket_size {{ $cfg.MapHashBucketSize }}; include /etc/nginx/mime.types; default_type text/html; {{ if $cfg.UseGzip }} gzip on; gzip_comp_level 5; gzip_http_version 1.1; gzip_min_length 256; gzip_types {{ $cfg.GzipTypes }}; gzip_proxied any; {{ end }} server_tokens {{ if $cfg.ShowServerTokens }}on{{ else }}off{{ end }}; log_format upstreaminfo '{{ if $cfg.UseProxyProtocol }}$proxy_protocol_addr{{ else }}$remote_addr{{ end }} - ' '[$proxy_add_x_forwarded_for] - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" ' '$request_length $request_time [$proxy_upstream_name] $upstream_addr $upstream_response_length $upstream_response_time $upstream_status'; {{/* map urls that should not appear in access.log */}} {{/* http://nginx.org/en/docs/http/ngx_http_log_module.html#access_log */}} map $request_uri $loggable { {{ range $reqUri := $cfg.SkipAccessLogURLs }} {{ $reqUri }} 0;{{ end }} default 1; } {{ if $cfg.DisableAccessLog }} access_log off; {{ else }} access_log /var/log/nginx/access.log upstreaminfo if=$loggable; {{ end }} error_log /var/log/nginx/error.log {{ $cfg.ErrorLogLevel }}; {{ buildResolvers $cfg.Resolver }} {{/* Whenever nginx proxies a request without a "Connection" header, the "Connection" header is set to "close" */}} {{/* when making the target request. This means that you cannot simply use */}} {{/* "proxy_set_header Connection $http_connection" for WebSocket support because in this case, the */}} {{/* "Connection" header would be set to "" whenever the original request did not have a "Connection" header, */}} {{/* which would mean no "Connection" header would be in the target request. Since this would deviate from */}} {{/* normal nginx behavior we have to use this approach. */}} # Retain the default nginx handling of requests without a "Connection" header map $http_upgrade $connection_upgrade { default upgrade; '' close; } # trust http_x_forwarded_proto headers correctly indicate ssl offloading map $http_x_forwarded_proto $pass_access_scheme { default $http_x_forwarded_proto; '' $scheme; } map $http_x_forwarded_port $pass_server_port { default $http_x_forwarded_port; '' $server_port; } # map port 442 to 443 for header X-Forwarded-Port map $pass_server_port $pass_port { 442 443; default $pass_server_port; } # Map a response error watching the header Content-Type map $http_accept $httpAccept { default html; application/json json; application/xml xml; text/plain text; } map $httpAccept $httpReturnType { default text/html; json application/json; xml application/xml; text text/plain; } server_name_in_redirect off; port_in_redirect off; ssl_protocols {{ $cfg.SSLProtocols }}; # turn on session caching to drastically improve performance {{ if $cfg.SSLSessionCache }} ssl_session_cache builtin:1000 shared:SSL:{{ $cfg.SSLSessionCacheSize }}; ssl_session_timeout {{ $cfg.SSLSessionTimeout }}; {{ end }} # allow configuring ssl session tickets ssl_session_tickets {{ if $cfg.SSLSessionTickets }}on{{ else }}off{{ end }}; # slightly reduce the time-to-first-byte ssl_buffer_size {{ $cfg.SSLBufferSize }}; {{ if not (empty $cfg.SSLCiphers) }} # allow configuring custom ssl ciphers ssl_ciphers '{{ $cfg.SSLCiphers }}'; ssl_prefer_server_ciphers on; {{ end }} {{ if not (empty $cfg.SSLDHParam) }} # allow custom DH file http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_dhparam ssl_dhparam {{ $cfg.SSLDHParam }}; {{ end }} {{ if not $cfg.EnableDynamicTLSRecords }} ssl_dyn_rec_size_lo 0; {{ end }} {{ if .CustomErrors }} # Custom error pages proxy_intercept_errors on; {{ end }} {{ range $errCode := $cfg.CustomHTTPErrors }} error_page {{ $errCode }} = @custom_{{ $errCode }};{{ end }} # In case of errors try the next upstream server before returning an error proxy_next_upstream error timeout invalid_header http_502 http_503 http_504{{ if $cfg.RetryNonIdempotent }} non_idempotent{{ end }}; {{range $name, $upstream := $backends}} upstream {{$upstream.Name}} { {{ if eq $upstream.SessionAffinity.AffinityType "cookie" }} sticky hash={{$upstream.SessionAffinity.CookieSessionAffinity.Hash}} name={{$upstream.SessionAffinity.CookieSessionAffinity.Name}} httponly; {{ else }} least_conn; {{ end }} {{ range $server := $upstream.Endpoints }}server {{ $server.Address }}:{{ $server.Port }} max_fails={{ $server.MaxFails }} fail_timeout={{ $server.FailTimeout }}; {{ end }} } {{ end }} {{/* build all the required rate limit zones. Each annotation requires a dedicated zone */}} {{/* 1MB -> 16 thousand 64-byte states or about 8 thousand 128-byte states */}} {{ range $zone := (buildRateLimitZones .Servers) }} {{ $zone }} {{ end }} {{ $backlogSize := .BacklogSize }} {{ range $index, $server := .Servers }} server { server_name {{ $server.Hostname }}; listen [::]:80{{ if $cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $index 0 }} ipv6only=off{{end}}{{ if eq $server.Hostname "_"}} default_server reuseport backlog={{ $backlogSize }}{{end}}; {{/* Listen on 442 because port 443 is used in the stream section */}} {{/* This listen cannot contains proxy_protocol directive because port 443 is in charge of decoding the protocol */}} {{ if not (empty $server.SSLCertificate) }}listen 442 {{ if eq $server.Hostname "_"}} default_server reuseport backlog={{ $backlogSize }}{{end}} ssl {{ if $cfg.UseHTTP2 }}http2{{ end }}; {{/* comment PEM sha is required to detect changes in the generated configuration and force a reload */}} # PEM sha: {{ $server.SSLPemChecksum }} ssl_certificate {{ $server.SSLCertificate }}; ssl_certificate_key {{ $server.SSLCertificate }}; {{ end }} {{ if (and (not (empty $server.SSLCertificate)) $cfg.HSTS) }} more_set_headers "Strict-Transport-Security: max-age={{ $cfg.HSTSMaxAge }}{{ if $cfg.HSTSIncludeSubdomains }}; includeSubDomains{{ end }}; preload"; {{ end }} {{ if $cfg.EnableVtsStatus }}vhost_traffic_status_filter_by_set_key $geoip_country_code country::$server_name;{{ end }} {{ range $location := $server.Locations }} {{ $path := buildLocation $location }} {{ $authPath := buildAuthLocation $location }} {{ if not (empty $location.CertificateAuth.CertFileName) }} # PEM sha: {{ $location.CertificateAuth.PemSHA }} ssl_client_certificate {{ $location.CertificateAuth.CAFileName }}; ssl_verify_client on; {{ end }} {{ if not (empty $authPath) }} location = {{ $authPath }} { internal; {{ if not $location.ExternalAuth.SendBody }} proxy_pass_request_body off; proxy_set_header Content-Length ""; {{ end }} {{ if not (empty $location.ExternalAuth.Method) }} proxy_method {{ $location.ExternalAuth.Method }}; {{ end }} proxy_set_header Host $host; proxy_pass_request_headers on; set $target {{ $location.ExternalAuth.URL }}; proxy_pass $target; } {{ end }} location {{ $path }} { set $proxy_upstream_name "{{ $location.Backend }}"; {{ if isLocationAllowed $location }} {{ if gt (len $location.Whitelist.CIDR) 0 }} {{ range $ip := $location.Whitelist.CIDR }} allow {{ $ip }};{{ end }} deny all; {{ end }} port_in_redirect {{ if $location.UsePortInRedirects }}on{{ else }}off{{ end }}; {{ if not (empty $authPath) }} # this location requires authentication auth_request {{ $authPath }}; {{ end }} {{ if (and (not (empty $server.SSLCertificate)) $location.Redirect.SSLRedirect) }} # enforce ssl on server side if ($scheme = http) { return 301 https://$host$request_uri; } {{ end }} {{/* if the location contains a rate limit annotation, create one */}} {{ $limits := buildRateLimit $location }} {{ range $limit := $limits }} {{ $limit }}{{ end }} {{ if $location.BasicDigestAuth.Secured }} {{ if eq $location.BasicDigestAuth.Type "basic" }} auth_basic "{{ $location.BasicDigestAuth.Realm }}"; auth_basic_user_file {{ $location.BasicDigestAuth.File }}; {{ else }} auth_digest "{{ $location.BasicDigestAuth.Realm }}"; auth_digest_user_file {{ $location.BasicDigestAuth.File }}; {{ end }} proxy_set_header Authorization ""; {{ end }} {{ if $location.EnableCORS }} {{ template "CORS" }} {{ end }} client_max_body_size "{{ $location.Proxy.BodySize }}"; proxy_set_header Host $host; # Pass Real IP proxy_set_header X-Real-IP $remote_addr; # Allow websocket connections proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $pass_port; proxy_set_header X-Forwarded-Proto $pass_access_scheme; # mitigate HTTPoxy Vulnerability # https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/ proxy_set_header Proxy ""; # Custom headers {{ range $k, $v := $proxyHeaders }} proxy_set_header {{ $k }} "{{ $v }}"; {{ end }} proxy_connect_timeout {{ $location.Proxy.ConnectTimeout }}s; proxy_send_timeout {{ $location.Proxy.SendTimeout }}s; proxy_read_timeout {{ $location.Proxy.ReadTimeout }}s; proxy_redirect off; proxy_buffering off; proxy_buffer_size "{{ $location.Proxy.BufferSize }}"; proxy_http_version 1.1; {{/* rewrite only works if the content is not compressed */}} {{ if $location.Redirect.AddBaseURL }} proxy_set_header Accept-Encoding ""; {{ end }} {{/* Add any additional configuration defined */}} {{ $location.ConfigurationSnippet }} {{ buildProxyPass $backends $location }} {{ else }} #{{ $location.Denied }} return 503; {{ end }} } {{ end }} {{ if eq $server.Hostname "_" }} # health checks in cloud providers require the use of port 80 location {{ $healthzURI }} { access_log off; return 200; } # this is required to avoid error if nginx is being monitored # with an external software (like sysdig) location /nginx_status { allow 127.0.0.1; allow ::1; deny all; access_log off; stub_status on; } {{ end }} {{ template "CUSTOM_ERRORS" $cfg }} } {{ end }} # default server, used for NGINX healthcheck and access to nginx stats server { # Use the port 18080 (random value just to avoid known ports) as default port for nginx. # Changing this value requires a change in: # https://github.com/kubernetes/contrib/blob/master/ingress/controllers/nginx/nginx/command.go#L104 listen [::]:18080 ipv6only=off default_server reuseport backlog={{ .BacklogSize }}; location {{ $healthzURI }} { access_log off; return 200; } location /nginx_status { {{ if $cfg.EnableVtsStatus }} vhost_traffic_status_display; vhost_traffic_status_display_format html; {{ else }} access_log off; stub_status on; {{ end }} } # this location is used to extract nginx metrics # using prometheus. # TODO: enable extraction for vts module. location /internal_nginx_status { allow 127.0.0.1; allow ::1; deny all; access_log off; stub_status on; } location / { set $proxy_upstream_name "upstream-default-backend"; proxy_pass http://upstream-default-backend; } {{ template "CUSTOM_ERRORS" $cfg }} } # default server for services without endpoints server { listen 8181; set $proxy_upstream_name "-"; location / { {{ if .CustomErrors }} content_by_lua_block { openURL(ngx.req.get_headers(0), 503) } {{ else }} return 503; {{ end }} } } } stream { # map FQDN that requires SSL passthrough map $ssl_preread_server_name $stream_upstream { {{ range $i, $passthrough := .PassthroughBackends }} {{ $passthrough.Hostname }} {{ $passthrough.Backend }}; {{ end }} # send SSL traffic to this nginx in a different port default nginx-ssl-backend; } log_format log_stream '$remote_addr [$time_local] $protocol [$ssl_preread_server_name] [$stream_upstream] $status $bytes_sent $bytes_received $session_time'; {{ if $cfg.DisableAccessLog }} access_log off; {{ else }} access_log /var/log/nginx/access.log log_stream; {{ end }} error_log /var/log/nginx/error.log; # configure default backend for SSL upstream nginx-ssl-backend { server 127.0.0.1:442; } {{ buildSSLPassthroughUpstreams $backends .PassthroughBackends }} server { listen [::]:443 ipv6only=off{{ if $cfg.UseProxyProtocol }} proxy_protocol{{ end }}; proxy_pass $stream_upstream; ssl_preread on; } {{ buildStreamUpstreams "tcp" $backends .TCPBackends }} {{ buildStreamUpstreams "udp" $backends .UDPBackends }} # TCP services {{ range $i, $tcpServer := .TCPBackends }} server { listen {{ $tcpServer.Path }}; proxy_pass tcp-{{ $tcpServer.Backend }}; } {{ end }} # UDP services {{ range $i, $udpServer := .UDPBackends }} server { listen {{ $udpServer.Path }} udp; proxy_responses 1; proxy_pass udp-{{ $udpServer.Backend }}; } {{ end }} } {{/* definition of templates to avoid repetitions */}} {{ define "CUSTOM_ERRORS" }} {{ range $errCode := .CustomHTTPErrors }} location @custom_{{ $errCode }} { internal; content_by_lua_block { openURL(ngx.req.get_headers(0), {{ $errCode }}) } } {{ end }} {{ end }} {{/* CORS support from https://michielkalkman.com/snippets/nginx-cors-open-configuration.html */}} {{ define "CORS" }} if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' '*'; # # Om nom nom cookies # add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; # # Custom headers and headers various browsers *should* be OK with but aren't # add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; # # Tell client that this pre-flight info is valid for 20 days # add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain charset=UTF-8'; add_header 'Content-Length' 0; return 204; } if ($request_method = 'POST') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; } if ($request_method = 'GET') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; } {{ end }}