How to solve websocket ping timeout issue - nginx

I have Django Channels (with Redis) served by Daphne, running behind Nginx ingress controller, proxying behind a LB, all setup in Kubernetes. The Websocket is upgraded and everything runs fine... for a few minutes. After between 5-15min (varies), my daphne logs (set in -v 2 to debug) show:
WARNING dropping connection to peer tcp4:10.2.0.163:43320 with abort=True: WebSocket ping timeout (peer did not respond with pong in time)
10.2.0.163 is the cluster IP address of my Nginx pod. Immediately after, Nginx logs the following:
[error] 39#39: *18644 recv() failed (104: Connection reset by peer) while proxying upgraded connection [... + client real IP]
After this, the websocket connection is getting wierd: the client can still send messages to the backend, but the same websocket connection in Django channels does not receive group messages anymore, as if the channel had unsubscribed from the group. I know my code works since everything runs smoothly until the error gets logged but I'm guessing there is a configuration error somewhere that causes the problem. I'm sadly all out of ideas. Here is my nginx ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
acme.cert-manager.io/http01-edit-in-place: "true"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.org/websocket-services: "daphne-svc"
name: ingress
namespace: default
spec:
tls:
- hosts:
- mydomain
secretName: letsencrypt-secret
rules:
- host: mydomain
http:
paths:
- path: /
backend:
service:
name: uwsgi-svc
port:
number: 80
pathType: Prefix
- path: /ws
backend:
service:
name: daphne-svc
port:
number: 80
pathType: Prefix
Configured according to this and this. Installation with helm:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ngingress ingress-nginx/ingress-nginx
Here is my Django Channels consumer:
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
user = self.scope['user']
if user.is_authenticated:
self.inbox_group_name = "inbox-%s" % user.id
device = self.scope.get('device', None)
added = False
if device:
added = await register_active_device(user, device)
if added:
# Join inbox group
await self.channel_layer.group_add(
self.inbox_group_name,
self.channel_name
)
await self.accept()
else:
await self.close()
else:
await self.close()
async def disconnect(self, close_code):
user = self.scope['user']
device = self.scope.get('device', None)
if device:
await unregister_active_device(user, device)
# Leave room group
if hasattr(self, 'inbox_group_name'):
await self.channel_layer.group_discard(
self.inbox_group_name,
self.channel_name
)
"""
Receive message from room group; forward it to client
"""
async def group_message(self, event):
message = event['message']
# Send message to WebSocket
await self.send(text_data=json.dumps(message))
async def forward_message_to_other_members(self, chat, message, notification_fallback=False):
user = self.scope['user']
other_members = await get_other_chat_members(chat, user)
for member in other_members:
if member.active_devices_count > 0:
#this will send the message to the user inbox; each consumer will handle it with the group_message method
await self.channel_layer.group_send(
member.inbox.group_name,
{
'type': 'group_message',
'message': message
}
)
else:
#no connection for this user, send a notification instead
if notification_fallback:
await ChatNotificationHandler().send_chat_notification(chat, message, recipient=member, author=user)

I ended up adding a ping internal on the client and increasing nginx timeout to 1 day, which changed the problem but also shows it's probably not a nginx/daphne configuration problem.

Related

Nginx Ingress Controller converting JSON request into XML

Client sends a HTTP POST request with JSON payload and content type "application/json".
Backend REST service that is proxied by NGINX (via Ingress Controller in k8s) receives the request with content type "application/xml" and the JSON is converted to a weird XML message. Below is my ingress object which has client auth turned on for mTLS:
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
nginx.ingress.kubernetes.io/auth-tls-secret: xyz/xyz-secret
nginx.ingress.kubernetes.io/auth-tls-verify-depth: "2"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
name: xyz-ingress
namespace: xyz
spec:
ingressClassName: nginx
rules:
- host: host_name
http:
paths:
- backend:
service:
name: api
port:
number: 80
path: /xyz/api
pathType: Prefix
tls:
- hosts:
- host_name
secretName: xyz-secret```
This is the request payload in XML received at the backend rest server:
```<.Proxy209 xmlns=""><tenant>xxx</tenant><serviceType>xxx</serviceType></.Proxy209>
The client sends out this JSON:
```{ "tenant": "xxx", "serviceType": "xxx" }```
The JSON is converted to the XML above by NGINX.

