From 2999f1d9a3cf45f153356a1bd44e3389fb63db2a Mon Sep 17 00:00:00 2001 From: Jaskaran Sarkaria Date: Fri, 1 Sep 2023 13:44:53 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20write=20audit=20logs=20t?= =?UTF-8?q?o=20file=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 write audit logs to file * terraform-docs: automated action --------- Co-authored-by: github-actions[bot] --- README.md | 9 ++ configmap.tf | 201 +++++++++++++++++++++++++++++++++++++ cron.tf | 69 +++++++++++++ main.tf | 24 +---- templates/modsecurity.conf | 15 ++- templates/values.yaml.tpl | 44 ++++++-- variables.tf | 20 +++- 7 files changed, 347 insertions(+), 35 deletions(-) create mode 100644 configmap.tf create mode 100644 cron.tf diff --git a/README.md b/README.md index 9bb4159..d46517a 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,14 @@ No modules. |------|------| | [helm_release.nginx_ingress](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | | [kubectl_manifest.nginx_ingress_default_certificate](https://registry.terraform.io/providers/gavinbunney/kubectl/latest/docs/resources/manifest) | resource | +| [kubernetes_config_map.fluent-bit-config](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/config_map) | resource | +| [kubernetes_config_map.fluent_bit_lua_script](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/config_map) | resource | | [kubernetes_config_map.modsecurity_nginx_config](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/config_map) | resource | +| [kubernetes_cron_job_v1.restart_modsec_containers](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/cron_job_v1) | resource | | [kubernetes_namespace.ingress_controllers](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | +| [kubernetes_role_binding_v1.restart_modsec_containers](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role_binding_v1) | resource | +| [kubernetes_role_v1.restart_modsec_containers](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role_v1) | resource | +| [kubernetes_service_account_v1.restart_modsec_containers](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service_account_v1) | resource | | [template_file.nginx_ingress_default_certificate](https://registry.terraform.io/providers/hashicorp/template/latest/docs/data-sources/file) | data source | ## Inputs @@ -48,6 +54,7 @@ No modules. |------|-------------|------|---------|:--------:| | [backend\_repo](#input\_backend\_repo) | repository for the default backend app | `string` | `"ministryofjustice/cloud-platform-custom-error-pages"` | no | | [backend\_tag](#input\_backend\_tag) | tag of the default backend app | `string` | `"0.6"` | no | +| [cluster](#input\_cluster) | cluster name used for opensearch indicies | `string` | `""` | no | | [cluster\_domain\_name](#input\_cluster\_domain\_name) | The cluster domain used for externalDNS annotations and certmanager | `any` | n/a | yes | | [controller\_name](#input\_controller\_name) | Will be used as the ingress controller name and the class annotation | `string` | n/a | yes | | [default\_cert](#input\_default\_cert) | Useful if you want to use a default certificate for your ingress controller. Format: namespace/secretName | `string` | `"ingress-controllers/default-certificate"` | no | @@ -56,9 +63,11 @@ No modules. | [enable\_latest\_tls](#input\_enable\_latest\_tls) | Provide support to tlsv1.3 along with tlsv1.2 | `bool` | `false` | no | | [enable\_modsec](#input\_enable\_modsec) | Enable https://github.com/SpiderLabs/ModSecurity-nginx | `bool` | `false` | no | | [enable\_owasp](#input\_enable\_owasp) | Use default ruleset from https://github.com/SpiderLabs/owasp-modsecurity-crs/ | `bool` | `false` | no | +| [fluent\_bit\_version](#input\_fluent\_bit\_version) | fluent bit container version used to exrtact modsec audit logs | `string` | `"2.1.8-amd64"` | no | | [is\_live\_cluster](#input\_is\_live\_cluster) | For live clusters externalDNS annotation will have var.live\_domain (default *.cloud-platform.service.justice.gov.uk) | `bool` | `false` | no | | [live1\_cert\_dns\_name](#input\_live1\_cert\_dns\_name) | This is to add the live-1 dns name for eks-live cluster default certificate | `string` | `""` | no | | [live\_domain](#input\_live\_domain) | The live domain used for externalDNS annotation | `string` | `"cloud-platform.service.justice.gov.uk"` | no | +| [opensearch\_modsec\_audit\_host](#input\_opensearch\_modsec\_audit\_host) | domain endpoint for the opensearch cluster | `string` | `""` | no | | [replica\_count](#input\_replica\_count) | Number of replicas set in deployment | `string` | n/a | yes | ## Outputs diff --git a/configmap.tf b/configmap.tf new file mode 100644 index 0000000..ce6ad55 --- /dev/null +++ b/configmap.tf @@ -0,0 +1,201 @@ +resource "kubernetes_config_map" "fluent-bit-config" { + count = var.enable_modsec ? 1 : 0 + + metadata { + name = "fluent-bit-config" + namespace = "ingress-controllers" + labels = { + "k8s-app" = var.controller_name + } + } + data = { + "fluent-bit.conf" = <<-EOT + [SERVICE] + Flush 1 + Log_Level info + Daemon Off + Grace 30 + Parsers_File parsers.conf + Parsers_File custom_parsers.conf + HTTP_Server On + HTTP_Listen 0.0.0.0 + HTTP_Port 2020 + Storage.path /var/log/flb-storage/ + Storage.max_chunks_up 64 + Storage.backlog.mem_limit 5MB + + [INPUT] + Name tail + Alias modsec_nginx_ingress_audit_index + Tag cp-ingress-modsec-index-audit.* + Path /var/log/audit/*.log + Parser modsec-audit-log-index + Refresh_Interval 5 + Buffer_Max_Size 5MB + Buffer_Chunk_Size 1M + Offset_Key pause_position_modsec-audit-index + DB cp-ingress-modsec-audit-index.db + DB.locking true + Storage.type filesystem + Storage.pause_on_chunks_overlimit True + + [INPUT] + Name tail + Alias modsec_nginx_ingress_audit + Tag cp-ingress-modsec-audit.* + Path /var/log/audit/**/**/* + Parser docker + Refresh_Interval 5 + Buffer_Max_Size 5MB + Buffer_Chunk_Size 1M + Offset_Key pause_position_modsec-audit + DB cp-ingress-modsec-audit.db + DB.locking true + Storage.type filesystem + Storage.pause_on_chunks_overlimit True + + [FILTER] + Name lua + Match cp-ingress-modsec-audit.* + script /fluent-bit/scripts/cb_extract_tag_value.lua + call cb_extract_tag_value + + [FILTER] + Name parser + Parser generic-json + Match cp-ingress-modsec-audit.* + Key_Name log + Reserve_Data On + Preserve_Key On + + [OUTPUT] + Name opensearch + Alias modsec_nginx_ingress_audit + Match * + Host ${var.opensearch_modsec_audit_host} + Port 443 + Type _doc + Time_Key @timestamp + Logstash_Prefix ${var.cluster}_k8s_modsec_ingress + tls On + Logstash_Format On + Replace_Dots On + Generate_ID On + Retry_Limit False + AWS_AUTH On + AWS_REGION eu-west-2 + Suppress_Type_Name On + Buffer_Size False + EOT + + "custom_parsers.conf" = <<-EOT + [PARSER] + Name modsec-audit-log-index + Format regex + Regex ^(?[^ ]+) (?[^ ]+) (?.*)$ + Time_Key time + Time_Format %d/%m/%Y:T%H:%M:%S.%z + [PARSER] + Name initial-json + Format json + Time_Key time + Time_Keep On + + [PARSER] + Name generic-json + Format json + Time_Key time + Time_Format %Y-%b-%dT%H:%M:%S + Time_Keep On + # Command | Decoder | Field | Optional Action + # =============|==================|================= + Decode_Field_As escaped_utf8 log do_next + Decode_Field_As json log + EOT + } + + depends_on = [ + kubernetes_namespace.ingress_controllers, + ] + + lifecycle { + ignore_changes = [metadata[0].annotations] + } +} + +resource "kubernetes_config_map" "fluent_bit_lua_script" { + count = var.enable_modsec ? 1 : 0 + + metadata { + name = "fluent-bit-luascripts" + namespace = "ingress-controllers" + labels = { + "k8s-app" = var.controller_name + } + } + data = { + "cb_extract_tag_value.lua" = <<-EOT + function cb_extract_tag_value(tag, timestamp, record) + local github_team = string.gmatch(record["log"], '%[tag "github_team=([%a+|%-]*)"%]') + local github_team_from_json = string.gmatch(record["log"], '"tags":%[.*"github_team=([%a+|%-]*)".*%]') + + local new_record = record + local team_matches = {} + local json_matches = {} + + for team in github_team do + table.insert(team_matches, team) + end + + for team in github_team_from_json do + table.insert(json_matches, team) + end + + if #team_matches > 0 then + new_record["github_teams"] = team_matches + return 1, timestamp, new_record + + elseif #json_matches > 0 then + new_record["github_teams"] = json_matches + + return 1, timestamp, new_record + + else + return 0, timestamp, record + end + end + EOT + } + + depends_on = [ + kubernetes_namespace.ingress_controllers, + ] + + lifecycle { + ignore_changes = [metadata[0].annotations] + } +} + +resource "kubernetes_config_map" "modsecurity_nginx_config" { + count = var.enable_modsec ? 1 : 0 + + metadata { + name = "modsecurity-nginx-config" + namespace = "ingress-controllers" + labels = { + "k8s-app" = var.controller_name + } + } + data = { + "modsecurity.conf" = file("${path.module}/templates/modsecurity.conf"), + } + + depends_on = [ + kubernetes_namespace.ingress_controllers, + ] + + lifecycle { + ignore_changes = [metadata[0].annotations] + } +} + diff --git a/cron.tf b/cron.tf new file mode 100644 index 0000000..ff9da7e --- /dev/null +++ b/cron.tf @@ -0,0 +1,69 @@ +resource "kubernetes_service_account_v1" "restart_modsec_containers" { + metadata { + name = "restart-modsec-containers" + namespace = "ingress-controllers" + } +} + +resource "kubernetes_role_v1" "restart_modsec_containers" { + metadata { + name = "restart-modsec-containers" + namespace = "ingress-controllers" + } + + rule { + api_groups = ["apps", "applications"] + resources = ["deployments"] + verbs = ["get", "list", "patch"] + } +} + +resource "kubernetes_role_binding_v1" "restart_modsec_containers" { + metadata { + name = "restart-modsec-containers" + namespace = "ingress-controllers" + } + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "Role" + name = "restart-modsec-containers" + } + subject { + kind = "ServiceAccount" + name = "restart-modsec-containers" + namespace = "ingress-controllers" + } +} + +resource "kubernetes_cron_job_v1" "restart_modsec_containers" { + metadata { + name = "restart-modsec-containers-nightly" + namespace = "ingress-controllers" + } + spec { + concurrency_policy = "Forbid" + failed_jobs_history_limit = 2 + schedule = "00 23 * * *" + starting_deadline_seconds = 10 + successful_jobs_history_limit = 0 + job_template { + metadata {} + spec { + backoff_limit = 2 + active_deadline_seconds = 600 + ttl_seconds_after_finished = 10 + template { + metadata {} + spec { + service_account_name = "restart-modsec-containers" + container { + name = "kubectl" + image = "bitnami/kubectl" + command = ["kubectl", "rollout", "restart", "deployment/nginx-ingress-modsec-controller"] + } + } + } + } + } + } +} diff --git a/main.tf b/main.tf index 47f8e6c..f6390b5 100644 --- a/main.tf +++ b/main.tf @@ -59,6 +59,7 @@ resource "helm_release" "nginx_ingress" { enable_external_dns_annotation = var.enable_external_dns_annotation backend_repo = var.backend_repo backend_tag = var.backend_tag + fluent_bit_version = var.fluent_bit_version })] depends_on = [ @@ -98,26 +99,3 @@ resource "kubectl_manifest" "nginx_ingress_default_certificate" { var.dependence_certmanager ] } - -resource "kubernetes_config_map" "modsecurity_nginx_config" { - count = var.enable_modsec ? 1 : 0 - - metadata { - name = "modsecurity-nginx-config" - namespace = "ingress-controllers" - labels = { - "k8s-app" = var.controller_name - } - } - data = { - "modsecurity.conf" = file("${path.module}/templates/modsecurity.conf"), - } - - depends_on = [ - kubernetes_namespace.ingress_controllers, - ] - - lifecycle { - ignore_changes = [metadata[0].annotations] - } -} diff --git a/templates/modsecurity.conf b/templates/modsecurity.conf index 45e3084..95eb15b 100644 --- a/templates/modsecurity.conf +++ b/templates/modsecurity.conf @@ -233,8 +233,15 @@ SecAuditLogParts AEFHKZ # Use a single file for logging. This is much easier to look at, but # assumes that you will use the audit log only ocassionally. # -SecAuditLogType Serial -SecAuditLog /dev/stdout +SecAuditLogType Concurrent + +SecAuditLogDirMode 0777 +SecAuditLogFileMode 0777 + +SecAuditLog /var/log/audit/index.log + +SecAuditLog2 /var/log/audit/index2.log +SecAuditLogStorageDir /var/log/audit/ SecAuditLogFormat JSON SecRuleRemoveById 920350 @@ -268,7 +275,5 @@ SecUnicodeMapFile unicode.mapping 20127 # The following information will be shared: ModSecurity version, # Web Server version, APR version, PCRE version, Lua version, Libxml2 # version, Anonymous unique id for host. -SecStatusEngine On - -SecAuditLogStorageDir /var/log/audit/ +SecStatusEngine Off diff --git a/templates/values.yaml.tpl b/templates/values.yaml.tpl index eda0392..fa3b276 100644 --- a/templates/values.yaml.tpl +++ b/templates/values.yaml.tpl @@ -5,18 +5,29 @@ controller: replicaCount: ${replica_count} %{ if enable_modsec ~} + extraVolumes: + ## Additional volumes to the controller pod. + - name: logs-volume + emptyDir: {} + - name: modsecurity-nginx-config + configMap: + name: modsecurity-nginx-config + - name: fluent-bit-config + configMap: + name: fluent-bit-config + - name: fluent-bit-luascripts + configMap: + name: fluent-bit-luascripts + + extraVolumeMounts: ## Additional volumeMounts to the controller main container. + - name: logs-volume + mountPath: /var/log/audit/ - name: modsecurity-nginx-config mountPath: /etc/nginx/modsecurity/modsecurity.conf subPath: modsecurity.conf readOnly: true - - extraVolumes: - ## Additional volumes to the controller pod. - - name: modsecurity-nginx-config - configMap: - name: modsecurity-nginx-config %{ endif ~} updateStrategy: @@ -26,6 +37,27 @@ controller: minReadySeconds: 12 +%{ if enable_modsec ~} + extraInitContainers: + - name: init-file-permissions + image: busybox + command: ["sh", "-c", "chmod -R 777 /var/log/audit"] + volumeMounts: + - name: logs-volume + mountPath: /var/log/audit + + extraContainers: + - name: flb-modsec-audit-logs + image: fluent/fluent-bit:${fluent_bit_version} + volumeMounts: + - name: fluent-bit-config + mountPath: /fluent-bit/etc/ + - name: fluent-bit-luascripts + mountPath: /fluent-bit/scripts/ + - name: logs-volume + mountPath: /var/log/audit/ +%{ endif ~} + # -- Process Ingress objects without ingressClass annotation/ingressClassName field # Overrides value for --watch-ingress-without-class flag of the controller binary # Defaults to false diff --git a/variables.tf b/variables.tf index 30940d2..0028b83 100644 --- a/variables.tf +++ b/variables.tf @@ -70,4 +70,22 @@ variable "enable_external_dns_annotation" { variable "dependence_certmanager" { description = "cert-manager module dependences in order to be executed." -} \ No newline at end of file +} + +variable "cluster" { + description = " cluster name used for opensearch indicies" + type = string + default = "" +} + +variable "opensearch_modsec_audit_host" { + description = "domain endpoint for the opensearch cluster" + type = string + default = "" +} + +variable "fluent_bit_version" { + description = "fluent bit container version used to exrtact modsec audit logs" + type = string + default = "2.1.8-amd64" +}