From a2b4edde59b6b76290912b5937b5f7248fd366e8 Mon Sep 17 00:00:00 2001 From: Anurag Singh Rajawat Date: Tue, 18 Feb 2025 13:12:57 +0530 Subject: [PATCH 1/2] feat(receiver): Add an option to automatically configure Nginx Inc. ingress Signed-off-by: Anurag Singh Rajawat --- deployments/sentryflow.yaml | 164 ---- deployments/sentryflow/Readme.md | 2 +- .../sentryflow/templates/clusterrole.yaml | 8 + .../sentryflow-configmap-for-nginx-inc.yaml | 118 +++ deployments/sentryflow/values.yaml | 42 +- hack/add-license-header | 2 +- sentryflow/Makefile | 2 +- sentryflow/config/default.yaml | 15 +- sentryflow/pkg/config/config.go | 51 +- sentryflow/pkg/config/config_test.go | 172 ++-- sentryflow/pkg/core/sentryflow.go | 7 +- sentryflow/pkg/core/server.go | 1 + sentryflow/pkg/k8s/client.go | 3 + .../other/nginx/nginxinc/autoconfigure.go | 287 ++++++ .../nginx/nginxinc/autoconfigure_test.go | 847 ++++++++++++++++++ .../receiver/other/nginx/nginxinc/nginx.go | 32 +- 16 files changed, 1434 insertions(+), 319 deletions(-) delete mode 100644 deployments/sentryflow.yaml create mode 100644 deployments/sentryflow/templates/sentryflow-configmap-for-nginx-inc.yaml create mode 100644 sentryflow/pkg/receiver/other/nginx/nginxinc/autoconfigure.go create mode 100644 sentryflow/pkg/receiver/other/nginx/nginxinc/autoconfigure_test.go diff --git a/deployments/sentryflow.yaml b/deployments/sentryflow.yaml deleted file mode 100644 index 91497ad..0000000 --- a/deployments/sentryflow.yaml +++ /dev/null @@ -1,164 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: sentryflow - labels: - app.kubernetes.io/part-of: sentryflow ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: sentryflow - namespace: sentryflow - labels: - app.kubernetes.io/part-of: sentryflow ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: sentryflow - labels: - app.kubernetes.io/part-of: sentryflow -rules: - - apiGroups: - - networking.istio.io - verbs: - - get - - create - - delete - resources: - - envoyfilters - - apiGroups: - - extensions.istio.io - verbs: - - get - - create - - delete - resources: - - wasmplugins - - apiGroups: - - "" - verbs: - - get - resources: - - configmaps - - apiGroups: - - apps - verbs: - - get - resources: - - deployments ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: sentryflow - labels: - app.kubernetes.io/part-of: sentryflow -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: sentryflow -subjects: - - kind: ServiceAccount - name: sentryflow - namespace: sentryflow ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: sentryflow - namespace: sentryflow - labels: - app.kubernetes.io/part-of: sentryflow -data: - config.yaml: |2- - filters: - server: - port: 8081 - envoy: - uri: 5gsec/sentryflow-httpfilter:latest - - receivers: - serviceMeshes: - - name: istio-sidecar - namespace: istio-system - - exporter: - grpc: - port: 8080 ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: sentryflow - namespace: sentryflow - labels: - app.kubernetes.io/part-of: sentryflow -spec: - replicas: 1 - selector: - matchLabels: - app: sentryflow - template: - metadata: - labels: - app: sentryflow - spec: - serviceAccountName: sentryflow - containers: - - name: sentryflow - image: docker.io/5gsec/sentryflow:latest - imagePullPolicy: Always - args: - - --config - - /var/lib/sentryflow/config.yaml - volumeMounts: - - mountPath: /var/lib/sentryflow/ - name: sentryflow - ports: - - containerPort: 8080 - name: exporter - protocol: TCP - securityContext: - capabilities: - drop: - - ALL - readOnlyRootFilesystem: true - runAsNonRoot: true - runAsUser: 1111 - allowPrivilegeEscalation: false - readinessProbe: - httpGet: - port: 8081 # Make sure to use the same port as `.filters.server.port` field in configMap - path: /healthz - httpHeaders: - - name: status - value: "200" - initialDelaySeconds: 5 - terminationGracePeriodSeconds: 30 - volumes: - - name: sentryflow - configMap: - name: sentryflow - defaultMode: 420 ---- -apiVersion: v1 -kind: Service -metadata: - namespace: sentryflow - name: sentryflow - labels: - app.kubernetes.io/part-of: sentryflow -spec: - selector: - app: sentryflow - ports: - - name: exporter - port: 8080 - targetPort: 8080 - protocol: TCP - - name: filter-server - port: 8081 # Make sure to use the same port as `.filters.server.port` field in configMap - targetPort: 8081 # Make sure to use the same port as `.filters.server.port` field in configMap - protocol: TCP diff --git a/deployments/sentryflow/Readme.md b/deployments/sentryflow/Readme.md index 560b227..3251b68 100644 --- a/deployments/sentryflow/Readme.md +++ b/deployments/sentryflow/Readme.md @@ -20,7 +20,7 @@ Configure SentryFlow receiver by following [this](../../docs/receivers.md). ### Deploy SentryFlow ```shell -helm install --values values.yaml sentryflow 5gsec/sentryflow-n sentryflow --create-namespace +helm install --values values.yaml sentryflow 5gsec/sentryflow -n sentryflow --create-namespace ``` Install SentryFlow using Helm charts locally (for testing) diff --git a/deployments/sentryflow/templates/clusterrole.yaml b/deployments/sentryflow/templates/clusterrole.yaml index 7ff37f6..5b2bb77 100644 --- a/deployments/sentryflow/templates/clusterrole.yaml +++ b/deployments/sentryflow/templates/clusterrole.yaml @@ -29,11 +29,19 @@ rules: - "" verbs: - get + - update resources: - configmaps + - apiGroups: + - "" + verbs: + - list + resources: + - services - apiGroups: - apps verbs: - get + - update resources: - deployments diff --git a/deployments/sentryflow/templates/sentryflow-configmap-for-nginx-inc.yaml b/deployments/sentryflow/templates/sentryflow-configmap-for-nginx-inc.yaml new file mode 100644 index 0000000..c1f0701 --- /dev/null +++ b/deployments/sentryflow/templates/sentryflow-configmap-for-nginx-inc.yaml @@ -0,0 +1,118 @@ +{{- range .Values.config.receivers.others}} +{{- if eq .name "nginx-inc-ingress-controller" }} +{{- if .autoConfigure }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $.Values.config.filters.nginxIngress.sentryFlowNjsConfigMapName }} + namespace: {{ .namespace }} + labels: + {{- with $.Values.genericLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} +data: + sentryflow.js: | + const DEFAULT_KEY = "sentryFlow"; + const ResStatusKey = ":status" + const MAX_BODY_SIZE = 1_000_000; // 1 MB + + function requestHandler(r, data, flags) { + // https://nginx.org/en/docs/njs/reference.html#r_sendbuffer + r.sendBuffer(data, flags); + + // https://nginx.org/en/docs/njs/reference.html#r_done + r.done(); + + let responseBody = "" + try { + responseBody = new TextDecoder("utf-8") + .decode(new Uint8Array(data)); + } catch (error) { + r.error(`failed to decode data, error: ${error}`) + // Do not return, process other info even without body. + } + + if (responseBody.length > MAX_BODY_SIZE) { + responseBody = "" + } + + let apiEvent = { + "metadata": { + // Divide by 1000 converts the timestamp from milliseconds to seconds. + "timestamp": Date.parse(r.variables.time_iso8601.split("+")[0]) / 1000, + "receiver_name": "nginx", + "receiver_version": ngx.version, + }, + "source": { + "ip": r.remoteAddress, + "port": r.variables.remote_port, + }, + "destination": { + "ip": r.variables.server_addr, + "port": r.variables.server_port, + }, + "request": { + "headers": {}, + "body": r.requestText || "", + }, + "response": { + "headers": {}, + "body": responseBody, + }, + "protocol": r.variables.server_protocol, + }; + + for (const header in r.headersIn) { + apiEvent.request.headers[header] = r.headersIn[header]; + } + + // https://nginx.org/en/docs/http/ngx_http_core_module.html#variables + apiEvent.request.headers[":scheme"] = r.variables.scheme + apiEvent.request.headers[":path"] = r.uri + apiEvent.request.headers[":method"] = r.variables.request_method + + // Number of bytes sent to a client, not counting the response header; this + // variable is compatible with the “%B” parameter of the mod_log_config Apache module. + apiEvent.request.headers["body_bytes_sent"] = r.variables.body_bytes_sent + + // Request length including request line, header, and request body. + apiEvent.request.headers["request_length"] = r.variables.request_length + + // Request processing time in seconds with a milliseconds resolution; + // Time elapsed since the first bytes were read from the client. + apiEvent.request.headers["request_time"] = r.variables.request_time + + // Query (args) in the request line. + apiEvent.request.headers["query"] = r.variables.query_string + + for (const header in r.headersOut) { + apiEvent.response.headers[header] = r.headersOut[header]; + } + apiEvent.response.headers[ResStatusKey] = r.variables.status + + // https://nginx.org/en/docs/njs/reference.html#ngx_shared + ngx.shared.apievents.set(DEFAULT_KEY, JSON.stringify(apiEvent)); + } + + async function dispatchHttpCall(r) { + try { + let apiEvent = ngx.shared.apievents.get(DEFAULT_KEY); + await r.subrequest("/sentryflow", { + method: "POST", body: apiEvent, detached: true + }) + } catch (error) { + r.error(`failed to dispatch HTTP call to SentryFlow, error: ${error}`) + return; + } finally { + ngx.shared.apievents.clear(); + } + + r.return(200, "OK"); + } + + export default { + requestHandler, dispatchHttpCall + }; +{{- end }} +{{- end }} +{{- end }} diff --git a/deployments/sentryflow/values.yaml b/deployments/sentryflow/values.yaml index a07c815..7426aee 100644 --- a/deployments/sentryflow/values.yaml +++ b/deployments/sentryflow/values.yaml @@ -89,30 +89,32 @@ config: filters: server: port: 9999 - # Envoy filter is required for `istio-sidecar` service-mesh receiver. - # Uncomment the following if you want to use `istio-sidecar` traffic source - # envoy: - # uri: 5gsec/sentryflow-httpfilter:latest + # Envoy filter is required for `istio-sidecar` service-mesh receiver. + envoy: + uri: 5gsec/sentryflow-httpfilter:latest - # Following is required for `nginx-inc-ingress-controller` receiver. - # Uncomment the following if you want to use `nginx-inc-ingress-controller` traffic source - # nginxIngress: - # deploymentName: nginx-ingress-controller - # configMapName: nginx-ingress - # sentryFlowNjsConfigMapName: sentryflow-njs + # Following is required for `nginx-inc-ingress-controller` receiver. + # Uncomment the following if you want to use `nginx-inc-ingress-controller` traffic source + nginxIngress: + deploymentName: nginx-ingress-controller + configMapName: nginx-ingress + sentryFlowNjsConfigMapName: sentryflow-nginx-inc receivers: # aka sources - # Uncomment the following receivers according to your requirement. - # serviceMeshes: - # To get API observability from Istio service mesh uncomment the following - # - name: istio-sidecar - # namespace: istio-system + # Uncomment the following receivers according to your requirement. +# serviceMeshes: + # To get API observability from Istio service mesh uncomment the following +# - name: istio-sidecar +# namespace: istio-system - # others: - # To get API observability from F5 nginx ingress controller uncomment the following - # - name: nginx-inc-ingress-controller - # namespace: default +# others: + # # To get API observability from F5 nginx ingress controller uncomment the following +# - name: nginx-inc-ingress-controller +# namespace: nginx-ingress + # Set to `true` to automatically configure the Nginx Inc. Ingress Controller deployment + # to enable HTTP access log observability. +# autoConfigure: true - # - name: nginx-webserver +# - name: nginx-webserver exporter: grpc: port: 8888 diff --git a/hack/add-license-header b/hack/add-license-header index a92bfb5..e80eef6 100755 --- a/hack/add-license-header +++ b/hack/add-license-header @@ -8,7 +8,7 @@ if ! command -v addlicense >/dev/null; then fi GIT_ROOT=$(git rev-parse --show-toplevel) -LICENSE_HEADER=${GIT_ROOT}/scripts/license.header +LICENSE_HEADER=${GIT_ROOT}/hack/license.header if [ -z "$1" ]; then echo "No Argument Supplied, Checking and Fixing all files from project root" diff --git a/sentryflow/Makefile b/sentryflow/Makefile index 53d9647..161779d 100644 --- a/sentryflow/Makefile +++ b/sentryflow/Makefile @@ -42,7 +42,7 @@ lint: golangci-lint ## Run golangci-lint linter .PHONY: license license: ## Check and fix license header on all go files - @../scripts/add-license-header + @../hack/add-license-header .PHONY: test test: ## Run unit tests diff --git a/sentryflow/config/default.yaml b/sentryflow/config/default.yaml index d7f1980..fab8ada 100644 --- a/sentryflow/config/default.yaml +++ b/sentryflow/config/default.yaml @@ -3,17 +3,17 @@ filters: server: - port: 8081 + port: 9999 # Envoy filter is required for `istio-sidecar` service-mesh receiver. # envoy: # uri: 5gsec/sentryflow-httpfilter:latest # Following is required for `nginx-inc-ingress-controller` receiver. -# nginxIngress: -# deploymentName: nginx-ingress-controller -# configMapName: nginx-ingress -# sentryFlowNjsConfigMapName: sentryflow-njs + nginxIngress: + deploymentName: nginx-ingress-controller + configMapName: nginx-ingress + sentryFlowNjsConfigMapName: sentryflow-nginx-inc receivers: # aka sources # Uncomment the following receivers according to your requirement. @@ -24,10 +24,11 @@ receivers: # aka sources # others: # - name: nginx-inc-ingress-controller -# namespace: default +# namespace: nginx-ingress +# autoConfigure: false # - name: nginx-webserver exporter: grpc: - port: 8080 + port: 8888 diff --git a/sentryflow/pkg/config/config.go b/sentryflow/pkg/config/config.go index 65f05dc..c5af136 100644 --- a/sentryflow/pkg/config/config.go +++ b/sentryflow/pkg/config/config.go @@ -15,47 +15,48 @@ import ( const ( DefaultConfigFilePath = "config/default.yaml" - SentryFlowDefaultFilterServerPort = 8081 + SentryFlowDefaultFilterServerPort = 9999 ) -type nameAndNamespace struct { - Name string `json:"name"` - Namespace string `json:"namespace,omitempty"` +type NameAndNamespace struct { + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` + AutoConfigure bool `json:"autoconfigure,omitempty"` } -type receivers struct { - ServiceMeshes []*nameAndNamespace `json:"serviceMeshes,omitempty"` - Others []*nameAndNamespace `json:"other,omitempty"` +type Receivers struct { + ServiceMeshes []*NameAndNamespace `json:"serviceMeshes,omitempty"` + Others []*NameAndNamespace `json:"other,omitempty"` } -type envoyFilterConfig struct { +type EnvoyFilterConfig struct { Uri string `json:"uri"` } -type server struct { +type Server struct { Port uint16 `json:"port"` } -type nginxIngressConfig struct { +type NginxIngressConfig struct { DeploymentName string `json:"deploymentName"` ConfigMapName string `json:"configMapName"` SentryFlowNjsConfigMapName string `json:"sentryFlowNjsConfigMapName"` } -type filters struct { - Envoy *envoyFilterConfig `json:"envoy,omitempty"` - NginxIngress *nginxIngressConfig `json:"nginxIngress,omitempty"` - Server *server `json:"server,omitempty"` +type Filters struct { + Envoy *EnvoyFilterConfig `json:"envoy,omitempty"` + NginxIngress *NginxIngressConfig `json:"nginxIngress,omitempty"` + Server *Server `json:"server,omitempty"` } -type exporterConfig struct { - Grpc *server `json:"grpc"` +type ExporterConfig struct { + Grpc *Server `json:"grpc"` } type Config struct { - Filters *filters `json:"filters"` - Receivers *receivers `json:"receivers"` - Exporter *exporterConfig `json:"exporter"` + Filters *Filters `json:"filters"` + Receivers *Receivers `json:"receivers"` + Exporter *ExporterConfig `json:"exporter"` } func (c *Config) validate() error { @@ -137,7 +138,7 @@ func New(configFilePath string, logger *zap.SugaredLogger) (*Config, error) { return nil, err } if config.Filters.Server == nil { - config.Filters.Server = &server{} + config.Filters.Server = &Server{} } if config.Filters.Server.Port == 0 { config.Filters.Server.Port = SentryFlowDefaultFilterServerPort @@ -148,11 +149,13 @@ func New(configFilePath string, logger *zap.SugaredLogger) (*Config, error) { return nil, err } - bytes, err := json.Marshal(config) - if err != nil { - logger.Errorf("Failed to marshal config file: %v", err) + if logger.Level() == zap.DebugLevel { + bytes, err := json.Marshal(config) + if err != nil { + logger.Errorf("Failed to marshal config file: %v", err) + } + logger.Debugf("Config: %s", string(bytes)) } - logger.Debugf("Config: %s", string(bytes)) return config, nil } diff --git a/sentryflow/pkg/config/config_test.go b/sentryflow/pkg/config/config_test.go index 50360f2..160ec0a 100644 --- a/sentryflow/pkg/config/config_test.go +++ b/sentryflow/pkg/config/config_test.go @@ -15,9 +15,9 @@ import ( func TestConfig_validate(t *testing.T) { type fields struct { - Filters *filters - Receivers *receivers - Exporter *exporterConfig + Filters *Filters + Receivers *Receivers + Exporter *ExporterConfig } tests := []struct { name string @@ -29,16 +29,16 @@ func TestConfig_validate(t *testing.T) { name: "with nil filter config should return error", fields: fields{ Filters: nil, - Receivers: &receivers{ - ServiceMeshes: []*nameAndNamespace{ + Receivers: &Receivers{ + ServiceMeshes: []*NameAndNamespace{ { Name: "istio-sidecar", Namespace: "istio-system", }, }, }, - Exporter: &exporterConfig{ - Grpc: &server{ + Exporter: &ExporterConfig{ + Grpc: &Server{ Port: 11111, }, }, @@ -49,21 +49,21 @@ func TestConfig_validate(t *testing.T) { { name: "with empty envoy URI in filter should return error", fields: fields{ - Filters: &filters{ - Envoy: &envoyFilterConfig{ + Filters: &Filters{ + Envoy: &EnvoyFilterConfig{ Uri: "", }, }, - Receivers: &receivers{ - ServiceMeshes: []*nameAndNamespace{ + Receivers: &Receivers{ + ServiceMeshes: []*NameAndNamespace{ { Name: "istio-sidecar", Namespace: "istio-system", }, }, }, - Exporter: &exporterConfig{ - Grpc: &server{ + Exporter: &ExporterConfig{ + Grpc: &Server{ Port: 11111, }, }, @@ -74,16 +74,16 @@ func TestConfig_validate(t *testing.T) { { name: "with nil exporter config should return error", fields: fields{ - Filters: &filters{ - Envoy: &envoyFilterConfig{ + Filters: &Filters{ + Envoy: &EnvoyFilterConfig{ Uri: "5gsec/http-filter:v0.1", }, - Server: &server{ + Server: &Server{ Port: SentryFlowDefaultFilterServerPort, }, }, - Receivers: &receivers{ - ServiceMeshes: []*nameAndNamespace{ + Receivers: &Receivers{ + ServiceMeshes: []*NameAndNamespace{ { Name: "istio-sidecar", Namespace: "istio-system", @@ -98,23 +98,23 @@ func TestConfig_validate(t *testing.T) { { name: "with nil exporter gRPC config should return error", fields: fields{ - Filters: &filters{ - Envoy: &envoyFilterConfig{ + Filters: &Filters{ + Envoy: &EnvoyFilterConfig{ Uri: "5gsec/http-filter:v0.1", }, - Server: &server{ + Server: &Server{ Port: SentryFlowDefaultFilterServerPort, }, }, - Receivers: &receivers{ - ServiceMeshes: []*nameAndNamespace{ + Receivers: &Receivers{ + ServiceMeshes: []*NameAndNamespace{ { Name: "istio-sidecar", Namespace: "istio-system", }, }, }, - Exporter: &exporterConfig{ + Exporter: &ExporterConfig{ Grpc: nil, }, }, @@ -124,24 +124,24 @@ func TestConfig_validate(t *testing.T) { { name: "without exporter's gRPC port config should return error", fields: fields{ - Filters: &filters{ - Envoy: &envoyFilterConfig{ + Filters: &Filters{ + Envoy: &EnvoyFilterConfig{ Uri: "5gsec/http-filter:v0.1", }, - Server: &server{ + Server: &Server{ Port: SentryFlowDefaultFilterServerPort, }, }, - Receivers: &receivers{ - ServiceMeshes: []*nameAndNamespace{ + Receivers: &Receivers{ + ServiceMeshes: []*NameAndNamespace{ { Name: "istio-sidecar", Namespace: "istio-system", }, }, }, - Exporter: &exporterConfig{ - Grpc: &server{}, + Exporter: &ExporterConfig{ + Grpc: &Server{}, }, }, wantErr: true, @@ -150,17 +150,17 @@ func TestConfig_validate(t *testing.T) { { name: "with nil receiver config should return error", fields: fields{ - Filters: &filters{ - Envoy: &envoyFilterConfig{ + Filters: &Filters{ + Envoy: &EnvoyFilterConfig{ Uri: "5gsec/http-filter:v0.1", }, - Server: &server{ + Server: &Server{ Port: SentryFlowDefaultFilterServerPort, }, }, Receivers: nil, - Exporter: &exporterConfig{ - Grpc: &server{ + Exporter: &ExporterConfig{ + Grpc: &Server{ Port: 11111, }, }, @@ -171,22 +171,22 @@ func TestConfig_validate(t *testing.T) { { name: "with istio-sidecar svcmesh and without envoy URI should return error", fields: fields{ - Filters: &filters{ + Filters: &Filters{ Envoy: nil, - Server: &server{ + Server: &Server{ Port: SentryFlowDefaultFilterServerPort, }, }, - Receivers: &receivers{ - ServiceMeshes: []*nameAndNamespace{ + Receivers: &Receivers{ + ServiceMeshes: []*NameAndNamespace{ { Name: util.ServiceMeshIstioSidecar, Namespace: "istio-system", }, }, }, - Exporter: &exporterConfig{ - Grpc: &server{ + Exporter: &ExporterConfig{ + Grpc: &Server{ Port: 11111, }, }, @@ -197,23 +197,23 @@ func TestConfig_validate(t *testing.T) { { name: "with empty service mesh name receiver should return error", fields: fields{ - Filters: &filters{ - Envoy: &envoyFilterConfig{ + Filters: &Filters{ + Envoy: &EnvoyFilterConfig{ Uri: "5gsec/http-filter:v0.1", }, - Server: &server{ + Server: &Server{ Port: SentryFlowDefaultFilterServerPort, }, }, - Receivers: &receivers{ - ServiceMeshes: []*nameAndNamespace{ + Receivers: &Receivers{ + ServiceMeshes: []*NameAndNamespace{ { Namespace: "istio-system", }, }, }, - Exporter: &exporterConfig{ - Grpc: &server{ + Exporter: &ExporterConfig{ + Grpc: &Server{ Port: 11111, }, }, @@ -224,23 +224,23 @@ func TestConfig_validate(t *testing.T) { { name: "with empty service mesh namespace receiver should return error", fields: fields{ - Filters: &filters{ - Envoy: &envoyFilterConfig{ + Filters: &Filters{ + Envoy: &EnvoyFilterConfig{ Uri: "5gsec/http-filter:v0.1", }, - Server: &server{ + Server: &Server{ Port: SentryFlowDefaultFilterServerPort, }, }, - Receivers: &receivers{ - ServiceMeshes: []*nameAndNamespace{ + Receivers: &Receivers{ + ServiceMeshes: []*NameAndNamespace{ { Name: "istio-sidecar", }, }, }, - Exporter: &exporterConfig{ - Grpc: &server{ + Exporter: &ExporterConfig{ + Grpc: &Server{ Port: 11111, }, }, @@ -251,24 +251,24 @@ func TestConfig_validate(t *testing.T) { { name: "with valid config should not return error", fields: fields{ - Filters: &filters{ - Envoy: &envoyFilterConfig{ + Filters: &Filters{ + Envoy: &EnvoyFilterConfig{ Uri: "5gsec/http-filter:v0.1", }, - Server: &server{ + Server: &Server{ Port: SentryFlowDefaultFilterServerPort, }, }, - Receivers: &receivers{ - ServiceMeshes: []*nameAndNamespace{ + Receivers: &Receivers{ + ServiceMeshes: []*NameAndNamespace{ { Name: "istio-sidecar", Namespace: "istio-system", }, }, }, - Exporter: &exporterConfig{ - Grpc: &server{ + Exporter: &ExporterConfig{ + Grpc: &Server{ Port: 11111, }, }, @@ -317,24 +317,24 @@ func TestNew(t *testing.T) { logger: logger, }, want: &Config{ - Filters: &filters{ - Envoy: &envoyFilterConfig{ + Filters: &Filters{ + Envoy: &EnvoyFilterConfig{ Uri: "anuragrajawat/httpfilter:v0.1", }, - Server: &server{ + Server: &Server{ Port: 8081, }, }, - Receivers: &receivers{ - ServiceMeshes: []*nameAndNamespace{ + Receivers: &Receivers{ + ServiceMeshes: []*NameAndNamespace{ { Name: "istio-sidecar", Namespace: "istio-system", }, }, }, - Exporter: &exporterConfig{ - Grpc: &server{ + Exporter: &ExporterConfig{ + Grpc: &Server{ Port: 8080, }, }, @@ -357,24 +357,24 @@ func TestNew(t *testing.T) { logger: logger, }, want: &Config{ - Filters: &filters{ - Envoy: &envoyFilterConfig{ + Filters: &Filters{ + Envoy: &EnvoyFilterConfig{ Uri: "anuragrajawat/httpfilter:v0.1", }, - Server: &server{ - Port: 8081, + Server: &Server{ + Port: 9999, }, }, - Receivers: &receivers{ - ServiceMeshes: []*nameAndNamespace{ + Receivers: &Receivers{ + ServiceMeshes: []*NameAndNamespace{ { Name: "istio-sidecar", Namespace: "istio-system", }, }, }, - Exporter: &exporterConfig{ - Grpc: &server{ + Exporter: &ExporterConfig{ + Grpc: &Server{ Port: 8080, }, }, @@ -388,24 +388,24 @@ func TestNew(t *testing.T) { logger: logger, }, want: &Config{ - Filters: &filters{ - Envoy: &envoyFilterConfig{ + Filters: &Filters{ + Envoy: &EnvoyFilterConfig{ Uri: "anuragrajawat/httpfilter:v0.1", }, - Server: &server{ - Port: 8081, + Server: &Server{ + Port: 9999, }, }, - Receivers: &receivers{ - ServiceMeshes: []*nameAndNamespace{ + Receivers: &Receivers{ + ServiceMeshes: []*NameAndNamespace{ { Name: "istio-sidecar", Namespace: "istio-system", }, }, }, - Exporter: &exporterConfig{ - Grpc: &server{ + Exporter: &ExporterConfig{ + Grpc: &Server{ Port: 8080, }, }, diff --git a/sentryflow/pkg/core/sentryflow.go b/sentryflow/pkg/core/sentryflow.go index 931ccd7..93055c5 100644 --- a/sentryflow/pkg/core/sentryflow.go +++ b/sentryflow/pkg/core/sentryflow.go @@ -65,8 +65,9 @@ func (m *Manager) run(cfg *config.Config, kubeConfig string) { m.Wg = &sync.WaitGroup{} m.ApiEvents = make(chan *protobuf.APIEvent, 10240) + runtimeSchemes := registerAndGetScheme() if m.areK8sReceivers(cfg) { - k8sClient, err := k8s.NewClient(registerAndGetScheme(), kubeConfig) + k8sClient, err := k8s.NewClient(runtimeSchemes, kubeConfig) if err != nil { m.Logger.Errorf("failed to create k8s client: %v", err) return @@ -115,7 +116,7 @@ func (m *Manager) run(cfg *config.Config, kubeConfig string) { case updatedConfig := <-m.configChan: m.receiversCancelFunc() if m.areK8sReceivers(updatedConfig) { - k8sClient, err := k8s.NewClient(registerAndGetScheme(), kubeConfig) + k8sClient, err := k8s.NewClient(runtimeSchemes, kubeConfig) if err != nil { m.Logger.Errorf("failed to create k8s client: %v", err) return @@ -149,7 +150,7 @@ func (m *Manager) watchConfig(configFilePath string, logger *zap.SugaredLogger) return } m.configChan <- cfg - m.Logger.Info("config file changed, reloading config...") + m.Logger.Warn("config file changed, reloading config...") }) } diff --git a/sentryflow/pkg/core/server.go b/sentryflow/pkg/core/server.go index 469fd05..c81ebc4 100644 --- a/sentryflow/pkg/core/server.go +++ b/sentryflow/pkg/core/server.go @@ -80,6 +80,7 @@ func (m *Manager) eventsHandler(writer http.ResponseWriter, request *http.Reques m.ApiEvents <- apiEvent writer.WriteHeader(http.StatusAccepted) + m.Logger.Debug(apiEvent) } func (m *Manager) healthzHandler(writer http.ResponseWriter, request *http.Request) { diff --git a/sentryflow/pkg/k8s/client.go b/sentryflow/pkg/k8s/client.go index 9d74f60..e84cca6 100644 --- a/sentryflow/pkg/k8s/client.go +++ b/sentryflow/pkg/k8s/client.go @@ -9,15 +9,18 @@ import ( "os" "path/filepath" + "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" ) // NewClient returns a new Client using the provided scheme to map go structs to // GroupVersionKinds. func NewClient(scheme *runtime.Scheme, kubeConfig string) (client.Client, error) { + log.SetLogger(logr.Logger{}) config, err := getConfig(kubeConfig) if err != nil { return nil, fmt.Errorf("failed to get config: %v", err) diff --git a/sentryflow/pkg/receiver/other/nginx/nginxinc/autoconfigure.go b/sentryflow/pkg/receiver/other/nginx/nginxinc/autoconfigure.go new file mode 100644 index 0000000..71657e7 --- /dev/null +++ b/sentryflow/pkg/receiver/other/nginx/nginxinc/autoconfigure.go @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Authors of SentryFlow + +package nginxinc + +import ( + "bytes" + "context" + "fmt" + "strings" + "text/template" + + "go.uber.org/zap" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/5GSEC/SentryFlow/pkg/config" + "github.com/5GSEC/SentryFlow/pkg/util" +) + +const volumeName = "sentryflow-nginx-inc" + +func deployResources(ctx context.Context, k8sClient client.Client, cfg *config.Config) error { + ingressDeployNs := getIngressControllerDeploymentNamespace(cfg) + if err := patchIngressDeploy(ctx, k8sClient, cfg.Filters.NginxIngress.DeploymentName, ingressDeployNs); err != nil { + return err + } + if err := patchIngressConfigMap(ctx, k8sClient, cfg, ingressDeployNs); err != nil { + return err + } + return nil +} + +func patchIngressConfigMap(ctx context.Context, k8sClient client.Client, cfg *config.Config, namespace string) error { + nginxConfigMap := &corev1.ConfigMap{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: cfg.Filters.NginxIngress.ConfigMapName, Namespace: namespace}, nginxConfigMap); err != nil { + return err + } + + const data = ` +http-snippets: +js_path "/etc/nginx/njs/"; +subrequest_output_buffer_size 8k; +js_shared_dict_zone zone=apievents:1M timeout=300s evict; +js_import main from sentryflow.js; + +location-snippets: +js_body_filter main.requestHandler buffer_type=buffer; +mirror /mirror_request; +mirror_request_body on; + +server-snippets: + +location /mirror_request { + internal; + js_content main.dispatchHttpCall; +} + +location /sentryflow { + internal; + # Update SentryFlow URL with path to ingest access logs if required. + proxy_pass http://{{ .SentryFlowSvcName }}.{{ .SentryFlowSvcNamespace }}:{{ .SentryFlowFilterServerPort }}/api/v1/events; + proxy_method POST; + proxy_set_header accept "application/json"; + proxy_set_header Content-Type "application/json"; +} +` + sentryflowSvcName, sentryflowSvcNamespace, err := sentryFlowSvcNameAndNs(ctx, k8sClient) + if err != nil { + return err + } + + values := struct { + SentryFlowSvcName string + SentryFlowSvcNamespace string + SentryFlowFilterServerPort uint16 + }{ + SentryFlowSvcName: sentryflowSvcName, + SentryFlowSvcNamespace: sentryflowSvcNamespace, + SentryFlowFilterServerPort: cfg.Filters.Server.Port, + } + + tmpl, err := template.New("nginx.tmpl").Parse(data) + if err != nil { + return err + } + + cmData := &bytes.Buffer{} + if err := tmpl.Execute(cmData, values); err != nil { + return err + } + cmDataStr := cmData.String() + + parts := strings.SplitN(cmDataStr, "http-snippets:", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid template format: missing http-snippets section") + } + httpSnippetsPart := parts[1] + + parts = strings.SplitN(httpSnippetsPart, "location-snippets:", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid template format: missing location-snippets section") + } + httpSnippets := strings.TrimSpace(parts[0]) + locationSnippetsPart := parts[1] + + parts = strings.SplitN(locationSnippetsPart, "server-snippets:", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid template format: missing server-snippets section") + } + + locationSnippets := strings.TrimSpace(parts[0]) + serverSnippets := strings.TrimSpace(parts[1]) + + if nginxConfigMap.Data == nil { + nginxConfigMap.Data = map[string]string{} + } + nginxConfigMap.Data["http-snippets"] = httpSnippets + nginxConfigMap.Data["location-snippets"] = locationSnippets + nginxConfigMap.Data["server-snippets"] = serverSnippets + + return k8sClient.Update(ctx, nginxConfigMap) +} + +func sentryFlowSvcNameAndNs(ctx context.Context, k8sClient client.Client) (string, string, error) { + svcs := &corev1.ServiceList{} + if err := k8sClient.List(ctx, svcs, &client.ListOptions{ + LabelSelector: labels.SelectorFromSet(map[string]string{ + "app.kubernetes.io/name": "sentryflow", + }), + }); err != nil { + return "", "", err + } + if len(svcs.Items) == 0 { + return "", "", fmt.Errorf("sentryFlow svc was not found") + } + return svcs.Items[0].Name, svcs.Items[0].Namespace, nil +} + +func patchIngressDeploy(ctx context.Context, k8sClient client.Client, deploymentName, namespace string) error { + nginxDeploy := &appsv1.Deployment{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, nginxDeploy); err != nil { + return err + } + + if !containsVolumeAndVolumeMount(nginxDeploy.Spec.Template.Spec) { + nginxDeploy.Spec.Template.Spec.Volumes = append(nginxDeploy.Spec.Template.Spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: volumeName, + }, + }, + }, + }) + + nginxDeploy.Spec.Template.Spec.Containers[0].VolumeMounts = append(nginxDeploy.Spec.Template.Spec.Containers[0].VolumeMounts, + corev1.VolumeMount{ + Name: volumeName, + MountPath: "/etc/nginx/njs/sentryflow.js", + SubPath: "sentryflow.js", + }, + ) + + if err := k8sClient.Update(ctx, nginxDeploy); err != nil { + return err + } + } + + return nil +} + +func containsVolumeAndVolumeMount(spec corev1.PodSpec) bool { + volumeFound, volumeMountFound := false, false + + for _, volume := range spec.Volumes { + if volume.Name == volumeName { + volumeFound = true + break + } + } + + for _, container := range spec.Containers { + for _, volumeMount := range container.VolumeMounts { + if volumeMount.MountPath == "/etc/nginx/njs/sentryflow.js" && volumeMount.SubPath == "sentryflow.js" { + volumeMountFound = true + break + } + } + } + + return volumeFound && volumeMountFound +} + +func autoConfigure(cfg *config.Config) bool { + for _, other := range cfg.Receivers.Others { + if other.Name == util.NginxIncorporationIngressController && other.AutoConfigure { + return true + } + } + return false +} + +func doCleanup(logger *zap.SugaredLogger, cfg *config.Config, k8sClient client.Client) { + logger.Info("shutting down nginx-incorporation ingress controller receiver") + ctx := context.Background() + + if autoConfigure(cfg) { + logger.Warn("nginx inc ingress deployment will restart") + ingressDeployNs := getIngressControllerDeploymentNamespace(cfg) + + var err error + if err = removePatchFromIngressConfigMap(ctx, k8sClient, cfg, ingressDeployNs); err != nil { + logger.Error("failed to remove patch from ingress-controller deployment", err) + // Do not return, always remove patches, even if errors occur. + } + if err = removePatchFromIngressDeploy(ctx, k8sClient, cfg.Filters.NginxIngress.DeploymentName, ingressDeployNs); err != nil { + logger.Error("failed to remove patch from ingress-controller deployment", err) + // Do not return, always remove patches, even if errors occur. + } + + if err == nil { + logger.Info("successfully removed patches from ingress-controller deployment and configmap") + } + } + + logger.Info("stopped nginx-incorporation ingress controller receiver") +} + +func removePatchFromIngressDeploy(ctx context.Context, k8sClient client.Client, deploymentName, namespace string) error { + nginxDeploy := &appsv1.Deployment{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: namespace}, nginxDeploy); err != nil { + return err + } + if !containsVolumeAndVolumeMount(nginxDeploy.Spec.Template.Spec) { + return nil + } + + var newVolumes []corev1.Volume + for _, volume := range nginxDeploy.Spec.Template.Spec.Volumes { + if volume.Name != volumeName { + newVolumes = append(newVolumes, volume) + } + } + nginxDeploy.Spec.Template.Spec.Volumes = newVolumes + + var newVolumeMounts []corev1.VolumeMount + for _, volumeMount := range nginxDeploy.Spec.Template.Spec.Containers[0].VolumeMounts { + if volumeMount.Name != volumeName { + newVolumeMounts = append(newVolumeMounts, volumeMount) + } + } + nginxDeploy.Spec.Template.Spec.Containers[0].VolumeMounts = newVolumeMounts + + return k8sClient.Update(ctx, nginxDeploy) +} + +func removePatchFromIngressConfigMap(ctx context.Context, k8sClient client.Client, cfg *config.Config, namespace string) error { + nginxConfigMap := &corev1.ConfigMap{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: cfg.Filters.NginxIngress.ConfigMapName, Namespace: namespace}, nginxConfigMap); err != nil { + return err + } + + var needToUpdate bool + if _, exists := nginxConfigMap.Data["http-snippets"]; exists { + delete(nginxConfigMap.Data, "http-snippets") + needToUpdate = true + } + if _, exists := nginxConfigMap.Data["location-snippets"]; exists { + delete(nginxConfigMap.Data, "location-snippets") + needToUpdate = true + } + if _, exists := nginxConfigMap.Data["server-snippets"]; exists { + delete(nginxConfigMap.Data, "server-snippets") + needToUpdate = true + } + + if needToUpdate { + return k8sClient.Update(ctx, nginxConfigMap) + } + + return nil +} diff --git a/sentryflow/pkg/receiver/other/nginx/nginxinc/autoconfigure_test.go b/sentryflow/pkg/receiver/other/nginx/nginxinc/autoconfigure_test.go new file mode 100644 index 0000000..0bf7a83 --- /dev/null +++ b/sentryflow/pkg/receiver/other/nginx/nginxinc/autoconfigure_test.go @@ -0,0 +1,847 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Authors of SentryFlow + +package nginxinc + +import ( + "context" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/5GSEC/SentryFlow/pkg/config" +) + +func Test_autoConfigure(t *testing.T) { + type args struct { + cfg *config.Config + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "when `autoconfigure` is true for `nginx-inc-ingress-controller` then return true", + args: args{ + cfg: &config.Config{ + Filters: &config.Filters{ + NginxIngress: &config.NginxIngressConfig{ + DeploymentName: "nginx-inc-ingress-controller", + ConfigMapName: "nginx-ingress", + SentryFlowNjsConfigMapName: "sentryflow-nginx-inc", + }, + Server: &config.Server{Port: 9999}, + }, + Receivers: &config.Receivers{ + Others: []*config.NameAndNamespace{ + { + Name: "nginx-inc-ingress-controller", + Namespace: "nginx-ingress", + AutoConfigure: true, + }, + }, + }, + Exporter: &config.ExporterConfig{ + Grpc: &config.Server{Port: 8888}, + }, + }, + }, + want: true, + }, + { + name: "when `autoconfigure` is not set for `nginx-inc-ingress-controller` then return false", + args: args{ + cfg: &config.Config{ + Filters: &config.Filters{ + NginxIngress: &config.NginxIngressConfig{ + DeploymentName: "nginx-inc-ingress-controller", + ConfigMapName: "nginx-ingress", + SentryFlowNjsConfigMapName: "sentryflow-nginx-inc", + }, + Server: &config.Server{Port: 9999}, + }, + Receivers: &config.Receivers{ + Others: []*config.NameAndNamespace{ + { + Name: "nginx-inc-ingress-controller", + Namespace: "nginx-ingress", + }, + }, + }, + Exporter: &config.ExporterConfig{ + Grpc: &config.Server{Port: 8888}, + }, + }, + }, + want: false, + }, + { + name: "when `autoconfigure` is true for other receiver then return false", + args: args{ + cfg: &config.Config{ + Filters: &config.Filters{ + Server: &config.Server{Port: 9999}, + }, + Receivers: &config.Receivers{ + Others: []*config.NameAndNamespace{ + { + Name: "nginx-webserver", + AutoConfigure: true, + }, + }, + }, + Exporter: &config.ExporterConfig{ + Grpc: &config.Server{Port: 8888}, + }, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := autoConfigure(tt.args.cfg); got != tt.want { + t.Errorf("autoConfigure() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_containsVolumeAndVolumeMount(t *testing.T) { + type args struct { + spec v1.PodSpec + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "when both volume and volumeMount path and subPath are present then return true", + args: args{ + spec: v1.PodSpec{ + Containers: []v1.Container{ + { + VolumeMounts: []v1.VolumeMount{ + { + Name: volumeName, + MountPath: "/etc/nginx/njs/sentryflow.js", + SubPath: "sentryflow.js", + }, + }, + }, + }, + Volumes: []v1.Volume{ + { + Name: volumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: volumeName, + }, + }, + }, + }, + }, + }, + }, + want: true, + }, + { + name: "when only volume is present then return false", + args: args{ + spec: v1.PodSpec{ + Volumes: []v1.Volume{ + { + Name: volumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: volumeName, + }, + }, + }, + }, + }, + }, + }, + want: false, + }, + { + name: "when only volumeMount is present then return false", + args: args{ + spec: v1.PodSpec{ + Containers: []v1.Container{ + { + VolumeMounts: []v1.VolumeMount{ + { + Name: volumeName, + MountPath: "/etc/nginx/njs/sentryflow.js", + SubPath: "sentryflow.js", + }, + }, + }, + }, + }, + }, + want: false, + }, + { + name: "when both volume and volumeMount are not present then return false", + args: args{ + spec: v1.PodSpec{ + Containers: []v1.Container{}, + Volumes: []v1.Volume{}, + }, + }, + want: false, + }, + { + name: "when incorrect volumeName is present then return false", + args: args{ + spec: v1.PodSpec{ + Containers: []v1.Container{ + { + VolumeMounts: []v1.VolumeMount{ + { + Name: volumeName, + MountPath: "/etc/nginx/njs/sentryflow.js", + SubPath: "sentryflow.js", + }, + }, + }, + }, + Volumes: []v1.Volume{ + { + Name: "other-volume", + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: volumeName, + }, + }, + }, + }, + }, + }, + }, + want: false, + }, + { + name: "when incorrect volumeMount path is present then return false", + args: args{ + spec: v1.PodSpec{ + Containers: []v1.Container{ + { + VolumeMounts: []v1.VolumeMount{ + { + Name: volumeName, + MountPath: "/etc/nginx/njs/other.js", + SubPath: "sentryflow.js", + }, + }, + }, + }, + Volumes: []v1.Volume{ + { + Name: volumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: volumeName, + }, + }, + }, + }, + }, + }, + }, + want: false, + }, + { + name: "when no volumeMount subPath is present then return false", + args: args{ + spec: v1.PodSpec{ + Containers: []v1.Container{ + { + VolumeMounts: []v1.VolumeMount{ + { + Name: volumeName, + MountPath: "/etc/nginx/njs/other.js", + }, + }, + }, + }, + Volumes: []v1.Volume{ + { + Name: volumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: volumeName, + }, + }, + }, + }, + }, + }, + }, + want: false, + }, + { + name: "when incorrect volumeMount subPath is present then return false", + args: args{ + spec: v1.PodSpec{ + Containers: []v1.Container{ + { + VolumeMounts: []v1.VolumeMount{ + { + Name: volumeName, + MountPath: "/etc/nginx/njs/other.js", + SubPath: "other.js", + }, + }, + }, + }, + Volumes: []v1.Volume{ + { + Name: volumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: volumeName, + }, + }, + }, + }, + }, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := containsVolumeAndVolumeMount(tt.args.spec); got != tt.want { + t.Errorf("containsVolumeAndVolumeMount() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_deployResources(t *testing.T) { + ctx := context.Background() + k8sClient := getFakeClient() + sentryFlowSvc := sentryFlowService() + cfg := configWithAutoConfigureTrue() + + if err := k8sClient.Create(ctx, sentryFlowSvc); err != nil { + t.Errorf("failed to create sentryflow service: %v", err) + } + + t.Run("when ingress deployment and configmap don't contain patches then patch them and return nil", func(t *testing.T) { + ingDeploy := ingressDeployment() + ingCm := ingressConfigMap() + wantErr := false + + if err := k8sClient.Create(ctx, ingDeploy); err != nil { + t.Errorf("failed to create ingress deployment: %v", err) + } + if err := k8sClient.Create(ctx, ingCm); err != nil { + t.Errorf("failed to create ingress configmap: %v", err) + } + defer cleanup(t, k8sClient, ingDeploy, ingCm, nil) + + if err := deployResources(ctx, k8sClient, cfg); (err != nil) != wantErr { + t.Errorf("deployResources() error = %v, wantErr %v", err, wantErr) + } + }) + + t.Run("when ingress deployment and configmap contain patches then don't patch them and return nil", func(t *testing.T) { + ingDeploy := ingressDeployment() + ingDeploy.Spec.Template.Spec.Containers[0].VolumeMounts = append(ingDeploy.Spec.Template.Spec.Containers[0].VolumeMounts, v1.VolumeMount{ + Name: volumeName, + MountPath: "/etc/nginx/njs/sentryflow.js", + SubPath: "sentryflow.js", + }) + ingDeploy.Spec.Template.Spec.Volumes = append(ingDeploy.Spec.Template.Spec.Volumes, v1.Volume{ + Name: volumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: volumeName, + }, + }, + }, + }) + ingCm := ingressConfigMap() + patchConfigMap(ingCm) + + wantErr := false + + if err := k8sClient.Create(ctx, ingDeploy); err != nil { + t.Errorf("failed to create ingress deployment: %v", err) + } + if err := k8sClient.Create(ctx, ingCm); err != nil { + t.Errorf("failed to create ingress configmap: %v", err) + } + defer cleanup(t, k8sClient, ingDeploy, ingCm, nil) + + if err := deployResources(ctx, k8sClient, cfg); (err != nil) != wantErr { + t.Errorf("deployResources() error = %v, wantErr %v", err, wantErr) + } + }) + + t.Run("when ingress deployment doesn't exist then return doesn't exist error", func(t *testing.T) { + ingCm := ingressConfigMap() + wantErr := true + errMessage := `deployments.apps "nginx-ingress" not found` + + if err := k8sClient.Create(ctx, ingCm); err != nil { + t.Errorf("failed to create ingress configmap: %v", err) + } + defer cleanup(t, k8sClient, nil, ingCm, nil) + + err := deployResources(ctx, k8sClient, cfg) + if (err != nil) != wantErr { + t.Errorf("deployResources() error = %v, wantErr %v", err, wantErr) + } + if err.Error() != errMessage { + t.Errorf("deployResources() errorMessage = %v, wantErrMessage %v", err.Error(), errMessage) + } + }) + + t.Run("when ingress configMap doesn't exist then return doesn't exist error", func(t *testing.T) { + ingressDeploy := ingressDeployment() + wantErr := true + errMessage := `configmaps "nginx-ingress" not found` + + if err := k8sClient.Create(ctx, ingressDeploy); err != nil { + t.Errorf("failed to create ingress configmap: %v", err) + } + defer cleanup(t, k8sClient, ingressDeploy, nil, nil) + + err := deployResources(ctx, k8sClient, cfg) + if (err != nil) != wantErr { + t.Errorf("deployResources() error = %v, wantErr %v", err, wantErr) + } + if err.Error() != errMessage { + t.Errorf("deployResources() errorMessage = %v, wantErrMessage %v", err.Error(), errMessage) + } + }) +} + +func Test_removePatchFromIngressConfigMap(t *testing.T) { + ctx := context.Background() + k8sClient := getFakeClient() + + t.Run("when ingress configmap doesn't contain any sentryflow patches then return nil", func(t *testing.T) { + cm := ingressConfigMap() + cfg := configWithAutoConfigureTrue() + wantErr := false + + if err := k8sClient.Create(ctx, cm); err != nil { + t.Errorf("failed to create ingress configmap: %v", err) + } + defer cleanup(t, k8sClient, nil, cm, nil) + + if err := removePatchFromIngressConfigMap(ctx, k8sClient, cfg, cm.Namespace); (err != nil) != wantErr { + t.Errorf("removePatchFromIngressConfigMap() error = %v, wantErr %v", err, wantErr) + } + }) + + t.Run("when ingress configmap contains sentryflow patches then remove then and return nil", func(t *testing.T) { + cm := ingressConfigMap() + patchConfigMap(cm) + cfg := configWithAutoConfigureTrue() + wantErr := false + if err := k8sClient.Create(ctx, cm); err != nil { + t.Errorf("failed to create ingress configmap: %v", err) + } + defer cleanup(t, k8sClient, nil, cm, nil) + + if err := removePatchFromIngressConfigMap(ctx, k8sClient, cfg, cm.Namespace); (err != nil) != wantErr { + t.Errorf("removePatchFromIngressConfigMap() error = %v, wantErr %v", err, wantErr) + } + + var updatedCm corev1.ConfigMap + if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: cm.Namespace, Name: cm.Name}, &updatedCm); err != nil { + t.Errorf("failed to get updated ingress configmap: %v", err) + } + + if snippets, exists := updatedCm.Data["http-snippets"]; exists { + t.Errorf("removePatchFromIngressConfigMap() got http-snippets = %v,\nwant = %v", snippets, "") + } + if snippets, exists := updatedCm.Data["location-snippets"]; exists { + t.Errorf("removePatchFromIngressConfigMap() got location-snippets = %v,\nwant = %v", snippets, "") + } + if snippets, exists := updatedCm.Data["server-snippets"]; exists { + t.Errorf("removePatchFromIngressConfigMap() got server-snippets = %v,\nwant = %v", snippets, "") + } + }) + +} + +func Test_removePatchFromIngressDeploy(t *testing.T) { + ctx := context.Background() + k8sClient := getFakeClient() + + t.Run("when ingress deployment doesn't contain patches then return nil", func(t *testing.T) { + deploy := ingressDeployment() + if err := k8sClient.Create(ctx, deploy); err != nil { + t.Errorf("failed to create ingress deployment: %v", err) + } + defer cleanup(t, k8sClient, deploy, nil, nil) + + wantErr := false + + if err := removePatchFromIngressDeploy(ctx, k8sClient, deploy.Name, deploy.Namespace); (err != nil) != wantErr { + t.Errorf("removePatchFromIngressDeploy() error = %v, wantErr %v", err, wantErr) + } + }) + + t.Run("when ingress deployment contains patches then remove them and return nil", func(t *testing.T) { + ingDeploy := ingressDeployment() + ingDeploy.Spec.Template.Spec.Containers[0].VolumeMounts = append(ingDeploy.Spec.Template.Spec.Containers[0].VolumeMounts, v1.VolumeMount{ + Name: volumeName, + MountPath: "/etc/nginx/njs/sentryflow.js", + SubPath: "sentryflow.js", + }) + ingDeploy.Spec.Template.Spec.Volumes = append(ingDeploy.Spec.Template.Spec.Volumes, v1.Volume{ + Name: volumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: volumeName, + }, + }, + }, + }) + if err := k8sClient.Create(ctx, ingDeploy); err != nil { + t.Errorf("failed to create ingress deployment: %v", err) + } + defer cleanup(t, k8sClient, ingDeploy, nil, nil) + wantErr := false + + if err := removePatchFromIngressDeploy(ctx, k8sClient, ingDeploy.Name, ingDeploy.Namespace); (err != nil) != wantErr { + t.Errorf("removePatchFromIngressDeploy() error = %v, wantErr %v", err, wantErr) + } + + var updatedDeploy appsv1.Deployment + if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: ingDeploy.Namespace, Name: ingDeploy.Name}, &updatedDeploy); err != nil { + t.Errorf("failed to get updated deployment: %v", err) + } + + if len(updatedDeploy.Spec.Template.Spec.Containers[0].VolumeMounts) != 0 { + t.Errorf("removePatchFromIngressDeploy() volumeMounts = %v, want 0", len(updatedDeploy.Spec.Template.Spec.Containers[0].VolumeMounts)) + } + if len(updatedDeploy.Spec.Template.Spec.Volumes) != 0 { + t.Errorf("removePatchFromIngressDeploy() volumes = %v, want 0", len(updatedDeploy.Spec.Template.Spec.Volumes)) + } + }) + + t.Run("when ingress deployment contains multiple volumes and volumeMounts then remove patched ones and return nil", func(t *testing.T) { + ingDeploy := ingressDeployment() + ingDeploy.Spec.Template.Spec.Containers[0].VolumeMounts = append(ingDeploy.Spec.Template.Spec.Containers[0].VolumeMounts, + v1.VolumeMount{ + Name: volumeName, + MountPath: "/etc/nginx/njs/sentryflow.js", + SubPath: "sentryflow.js", + }, + v1.VolumeMount{ + Name: "other-volume", + MountPath: "/some/mount/path", + ReadOnly: true, + }, + ) + ingDeploy.Spec.Template.Spec.Volumes = append(ingDeploy.Spec.Template.Spec.Volumes, + v1.Volume{ + Name: volumeName, + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: volumeName, + }, + }, + }, + }, + v1.Volume{ + Name: "other-volume", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + ) + if err := k8sClient.Create(ctx, ingDeploy); err != nil { + t.Errorf("failed to create ingress deployment: %v", err) + } + defer cleanup(t, k8sClient, ingDeploy, nil, nil) + wantErr := false + + if err := removePatchFromIngressDeploy(ctx, k8sClient, ingDeploy.Name, ingDeploy.Namespace); (err != nil) != wantErr { + t.Errorf("removePatchFromIngressDeploy() error = %v, wantErr %v", err, wantErr) + } + + var updatedDeploy appsv1.Deployment + if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: ingDeploy.Namespace, Name: ingDeploy.Name}, &updatedDeploy); err != nil { + t.Errorf("failed to get updated deployment: %v", err) + } + + if len(updatedDeploy.Spec.Template.Spec.Containers[0].VolumeMounts) != 1 { + t.Errorf("removePatchFromIngressDeploy() volumeMounts = %v, want 1", len(updatedDeploy.Spec.Template.Spec.Containers[0].VolumeMounts)) + } + if len(updatedDeploy.Spec.Template.Spec.Volumes) != 1 { + t.Errorf("removePatchFromIngressDeploy() volumes = %v, want 1", len(updatedDeploy.Spec.Template.Spec.Volumes)) + } + }) +} + +func Test_sentryFlowSvcNameAndNs(t *testing.T) { + ctx := context.Background() + k8sClient := getFakeClient() + + t.Run("when sentrflow doesn't exist then return empty name and namespace and doesn't exist error", func(t *testing.T) { + wantErr := true + svcName, namespace := "", "" + errMessage := "sentryFlow svc was not found" + + gotSvcName, gotNsName, err := sentryFlowSvcNameAndNs(ctx, k8sClient) + if (err != nil) != wantErr { + t.Errorf("sentryFlowSvcNameAndNs() error = %v, wantErr %v", err, wantErr) + } + if err.Error() != errMessage { + t.Errorf("sentryFlowSvcNameAndNs() errorMessage = %v, wantErrMessage %v", err.Error(), errMessage) + } + if gotSvcName != svcName { + t.Errorf("sentryFlowSvcNameAndNs() got = %v, want %v", gotSvcName, svcName) + } + if gotNsName != namespace { + t.Errorf("sentryFlowSvcNameAndNs() got1 = %v, want %v", gotNsName, namespace) + } + }) + + t.Run("when sentrflow exist then return its name and namespace and no error", func(t *testing.T) { + wantErr := false + sentryFlowSvc := sentryFlowService() + + if err := k8sClient.Create(ctx, sentryFlowSvc); err != nil { + t.Errorf("failed to create sentry flow service: %v", err) + } + defer cleanup(t, k8sClient, nil, nil, sentryFlowSvc) + + gotSvcName, gotNsName, err := sentryFlowSvcNameAndNs(ctx, k8sClient) + if (err != nil) != wantErr { + t.Errorf("sentryFlowSvcNameAndNs() error = %v, wantErr %v", err, wantErr) + } + if gotSvcName != sentryFlowSvc.Name { + t.Errorf("sentryFlowSvcNameAndNs() got = %v, want %v", gotSvcName, sentryFlowSvc.Name) + } + if gotNsName != sentryFlowSvc.Namespace { + t.Errorf("sentryFlowSvcNameAndNs() got1 = %v, want %v", gotNsName, sentryFlowSvc.Namespace) + } + }) + + t.Run("when sentrflow exist with different labels then return empty name and namespace and doesn't exist error ", func(t *testing.T) { + wantErr := true + sentryFlowSvc := sentryFlowService() + sentryFlowSvc.Labels = map[string]string{ + "app": "sentryflow", + } + errMessage := "sentryFlow svc was not found" + + if err := k8sClient.Create(ctx, sentryFlowSvc); err != nil { + t.Errorf("failed to create sentry flow service: %v", err) + } + defer cleanup(t, k8sClient, nil, nil, sentryFlowSvc) + + gotSvcName, gotNsName, err := sentryFlowSvcNameAndNs(ctx, k8sClient) + if (err != nil) != wantErr { + t.Errorf("sentryFlowSvcNameAndNs() error = %v, wantErr %v", err, wantErr) + } + if err.Error() != errMessage { + t.Errorf("sentryFlowSvcNameAndNs() errorMessage = %v, wantErrMessage %v", err, errMessage) + } + if gotSvcName != "" { + t.Errorf("sentryFlowSvcNameAndNs() got = %v, want %v", gotSvcName, "") + } + if gotNsName != "" { + t.Errorf("sentryFlowSvcNameAndNs() got1 = %v, want %v", gotNsName, "") + } + }) +} + +func getFakeClient() client.WithWatch { + scheme := runtime.NewScheme() + utilruntime.Must(corev1.AddToScheme(scheme)) + utilruntime.Must(appsv1.AddToScheme(scheme)) + return fake. + NewClientBuilder(). + WithScheme(scheme). + Build() +} + +func cleanup(t *testing.T, k8sClient client.WithWatch, deployment *appsv1.Deployment, configMap *v1.ConfigMap, svc *v1.Service) { + t.Helper() + + ctx := context.Background() + if deployment != nil { + if err := k8sClient.Delete(ctx, deployment); err != nil { + t.Errorf("failed to delete deployment: %v", err) + } + } + if configMap != nil { + if err := k8sClient.Delete(ctx, configMap); err != nil { + t.Errorf("failed to delete configmap: %v", err) + } + } + if svc != nil { + if err := k8sClient.Delete(ctx, svc); err != nil { + t.Errorf("failed to delete service: %v", err) + } + } +} + +func sentryFlowService() *corev1.Service { + return &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "sentryflow", + Namespace: "sentryflow", + Labels: map[string]string{ + "app.kubernetes.io/name": "sentryflow", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "http", + Port: 9999, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "grpc", + Port: 8888, + Protocol: corev1.ProtocolTCP, + }, + }, + Selector: map[string]string{ + "app.kubernetes.io/name": "sentryflow", + }, + Type: corev1.ServiceTypeClusterIP, + }, + } +} + +func ingressConfigMap() *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx-ingress", + Namespace: "nginx-ingress", + }, + } +} + +func configWithAutoConfigureTrue() *config.Config { + return &config.Config{ + Filters: &config.Filters{ + NginxIngress: &config.NginxIngressConfig{ + DeploymentName: "nginx-ingress", + ConfigMapName: "nginx-ingress", + SentryFlowNjsConfigMapName: volumeName, + }, + Server: &config.Server{ + Port: 9999, + }, + }, + Receivers: &config.Receivers{ + Others: []*config.NameAndNamespace{ + { + Name: "nginx-inc-ingress-controller", + Namespace: "nginx-ingress", + AutoConfigure: true, + }, + }, + }, + Exporter: &config.ExporterConfig{ + Grpc: &config.Server{Port: 8888}, + }, + } +} + +func ingressDeployment() *appsv1.Deployment { + return &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx-ingress", + Namespace: "nginx-ingress", + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "nginx-ingress", + }, + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": "nginx-ingress", + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "nginx-ingress", + Image: "nginx/nginx-ingress:4.0.0", + }, + }, + }, + }, + }, + } +} + +func patchConfigMap(cm *v1.ConfigMap) { + cm.Data = map[string]string{ + "http-snippets": `js_path "/etc/nginx/njs/"; +subrequest_output_buffer_size 8k; +js_shared_dict_zone zone=apievents:1M timeout=300s evict; +js_import main from sentryflow.js;`, + + "location-snippets": `js_body_filter main.requestHandler buffer_type=buffer; +mirror /mirror_request; +mirror_request_body on;`, + + "server-snippets": ` +location /mirror_request { + internal; + js_content main.dispatchHttpCall; +} + +location /sentryflow { + internal; + # Update SentryFlow URL with path to ingest access logs if required. + proxy_pass http://{{ .SentryFlowSvcName }}.{{ .SentryFlowSvcNamespace }}:{{ .SentryFlowFilterServerPort }}/api/v1/events; + proxy_method POST; + proxy_set_header accept "application/json"; + proxy_set_header Content-Type "application/json"; +}`, + } +} diff --git a/sentryflow/pkg/receiver/other/nginx/nginxinc/nginx.go b/sentryflow/pkg/receiver/other/nginx/nginxinc/nginx.go index 89eb5f4..7bd7115 100644 --- a/sentryflow/pkg/receiver/other/nginx/nginxinc/nginx.go +++ b/sentryflow/pkg/receiver/other/nginx/nginxinc/nginx.go @@ -19,18 +19,26 @@ import ( func Start(ctx context.Context, cfg *config.Config, k8sClient client.Client) { logger := util.LoggerFromCtx(ctx) + defer doCleanup(logger, cfg, k8sClient) logger.Info("Starting nginx-incorporation ingress controller receiver") + + if autoConfigure(cfg) { + logger.Warn("nginx inc ingress deployment will restart") + if err := deployResources(ctx, k8sClient, cfg); err != nil { + logger.Errorf("failed to configure resources for nginx inc ingress receiver: %v", err) + return + } + logger.Info("successfully configured nginx-incorporation ingress controller") + } + if err := validateResources(ctx, cfg, k8sClient); err != nil { - // Todo(@anurag-rajawat): Log docs link for reference on how to configure this receiver properly. logger.Errorf("%v. Stopped nginx-incorporation ingress controller receiver", err) return } logger.Info("Started nginx-incorporation ingress controller receiver") <-ctx.Done() - logger.Info("Shutting down nginx-incorporation ingress controller receiver") - logger.Info("Stopped nginx-incorporation ingress controller receiver") } func validateResources(ctx context.Context, cfg *config.Config, k8sClient client.Client) error { @@ -105,32 +113,32 @@ func validateIngressDeployAndConfigMap(ctx context.Context, cfg *config.Config, if !exists { return fmt.Errorf("sentryflow http-snippets not found in nginx-incorporation ingress configmap") } - expectedHttpSnippets := `js_path "/etc/nginx/njs/"; + expectedHttpSnippets := strings.TrimSpace(`js_path "/etc/nginx/njs/"; subrequest_output_buffer_size 8k; js_shared_dict_zone zone=apievents:1M timeout=300s evict; js_import main from sentryflow.js; -` +`) if !strings.Contains(httpSnippets, expectedHttpSnippets) { - return fmt.Errorf("sentryflow http-snippets were not properly configured in nginx-incorporation ingress configmap") + return fmt.Errorf("sentryflow http-snippets were not properly configured in nginx-incorporation ingress configmap.\nGOT\n%v, \nEXPECTED\n%v", httpSnippets, expectedHttpSnippets) } locationSnippets, exists := ingressCm.Data["location-snippets"] if !exists { return fmt.Errorf("sentryflow location-snippets not found in nginx-incorporation ingress configmap") } - expectedLocationSnippets := `js_body_filter main.requestHandler buffer_type=buffer; + expectedLocationSnippets := strings.TrimSpace(`js_body_filter main.requestHandler buffer_type=buffer; mirror /mirror_request; mirror_request_body on; -` +`) if !strings.Contains(locationSnippets, expectedLocationSnippets) { - return fmt.Errorf("sentryflow location-snippets were not properly configured in nginx-incorporation ingress configmap") + return fmt.Errorf("sentryflow location-snippets were not properly configured in nginx-incorporation ingress configmap.\nGOT\n%v, \nEXPECTED\n%v", locationSnippets, expectedLocationSnippets) } serverSnippets, exists := ingressCm.Data["server-snippets"] if !exists { return fmt.Errorf("sentryflow server-snippets not found in nginx-incorporation ingress configmap") } - expectedServerSnippets := `location /mirror_request { + expectedServerSnippets := strings.TrimSpace(`location /mirror_request { internal; js_content main.dispatchHttpCall; } @@ -140,11 +148,11 @@ location /sentryflow { proxy_set_header accept "application/json"; proxy_set_header Content-Type "application/json"; } -` +`) // The server snippet might have different SentryFlow URL in `proxy_pass` // directive. To avoid potential conflicts, check without that directive. if !strings.ContainsAny(serverSnippets, expectedServerSnippets) { - return fmt.Errorf("sentryflow server-snippets were not properly configured in nginx-incorporation ingress configmap") + return fmt.Errorf("sentryflow server-snippets were not properly configured in nginx-incorporation ingress configmap.\nGOT\n%v, \nEXPECTED\n%v", serverSnippets, expectedServerSnippets) } return nil From cbb833c45f411b0e31f28bf27c52f513675416f2 Mon Sep 17 00:00:00 2001 From: Anurag Singh Rajawat Date: Tue, 18 Feb 2025 13:13:06 +0530 Subject: [PATCH 2/2] docs: Update docs Signed-off-by: Anurag Singh Rajawat --- docs/getting_started.md | 33 ++---- .../ingress-controller/nginx-inc/nginx_inc.md | 108 +++++++++++++----- .../receivers/other/web-server/nginx/nginx.md | 43 ++++--- docs/receivers/service-mesh/istio/istio.md | 54 ++++----- 4 files changed, 139 insertions(+), 99 deletions(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index a92706f..e25d228 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -13,40 +13,21 @@ observability. It includes detailed commands for each step along with their expl ## 2. Deploying SentryFlow -Configure SentryFlow receiver by following [this](receivers.md). Then deploy updated SentryFlow manifest by following `kubectl` command: +- Add SentryFlow repo ```shell -kubectl apply -f sentryflow.yaml +helm repo add 5gsec https://5gsec.github.io/charts +helm repo update 5gsec ``` -This will create a namespace named `sentryflow` and will deploy the necessary Kubernetes resources. - -Then, check if SentryFlow is up and running by: - -```shell -$ kubectl -n sentryflow get pods -NAME READY STATUS RESTARTS AGE -sentryflow-cff887bbd-rljm7 1/1 Running 0 73s -``` - -## 3. Deploying SentryFlow Clients - -SentryFlow has now been deployed in the cluster. In addition, SentryFlow exports API access logs through `gRPC`. - -For testing purposes, a client has been developed. - -- `log-client`: Simply logs everything on `STDOUT` coming from SentryFlow. - -It can be deployed into the cluster under namespace `sentryflow` by following the command: +- Update `values.yaml` file as follows by following [this](receivers.md). ```shell -kubectl apply -f https://raw.githubusercontent.com/5GSEC/SentryFlow/refs/heads/main/deployments/sentryflow-client.yaml +helm show values 5gsec/sentryflow > values.yaml ``` -Then, check if it is up and running by: +- Deploy SentryFlow ```shell -kubectl get pods -n sentryflow +helm install --values values.yaml sentryflow 5gsec/sentryflow -n sentryflow --create-namespace ``` - -If you observe `log-client`, is running, the setup has been completed successfully. diff --git a/docs/receivers/other/ingress-controller/nginx-inc/nginx_inc.md b/docs/receivers/other/ingress-controller/nginx-inc/nginx_inc.md index 8179e4f..847ed9c 100644 --- a/docs/receivers/other/ingress-controller/nginx-inc/nginx_inc.md +++ b/docs/receivers/other/ingress-controller/nginx-inc/nginx_inc.md @@ -18,9 +18,53 @@ SentryFlow make use of following to provide visibility into API calls: ## How to -To Observe API calls of your workloads served by Nginx inc. ingress controller in Kubernetes environment, follow -the below -steps: +To Observe API calls of your workloads served by Nginx inc. ingress controller in Kubernetes environment, you've two +options: + +### Automatic configuration + +> **Note**: This option will restart your ingress controller deployment. + +- Add SentryFlow repo + +```shell +helm repo add 5gsec https://5gsec.github.io/charts +helm repo update 5gsec +``` + +- Update `values.yaml` file as follows. + +```shell +helm show values 5gsec/sentryflow > values.yaml +``` + +```yaml +filters: +server: + # Existing snippets + # Following is required for `nginx-inc-ingress-controller` receiver. + nginxIngress: + deploymentName: + configMapName: + sentryFlowNjsConfigMapName: + +receivers: + others: + - name: nginx-inc-ingress-controller # SentryFlow makes use of `name` to configure receivers. DON'T CHANGE IT. + namespace: # Kubernetes namespace in which you've deployed the ingress controller. + autoConfigure: true +# Existing snippets +``` + +- Deploy SentryFlow + +```shell +helm install --values values.yaml sentryflow 5gsec/sentryflow -n sentryflow --create-namespace +``` + +### Manual configuration + +To configure it manually follow the below steps: 1. Create the following configmap in the same namespace as ingress controller. @@ -167,38 +211,42 @@ data: } ``` -4. Download SentryFlow manifest file +4. Deploy SentryFlow - ```shell - curl -sO https://raw.githubusercontent.com/5GSEC/SentryFlow/refs/heads/main/deployments/sentryflow.yaml - ``` +- Add SentryFlow repo -5. Update the `.receivers` configuration in `sentryflow` [configmap](../../../../../deployments/sentryflow.yaml) as - follows: +```shell +helm repo add 5gsec https://5gsec.github.io/charts +helm repo update 5gsec +``` - ```yaml - filters: - server: - port: 8081 - # Following is required for `nginx-inc-ingress-controller` receiver. - nginxIngress: - deploymentName: - configMapName: - sentryFlowNjsConfigMapName: +- Update `values.yaml` file as follows. - receivers: - others: - - name: nginx-inc-ingress-controller # SentryFlow makes use of `name` to configure receivers. DON'T CHANGE IT. - namespace: # Kubernetes namespace in which you've deployed the ingress controller. - ... - ``` +```shell +helm show values 5gsec/sentryflow > values.yaml +``` -6. Deploy SentryFlow +```yaml +filters: +server: + # Existing snippets + # Following is required for `nginx-inc-ingress-controller` receiver. + nginxIngress: + deploymentName: + configMapName: + sentryFlowNjsConfigMapName: + +receivers: + others: + - name: nginx-inc-ingress-controller # SentryFlow makes use of `name` to configure receivers. DON'T CHANGE IT. + namespace: # Kubernetes namespace in which you've deployed the ingress controller. +# Existing snippets +``` - ```shell - kubectl apply -f sentryflow.yaml - ``` +- Deploy SentryFlow -7. Trigger API calls to generate traffic. +```shell +helm install --values values.yaml sentryflow 5gsec/sentryflow -n sentryflow --create-namespace +``` -8. Use SentryFlow [log client](../../../../client) to see the API Events. +5. Trigger API calls to generate traffic. diff --git a/docs/receivers/other/web-server/nginx/nginx.md b/docs/receivers/other/web-server/nginx/nginx.md index d426663..e002df3 100644 --- a/docs/receivers/other/web-server/nginx/nginx.md +++ b/docs/receivers/other/web-server/nginx/nginx.md @@ -70,26 +70,35 @@ Here is the sample [nginx.conf](../../../../../filter/nginx/nginx.conf) file for $ sudo nginx -s reload ``` -4. Update the `.receivers` configuration in `sentryflow` [configmap](../../../../deployments/sentryflow.yaml) as - follows: +4. Deploy SentryFlow - ```yaml - filters: - server: - port: 8081 +- Add SentryFlow repo - receivers: - others: - - name: nginx-webserver # SentryFlow makes use of `name` to configure receivers. DON'T CHANGE IT. - ... - ``` +```shell +helm repo add 5gsec https://5gsec.github.io/charts +helm repo update 5gsec +``` + +- Update `values.yaml` file as follows. + +```shell +helm show values 5gsec/sentryflow > values.yaml +``` -5. Deploy SentryFlow +```yaml +filters: + server: +# Existing snippets +receivers: + others: + - name: nginx-webserver # SentryFlow makes use of `name` to configure receivers. DON'T CHANGE IT. + # Existing snippets +``` - ```shell - kubectl apply -f sentryflow.yaml - ``` +- Deploy SentryFlow -6. Trigger API calls to generate traffic. +```shell +helm install --values values.yaml sentryflow 5gsec/sentryflow -n sentryflow --create-namespace +``` -7. Use SentryFlow [log client](../../../../client) to see the API Events. \ No newline at end of file +5. Trigger API calls to generate traffic. diff --git a/docs/receivers/service-mesh/istio/istio.md b/docs/receivers/service-mesh/istio/istio.md index 4792d3c..626f6de 100644 --- a/docs/receivers/service-mesh/istio/istio.md +++ b/docs/receivers/service-mesh/istio/istio.md @@ -25,38 +25,40 @@ SentryFlow makes use of following to provide visibility into API calls: To Observe API calls of your workloads running on top of Istio Service Mesh in Kubernetes environment, follow the below steps: -1. Download SentryFlow manifest file +- Add SentryFlow repo - ```shell - curl -sO https://raw.githubusercontent.com/5GSEC/SentryFlow/refs/heads/main/deployments/sentryflow.yaml - ``` - -2. Update the `.receivers` configuration in `sentryflow` [configmap](../../../../deployments/sentryflow.yaml) as - follows: +```shell +helm repo add 5gsec https://5gsec.github.io/charts +helm repo update 5gsec +``` - ```yaml - filters: - server: - port: 8081 +- Update `values.yaml` file as follows. - # Envoy filter is required for `istio-sidecar` service-mesh receiver. - # Leave it as it is unless you want to use your filter. - envoy: - uri: 5gsec/sentryflow-httpfilter:v0.1 +```shell +helm show values 5gsec/sentryflow > values.yaml +``` - receivers: - serviceMeshes: - - name: istio-sidecar # SentryFlow makes use of `name` to configure receivers. DON'T CHANGE IT. - namespace: istio-system # Kubernetes namespace in which you've deployed Istio. - ... - ``` +```yaml +filters: + server: + # Existing snippets + + # Envoy filter is required for `istio-sidecar` service-mesh receiver. + # Leave it as it is unless you want to use your filter. + envoy: + uri: 5gsec/sentryflow-httpfilter:latest + +receivers: + serviceMeshes: + - name: istio-sidecar # SentryFlow makes use of `name` to configure receivers. DON'T CHANGE IT. + namespace: istio-system # Kubernetes namespace in which you've deployed Istio. + # Existing snippets +``` -3. Apply the updated manifest file: +- Deploy SentryFlow ```shell -kubectl apply -f sentryflow.yaml +helm install --values values.yaml sentryflow 5gsec/sentryflow -n sentryflow --create-namespace ``` -3. Trigger API calls to generate traffic. - -4. Use SentryFlow [log client](../../../../client) to see the API Events. +- Trigger API calls to generate traffic.