Ambassador Edge Stack JWT filter with Firebase token not working

I'm trying to verify a Firebase generated JWT token with an Ambassador Edge Stack (datawire/edge-stack version 3.3.0) Filter.
The Firebase token is generated using a login/password authent mechanism on firebase, something like (in Python):
email=input("Enter email: ")
password=input("Enter password: ")
user = authentication.sign_in_with_email_and_password(email, password)
custom_token = auth.create_custom_token(user["localId"], additional_claims)
print("JWT Token :")
print(custom_token)
After the token is generated, I use it with a curl command such as:
curl -H "Authorization: Bearer $TOKEN" https://ambassador-ip.nip.io/hello-world/
and the curl command returns the following error:
},
"message": "Token validation error: token is invalid: errorFlags=0x00000002=(ValidationErrorUnverifiable) wrappedError=(KeyID=\"50***redacted***1\": JWK not found)",
"status_code": 401
}
Here is the ambassador Filter I've declared:
apiVersion: getambassador.io/v2
kind: Filter
metadata:
name: "firebase-filter"
namespace: ${kubernetes_namespace.hello_world.metadata[0].name}
spec:
JWT:
jwksURI: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken#system.gserviceaccount.com"
audience: "${local.project_id}"
issuer: "https://securetoken.google.com/${local.project_id}"
And the policy filter applied to my backend:
apiVersion: getambassador.io/v3alpha1
kind: FilterPolicy
metadata:
name: "firebase-filter-policy"
namespace: ${kubernetes_namespace.hello_world.metadata[0].name}
spec:
rules:
- host: "*"
path: "/hello-world/"
filters:
- name: "firebase-filter"
namespace: "${kubernetes_namespace.hello_world.metadata[0].name}"
For the record, the curl command with the same token works on a deployed hello-world Cloud Run with a GCP API gateway configured as follow:
swagger: '2.0'
info:
title: Example Firebase auth Gateway
description: API Gateway with firebase auth
version: 1.0.0
schemes:
- https
produces:
- application/json
securityDefinitions:
firebase:
authorizationUrl: ''
flow: implicit
type: oauth2
x-google-issuer: "https://securetoken.google.com/${project_id}"
x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken#system.gserviceaccount.com"
x-google-audiences: "${project_id}"
paths:
/v1/hello:
get:
security:
- firebase: []
description: Hello
operationId: hello
responses:
'200':
description: Success
x-google-backend:
address: 'https://hello-redacted-ew.a.run.app'
Any idea why the Ambassador filter is misconfigured ?
Ambassador JWT Filter needs the jwksURI to point to the Firebase secure token service account public keys and not the X509 certificates, therefore the Filter should be:
apiVersion: getambassador.io/v2
kind: Filter
metadata:
name: "firebase-filter"
namespace: ${kubernetes_namespace.hello_world.metadata[0].name}
spec:
JWT:
jwksURI: "https://www.googleapis.com/service_accounts/v1/jwk/securetoken#system.gserviceaccount.com"
audience: "${local.project_id}"
issuer: "https://securetoken.google.com/${local.project_id}"
This is working for Firebase tokens only. If you want to make this works with Custom tokens using some dedicated service account for example, you might need the jwksURI to point to your service account public keys, something like:
apiVersion: getambassador.io/v2
kind: Filter
metadata:
name: "firebase-custom-filter"
namespace: ${kubernetes_namespace.hello_world.metadata[0].name}
spec:
JWT:
jwksURI: "https://www.googleapis.com/service_accounts/v1/jwk/${service_account}#${local.project_id}.iam.gserviceaccount.com"
audience: "${local.project_id}"
issuer: "https://securetoken.google.com/${local.project_id}"
The JWT Filter requires you to provide the url for the .well-known/openid-configuration so that it can verify the signature of the token. I'm not familiar with Firebase but looking on their docs it appears you can find this here:
https://firebase.google.com/docs/auth/web/openid-connect
For example your Filter should be configured something like the following (i'm guessing on the jwksURI):
apiVersion: getambassador.io/v2
kind: Filter
metadata:
name: "firebase-filter"
namespace: ${kubernetes_namespace.hello_world.metadata[0].name}
spec:
JWT:
jwksURI: "https://securetoken.google.com/${local.project_id}/.well-known/openid-configuration"
audience: "${local.project_id}"
issuer: "https://securetoken.google.com/${local.project_id}"

Ingress nginx and gRPC comunication service error

I've deployed Nginx Ingress Controller, manifest:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: namespace-servicename
name: servicename
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-cf-dns"
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
tls:
- hosts:
- roabc.domain.net
secretName: wildcard-roudh-domain-net
ingressClassName: public
rules:
- host: roabc.domain.net
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: servicename
port:
number: 80
kubectl describe svc servicename -n namespace-servicename
Name: servicename
Namespace: namespace-servicename
Labels: app=ro**
continent=eu
hub-id=eu-**-mad013
Annotations: <none>
Selector: app=ro**,continent=eu,hub-id=eu-**-mad013
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.152.183.44
IPs: 10.152.183.44
Port: grpc 80/TCP
TargetPort: 80/TCP
Endpoints: 10.1.252.108:80
Session Affinity: None
Events: <none>
But, unfortunately, grpcurl can't reach gRPC application, throws error rpc error: code = Internal desc = failed to query for service descriptor"*.services..SearchList":stream terminated by RST_STREAM with error code: PROTOCOL_ERROR
grpcurl -d "#" -H "authorization: Bt** dert**" roabc.domain.net:443
Error invoking method"***.services.**.SearchList": rpc error: code = Internal desc = failed to query for service descriptor"***.services.**.SearchList": **stream terminated by RST_STREAM with error code: PROTOCOL_ERROR**
gRPC app running in a Pod, constantly gives this error: OPENSSL_internal:WRONG_VERSION_NUMBER
**ssl_transport_security.cc:1455] Handshake failed with fatal error SSL_ERROR_SSL: error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER.**
If you prefer to forward encrypted traffic to your POD and terminate TLS at the gRPC server itself, add the ingress annotation
nginx.ingress.kubernetes.io/backend-protocol: "GRPCS"

Block particular path on ingress-nginx Loadbalancer

I have many domain pointing to Ingress Controller IP. I want to block /particular-path for all the domains/sites. Is there a way to do this.
I can use nginx.ingress.kubernetes.io/configuration-snippet: | for each site. But looking for way to do for all sites/domains/Ingress resource at once.
Controller used: https://kubernetes.github.io/ingress-nginx/
There are two ways to achieve this:
1. First one is with using server-snippet annotation:
Using the annotation nginx.ingress.kubernetes.io/server-snippet it
is possible to add custom configuration in the server configuration
block.
Here is my manifest for the ingress object:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: minimal-ingress
annotations:
nginx.ingress.kubernetes.io/server-snippet: |
location ~* /admin-access {
deny all;
return 403;
}
spec:
rules:
- host: domain.com
http:
paths:
- path: /
backend:
serviceName: web
servicePort: 80
Please note that using this approach :
This annotation can be used only once per host.
2. Second one is with usage of ConfigMaps and Server-snippet:
What you have to do is to locate your configMap:
kubectl get pod <nginx-ingress-controller> -o yaml
This is located the container args:
spec:
containers:
- args:
- /nginx-ingress-controller
- configmap=$(POD_NAMESPACE)/nginx-loadbalancer-conf
And then just edit it and place add the server-snippet part:
apiVersion: v1
data:
server-snippet: |
location /admin-access {
deny all;
}
This approach allows you to define restricted location globally for all host defined in Ingress resource.
Please note that with usage of server-snippet the path that you are blocking cannot be defined in ingress resource object. There is however another way with location-snippet via ConfigMap:
location ~* "^/web/admin {
deny all;
}
With this for every existing path in ingress object there will be ingress rule but it will be blocked for specific uri (In the example above it be be blocked when admin will appear after web). All of the other uri will be passed through.
3. Here`s a test:
➜ curl -H "Host: domain.com" 172.17.0.4/test
...
"path": "/test",
"headers": {
...
},
"method": "GET",
"body": "",
"fresh": false,
"hostname": "domain.com",
"ip": "172.17.0.1",
"ips": [
"172.17.0.1"
],
"protocol": "http",
"query": {},
"subdomains": [],
"xhr": false,
"os": {
"hostname": "web-6b686fdc7d-4pxt9"
...
And here is a test with a path that has been denied:
➜ curl -H "Host: domain.com" 172.17.0.4/admin-access
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.19.0</center>
</body>
</html>
➜ curl -H "Host: domain.com" 172.17.0.4/admin-access/test
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.19.0</center>
</body>
</html>
Additional information: Deprecated APIs Removed In 1.16. Here’s What You Need To Know:
The v1.22 release will stop serving the following deprecated API
versions in favor of newer and more stable API versions:
Ingress in the extensions/v1beta1 API version will no longer be
served
You cannot block specific paths. What you can do is point the path of the host inside your ingress to a default backedn application that says 404 default backedn for example.
you can apply it using the ingress annotation
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: channel-dev
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/whitelist-source-range: "27.110.30.45, 68.50.85.421"
name: dev-ingress
namespace: development
spec:
rules:
- host: hooks.dev.example.com
http:
paths:
- backend:
serviceName: hello-service
servicePort: 80
path: /incoming/message/
tls:
- hosts:
- hooks.dev.example.com
secretName: channel-dev
path https://hooks.dev.example.com/incoming/message/ will be only accessible from mentioned IPs other users will get 403 error and wont be able to access the URL.
just add this annotation in ingress
nginx.ingress.kubernetes.io/whitelist-source-range

How to debug ingress-controller connections with a single IP by ConfigMap

We are trying to edit our ingress-nginx.yml to make ingress-controllers pods debug traffic coming from a specific source IP.
Our setup is:
Kubernetes v1.13
Ingress-Controller v0.24.1
From NGINX and Kubernetes DOCs it appears there is no very easy way to debug traffic from a single ip (you cannot edit the nginx config directly). So, we would like to add the debug_connection directive to appear like this:
error_log /path/to/log;
...
events {
debug_connection 192.168.1.1;
}
The correct way to do it shall be through CustomAnnotations in a ConfigMap + a new ingress to enable the CustomAnnotation, so we tried this:
kind: ConfigMap
apiVersion: v1
metadata:
name: nginx-configuration
namespace: ingress-nginx
labels:
app: ingress-nginx
data:
ingress-template: |
#Creating the custom annotation to make debug_connection on/off
{if index $.Ingress.Annotations "custom.nginx.org/debug_connection"}
{$ip := index $.Ingress.Annotations "custom.nginx.org/ip"}
{end}
{range $events := .Events}
events {
# handling custom.nginx.org/debug_connection
{if index $.Ingress.Annotations "custom.nginx.org/debug_connection"}
{end}
And:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: debugenabler
annotations:
kubernetes.io/ingress.class: "nginx"
custom.nginx.org/debug_connection: "on"
custom.nginx.org/ip: "192.168.1.1"
spec:
rules:
- host: "ourhostname"
http:
paths:
- path: /tea
backend:
serviceName: tea-svc
servicePort: 80
- path: /coffee
backend:
serviceName: coffee-svc
servicePort: 80
We applied ingress-nginx.yml with no errors. We see new lines in the nginx conf:
location /coffee {
set $namespace "test";
set $ingress_name "debugenabler";
set $service_name "coffee-svc";
set $service_port "80";
set $location_path "/coffee";
rewrite_by_lua_block {
lua_ingress.rewrite({
force_ssl_redirect = true,
use_port_in_redirects = false,
})
balancer.rewrite()
But still nothing as regard the debug_connection in the events block:
events {
multi_accept on;
worker_connections 16384;
use epoll;
}
How to insert debug_connection in the events context ?
For those who may face similar challenges, I actually managed to do it by:
Creating a ConfigMap with a new ingress-controller template file (nginx.tmpl) containing the debug_connection line (double check your ingress-controller version here, the file changes dramatically)
Creating a Volume which links at the Configmap (specifying Volume and Volumemount)
Creating a InitContainer which copy the content of the volume inside the /etc/nginx/template (this was needed to overcome probably permission-related issues) before the container start.
For point 2 and 3 you can add the relevant code at the end of a deployment or a pod code, I share an example:
volumes:
- name: nginxconf2
configMap:
name: nginxconf2
items:
- key: nginx.tmpl
path: nginx.tmpl
initContainers:
- name: copy-configs
image: {{ kubernetes.ingress_nginx.image }}
volumeMounts:
- mountPath: /nginx
name: nginxconf2
command: ['sh', '-c', 'cp -R /nginx/ /etc/nginx/template/']

Resources