Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
Last revisionBoth sides next revision
devops:deploy_elk_using_helm [2020/04/08 09:19] – [Ошибка при начальной смене паролей bin/elasticsearch-setup-passwords] admindevops:deploy_elk_using_helm [2021/04/08 14:05] – [Kibana Ingress] admin
Line 1: Line 1:
 +====== Add elastic repo ======
 +  helm repo add elastic https://helm.elastic.co
 +====== Install Elasticsearch ======
 +===== Create namespace =====
 +  kubectl create ns elasticsearch
 +===== deploy Elasticsearch from helm =====
 +  helm install --namespace elasticsearch --name elasticsearch elastic/elasticsearch --set replicas=1
 +===== Create Persistent Volume for Elasticsearch =====
 +  sudo mkdir /kubernetes_volumes/elasticsearch-data
 +  sudo chmod a+rw -R /kubernetes_volumes/elasticsearch-data/
 +<code>apiVersion: v1
 +kind: PersistentVolume
 +metadata:
 +   name: elasticsearch-data
 +   namespace: elasticsearch
 +   labels:
 +     app: elasticsearch-master
 +spec:
 +  capacity:
 +    storage: 32Gi
 +  accessModes:
 +  - ReadWriteOnce
 +  hostPath:
 +    path: "/kubernetes_volumes/elasticsearch-data"
 +    type: Directory
 +  persistentVolumeReclaimPolicy: Retain</code>
 +  
 +И дадим права на запись в индексы:
 +  kubectl exec -it -n elasticsearch elasticsearch-master-0 -- curl -XPUT -H "Content-Type: application/json" http://localhost:9200/_all/_settings -d '{"index.blocks.read_only_allow_delete": null}'
 +Так как нода у меня одна, то мне нужно запретить создавать реплики. Иниче - в дальнейшем кластер просто перестанет работать:
 +  kubectl exec -it -n elasticsearch elasticsearch-master-0 -- curl -H "Content-Type: application/json" -XPUT 'http://127.0.0.1:9200/_template/default' -d '{"index_patterns": ["*"],"order": -1,"settings": {"number_of_shards": "1","number_of_replicas": "0"}}'
 +И почистим уже созданные индексы:
 +  kubectl exec -it -n elasticsearch elasticsearch-master-0 -- curl -XDELETE 'http://127.0.0.1:9200/*'
 +Теперь в логах пода **elasticsearch-master-0** появится строка:
 +  Cluster health status changed from [YELLOW] to [GREEN]
 +И в этот elasticsearch можно будет писать логи.
 +====== Install Kibana ======
 +  kubectl create ns kibana
 +  helm install --namespace elasticsearch --name kibana elastic/kibana --set elasticsearchHosts=http://elasticsearch-master.elasticsearch.svc.cluster.local:9200
 +
 +    helm upgrade kibana elastic/kibana --set elasticsearchHosts=http://openresty-oidc-http.elasticsearch.svc.cluster.local:9200 --set elasticsearch.requestHeadersWhitelist=[X-Auth-Username]  
 +===== Kibana Ingress =====
 +<code>apiVersion: extensions/v1beta1
 +kind: Ingress
 +metadata:
 +  annotations:
 +    kubernetes.io/ingress.class: nginx
 +    cert-manager.io/cluster-issuer: letsencrypt
 +  name: kibana-ingress
 +  namespace: elasticsearch
 +spec:
 +  rules:
 +  - host: kibana.autosys.tk
 +    http:
 +      paths:
 +      - backend:
 +          serviceName: kibana-kibana
 +          servicePort: 5601
 +        path: /
 +  tls:
 +  - hosts:
 +    - kibana.autosys.tk
 +    secretName:  kibana-autosys-tk-tls
 +</code>
 +
 +===== Kibana Saved Objects =====
 +В кибане есть сохраненные пользователем объекты. Например - Index Patterns.\\
 +Создать можно так:
 +  kubectl exec -it -n elk elk-openresty-oidc-559f46d69d-shbwg -- curl -H 'Authorization: Basic dXN....o' -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d '{"attributes": {"title": "my-pattern-*"}}' http://elk-kb-http:5601/api/saved_objects/index-pattern/my-pattern
 +Посмотреть их можно так:
 +  kubectl exec -it -n elk elk-openresty-oidc-559f46d69d-shbwg -- curl -H 'Authorization: Basic dXN....o' http://elk-kb-http:5601/api/saved_objects/_find?type=index-pattern | grep '"type":"index-pattern"'
 +Удалить какой-то можно так:
 +  kubectl exec -it -n elk elk-openresty-oidc-559f46d69d-shbwg -- curl -X DELETE -H 'kbn-xsrf: true' -H 'Authorization: Basic dXN....o' http://elk-kb-http:5601/api/saved_objects/index-pattern/vrm
 +
 +
 +
 +
 +====== Logstash ======
 +https://habr.com/ru/post/421819/ \\
 +https://helm.elastic.co/ \\
 +  helm install --namespace elasticsearch --name logstash elastic/logstash -f ./values.yaml
 +
 +===== Настройка rsyslog и фильтра logstash =====
 +У меня **rsyslog** сыплет логи в таком виде:
 +  Dec 25 14:06:25 kub kubelet[989]: W1225 14:06:25.516800     989 volume_linux.go:45] Setting volume ownership for /var/lib/kubelet/pods/d36eb244-fe63-4f5d-b133-c4e4232c68b3/volumes/kubernetes.io~configmap/sc-dashboard-provider and fsGroup set. If the volume has a lot of files then setting volume ownership could be slow, see https://github.com/kubernetes/kubernetes/issues/69699
 +
 +Для того, чтобы **logstash** смог сформировать объект json-объект **message** нужно в конфигурацию прописать фильтр, который распарсит строку **message** , чтобы на выходе получился объект. \\
 +Встроенные паттерны тут: https://github.com/logstash-plugins/logstash-patterns-core/blob/master/patterns/grok-patterns \\
 +Проверить паттерны можно тут: http://grokdebug.herokuapp.com/ \\
 +Для вышеприведенной стоки подходит такой фильтр:
 +  %{SYSLOGTIMESTAMP:timestamp}%{SPACE}%{SYSLOGHOST:host}%{SPACE}%{SYSLOGPROG:process}:%{SPACE}%{GREEDYDATA:SYSLOGMESSAGE}
 +Для того, чтобы логи попадали в **elasticsearch** в конфиг **rsyslog** (/etc/rsyslog.conf) в конце надо дописать такое:
 +<code>*.*  @@logstash.autosys.tk:1514</code>
 +Тут *.* - это значит все записи, @@ - это TCP (@ - UDP), дальше хост **logstash** и порт.
 +===== logstash_values.yaml =====
 +<code>---
 +replicas: 1
 +
 +# Allows you to add any config files in /usr/share/logstash/config/
 +# such as logstash.yml and log4j2.properties
 +logstashConfig:
 +  logstash.yml: |
 +    http.host: "0.0.0.0"
 +    config.reload.automatic: "true"
 +    xpack.management.enabled: "false"
 +    
 +#    key:
 +#      nestedkey: value
 +#  log4j2.properties: |
 +#    key = value
 +
 +# Allows you to add any pipeline files in /usr/share/logstash/pipeline/
 +logstashPipeline:
 +  input_main.conf: |
 +    input {
 +      udp {
 +        port => 1514
 +        type => syslog
 +      }
 +      tcp {
 +        port => 1514
 +        type => syslog
 +      }
 +      http {
 +        port => 8080
 +      }
 +      # kafka {
 +      #  ## ref: https://www.elastic.co/guide/en/logstash/current/plugins-inputs-kafka.html
 +      #   bootstrap_servers => "kafka-input:9092"
 +      #   codec => json { charset => "UTF-8" }
 +      #   consumer_threads => 1
 +      #   topics => ["source"]
 +      #   type => "example"
 +      # }
 +    }
 +  filter_main.conf: |
 +    filter {
 +      if [type] == "syslog" {
 +       # Uses built-in Grok patterns to parse this standard format
 +        grok {
 +          match => {
 +            "message" => "%{SYSLOGTIMESTAMP:@timestamp}%{SPACE}%{SYSLOGHOST:host}%{SPACE}%{SYSLOGPROG:process}:%{SPACE}%{GREEDYDATA:syslogmessage}"
 +          }
 +        }
 +        # Sets the timestamp of the event to the timestamp of recorded in the log-data
 +        # By default, logstash sets the timestamp to the time it was ingested.
 +        #date {
 +        #  match => [ "timestamp", "MMM  d HH:mm:ss", "MMM dd HH:mm:ss" ]
 +        #}
 +        mutate {
 +          rename => ["syslogmessage", "message" ]
 +        }
 +      }
 +    }
 +  output_main.conf: |
 +    output {
 +     # stdout { codec => rubydebug }
 +      elasticsearch {
 +        hosts => ["${ELASTICSEARCH_HOST}:${ELASTICSEARCH_PORT}"]
 +        manage_template => false
 +        index => "logs-%{+YYYY.MM.dd}"
 +     }
 +     # kafka {
 +     #   ## ref: https://www.elastic.co/guide/en/logstash/current/plugins-outputs-kafka.html
 +     #   bootstrap_servers => "kafka-output:9092"
 +     #   codec => json { charset => "UTF-8" }
 +     #   compression_type => "lz4"
 +     #   topic_id => "destination"
 +     # }
 +    }
 +
 +# Extra environment variables to append to this nodeGroup
 +# This will be appended to the current 'env:' key. You can use any of the kubernetes env
 +# syntax here
 +extraEnvs: 
 +  - name: "ELASTICSEARCH_HOST"
 +    value: "elasticsearch-master.elasticsearch.svc.cluster.local"
 +  - name: "ELASTICSEARCH_PORT"
 +    value: "9200"
 +#  - name: MY_ENVIRONMENT_VAR
 +#    value: the_value_goes_here
 +
 +# A list of secrets and their paths to mount inside the pod
 +secretMounts: []
 +
 +image: "docker.elastic.co/logstash/logstash"
 +imageTag: "7.5.1"
 +imagePullPolicy: "IfNotPresent"
 +imagePullSecrets: []
 +
 +podAnnotations: {}
 +
 +# additionals labels
 +labels:
 +  app: logstash
 +  
 +logstashJavaOpts: "-Xmx1g -Xms1g"
 +
 +resources:
 +  requests:
 +    cpu: "100m"
 +    memory: "1536Mi"
 +  limits:
 +    cpu: "1000m"
 +    memory: "1536Mi"
 +
 +volumeClaimTemplate:
 +  accessModes: [ "ReadWriteOnce" ]
 +  resources:
 +    requests:
 +      storage: 1Gi
 +
 +rbac:
 +  create: false
 +  serviceAccountName: ""
 +
 +podSecurityPolicy:
 +  create: false
 +  name: ""
 +  spec:
 +    privileged: true
 +    fsGroup:
 +      rule: RunAsAny
 +    runAsUser:
 +      rule: RunAsAny
 +    seLinux:
 +      rule: RunAsAny
 +    supplementalGroups:
 +      rule: RunAsAny
 +    volumes:
 +      - secret
 +      - configMap
 +      - persistentVolumeClaim
 +
 +persistence:
 +  enabled: false
 +  annotations: {}
 +
 +extraVolumes: ""
 +  # - name: extras
 +  #   emptyDir: {}
 +
 +extraVolumeMounts: ""
 +  # - name: extras
 +  #   mountPath: /usr/share/extras
 +  #   readOnly: true
 +
 +extraContainers: ""
 +  # - name: do-something
 +  #   image: busybox
 +  #   command: ['do', 'something']
 +
 +extraInitContainers: ""
 +  # - name: do-something
 +  #   image: busybox
 +  #   command: ['do', 'something']
 +
 +# This is the PriorityClass settings as defined in
 +# https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass
 +priorityClassName: ""
 +
 +# By default this will make sure two pods don't end up on the same node
 +# Changing this to a region would allow you to spread pods across regions
 +antiAffinityTopologyKey: "kubernetes.io/hostname"
 +
 +# Hard means that by default pods will only be scheduled if there are enough nodes for them
 +# and that they will never end up on the same node. Setting this to soft will do this "best effort"
 +antiAffinity: "hard"
 +
 +# This is the node affinity settings as defined in
 +# https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#node-affinity-beta-feature
 +nodeAffinity: {}
 +
 +# The default is to deploy all pods serially. By setting this to parallel all pods are started at
 +# the same time when bootstrapping the cluster
 +podManagementPolicy: "Parallel"
 +
 +httpPort: 9600
 +
 +updateStrategy: RollingUpdate
 +
 +# This is the max unavailable setting for the pod disruption budget
 +# The default value of 1 will make sure that kubernetes won't allow more than 1
 +# of your pods to be unavailable during maintenance
 +maxUnavailable: 1
 +
 +podSecurityContext:
 +  fsGroup: 1000
 +  runAsUser: 1000
 +
 +securityContext:
 +  capabilities:
 +    drop:
 +    - ALL
 +  # readOnlyRootFilesystem: true
 +  runAsNonRoot: true
 +  runAsUser: 1000
 +
 +# How long to wait for logstash to stop gracefully
 +terminationGracePeriod: 120
 +
 +livenessProbe:
 +  httpGet:
 +    path: /
 +    port: http
 +  initialDelaySeconds: 300
 +  periodSeconds: 10
 +  timeoutSeconds: 5
 +  failureThreshold: 3
 +  successThreshold: 1
 +
 +readinessProbe:
 +  httpGet:
 +    path: /
 +    port: http
 +  initialDelaySeconds: 60
 +  periodSeconds: 10
 +  timeoutSeconds: 5
 +  failureThreshold: 3
 +  successThreshold: 3
 +
 +## Use an alternate scheduler.
 +## ref: https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/
 +##
 +schedulerName: ""
 +
 +nodeSelector: {}
 +tolerations: []
 +
 +nameOverride: ""
 +fullnameOverride: ""
 +
 +lifecycle: {}
 +  # preStop:
 +  #   exec:
 +  #     command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
 +  # postStart:
 +  #   exec:
 +  #     command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
 +
 +service:
 +  annotations: {}
 +  type: LoadBalancer
 +  ports:
 +  - name: beats
 +    port: 5044
 +    protocol: TCP
 +    targetPort: 5044
 +  - name: http
 +    port: 8080
 +    protocol: TCP
 +    targetPort: 8080
 +  - name: syslog
 +    port: 1514
 +    targetPort: 1514
 +</code>
 +
 +===== Create Persistent Volume for LogStash =====
 +  sudo mkdir /kubernetes_volumes/logstash-data
 +  sudo chmod a+rw -R /kubernetes_volumes/logstash-data
 +<code>apiVersion: v1
 +kind: PersistentVolume
 +metadata:
 +   name: logstash-data
 +   namespace: elasticsearch
 +   labels:
 +     app: logstash
 +spec:
 +  capacity:
 +    storage: 2Gi
 +  accessModes:
 +  - ReadWriteOnce
 +  hostPath:
 +    path: "/kubernetes_volumes/logstash-data"
 +    type: Directory
 +  persistentVolumeReclaimPolicy: Retain</code>
 + 
 +====== Проверка работоспособности ELK ======
 +Теперь можно проверить, что всё вместе работает. \\
 +Можно отправить сообщение в **logstash** на **input http**:
 +  curl -XPUT 'http://~~~logstash~IP~~~:8080/test/test1/1' -d 'hello'
 +В ответ должно быть **ok**. \\
 +А в **kibana** можно нажать **Discovery**, увидеть там новый индекс.
 +====== Filebeat ======
 +Для работы с файлами логов потребуется дополнительный сервис **Filebeat**, который будет читать логи из файла и отправлять в **Logstash**. Пример конфигурации:
 +<code>filebeat.inputs:
 +- type: log
 +  enabled: true
 +  paths:
 +      - /var/log/nginx/access.log
 +  fields:
 +    type: nginx
 +  fields_under_root: true
 +  scan_frequency: 5s
 +
 +output.logstash:
 +  hosts: ["logstash:5000"]</code>
 +
 +====== Настройка безопасности ======
 +
 +По-умолчанию стек **ELK** не обеспечивает безопасного доступа к данным и компоненты никак не аутентифицируются. То есть записать и читать данные может кто угодно. \\
 +Базовая бесплатная лицензия не позволяет аутентифицировать пользователей из каталогов **LDAP**, поэтому нужно либо платить, либо настраивать аутентификацют сторонними методами. \\
 +
 +
 +===== LDAP-аутентификация в Kibana и RBAC для elasticsearch помощью oidc и proxy =====
 +Общая идея такая:
 +  * Пользователи имеют доступ только к **kibana**
 +  * Перед **Kibana** работает **oidc-proxy** (**openresty**), который осуществляет аутентификацию пользователя с помощью **keycloak**. Этот прокси добавляет в запросы, поступающие в **kibana**, заголовки, содержащие информацию о пользователе (в частности - список групп).
 +  * **Kibana** добавляет выбранные заголовки в запросы к **elasticsearch**.
 +  * Запросы от **kibana** к **elasticsearch** идет через второй **proxy**, который смотрит состав групп из заголовков и проксирует запросы только с разрешенными для данной группы URI и HTTP-методами.
 +Используемые компоненты:
 +  * **Active Directory**
 +  * **Keycloak**
 +  * **openresty** + **lua-resty-openidc**
 +Вот ссылочки, которые помогали мне в настройке **oidc-proxy** для LDAP-аутентификации :
 +  - https://mapr.com/blog/how-secure-elasticsearch-and-kibana/
 +  - https://github.com/zmartzone/lua-resty-openidc
 +  - https://developers.redhat.com/blog/2018/10/08/configuring-nginx-keycloak-oauth-oidc/
 +  - https://daenney.github.io/2019/10/05/beyondcorp-at-home-authn-authz-openresty
 +  - https://discuss.elastic.co/t/kibana-default-basic-auth/86045
 +
 +Я изобретаю свои велосипеды, а вот есть некторые готовые компоненты:
 +  * образ **openresty** с **oidc** - https://github.com/flix-tech/openresty-oidc-proxy
 +  * вот решение для аутентификации **elasticsearch** в **LDAP** - https://mapr.com/blog/how-secure-elasticsearch-and-kibana/ - без использования **OIDC**, но там описана авторизация доступа к определенным **URI** и **HTTP**-методам с помощью **lua**, на основе имени пользователя (**HTTP**-заголовка) из реквеста. Я настраиваю более универсальный (но и более ресурсоемкий по причине перебора всех доступных групп) вариант с группами. Более экономичным может быть вариант с ролями. Для реализации ролей - их нужно настроить на **keycloak**. Роль, в сущности, это та же группа, однако, есть нюансы. **LDAP**-роль в **keycloak** дается на основании членства в группе. Роль может быть единственной (остальные, если есть, отбрасываются). Группы ролей, должны быть расположены в определенной **OU**. 
 +
 +==== Keycloak ====
 +Развернут с помощью **helm** - [[devops:deploy_keycloak_using_helm|]]. \\
 +**REALM** настроен так: https://habr.com/ru/post/441112/ 
 +==== Собираем образ openresty с модулем lua-resty-openidc ====
 +**Dockerfile** на базе https://github.com/Revomatico/docker-openresty-oidc/blob/master/Dockerfile :
 +<code>FROM alpine:3.10
 +MAINTAINER Mikhail Usik <mike@autosys.tk>
 +
 +ENV LUA_SUFFIX=jit-2.1.0-beta3 \
 +    LUAJIT_VERSION=2.1 \
 +    NGINX_PREFIX=/opt/openresty/nginx \
 +    OPENRESTY_PREFIX=/opt/openresty \
 +    OPENRESTY_SRC_SHA256=bf92af41d3ad22880047a8b283fc213d59c7c1b83f8dae82e50d14b64d73ac38 \
 +    OPENRESTY_VERSION=1.15.8.2 \
 +    LUAROCKS_VERSION=3.1.3 \
 +    LUAROCKS_SRC_SHA256=c573435f495aac159e34eaa0a3847172a2298eb6295fcdc35d565f9f9b990513 \
 +    LUA_RESTY_OPENIDC_VERSION=1.7.2-1 \
 +    VAR_PREFIX=/var/nginx
 +
 +RUN set -ex \
 +  && apk --no-cache add \
 +    libgcc \
 +    libpcrecpp \
 +    libpcre16 \
 +    libpcre32 \
 +    libssl1.1 \
 +    libstdc++ \
 +    openssl \
 +    pcre \
 +    curl \
 +    unzip \
 +    git \
 +    dnsmasq \
 +    ca-certificates \
 +  && apk --no-cache add --virtual .build-dependencies \
 +    make \
 +    musl-dev \
 +    gcc \
 +    ncurses-dev \
 +    openssl-dev \
 +    pcre-dev \
 +    perl \
 +    readline-dev \
 +    zlib-dev \
 +    libc-dev \
 +  \
 +## OpenResty
 +  && curl -fsSL https://github.com/openresty/openresty/releases/download/v${OPENRESTY_VERSION}/openresty-${OPENRESTY_VERSION}.tar.gz -o /tmp/openresty.tar.gz \
 +  \
 +  && cd /tmp \
 +  && echo "${OPENRESTY_SRC_SHA256} *openresty.tar.gz" | sha256sum -c - \
 +  && tar -xzf openresty.tar.gz \
 +  \
 +  && cd openresty-* \
 +  && readonly NPROC=$(grep -c ^processor /proc/cpuinfo 2>/dev/null || 1) \
 +  && ./configure \
 +    --prefix=${OPENRESTY_PREFIX} \
 +    --http-client-body-temp-path=${VAR_PREFIX}/client_body_temp \
 +    --http-proxy-temp-path=${VAR_PREFIX}/proxy_temp \
 +    --http-log-path=${VAR_PREFIX}/access.log \
 +    --error-log-path=${VAR_PREFIX}/error.log \
 +    --pid-path=${VAR_PREFIX}/nginx.pid \
 +    --lock-path=${VAR_PREFIX}/nginx.lock \
 +    --with-luajit \
 +    --with-pcre-jit \
 +    --with-ipv6 \
 +    --with-http_ssl_module \
 +    --without-http_ssi_module \
 +    --with-http_realip_module \
 +    --without-http_scgi_module \
 +    --without-http_uwsgi_module \
 +    --without-http_userid_module \
 +    -j${NPROC} \
 +  && make -j${NPROC} \
 +  && make install \
 +  \
 +  && rm -rf /tmp/openresty* \
 +  \
 +## LuaRocks
 +  && curl -fsSL http://luarocks.github.io/luarocks/releases/luarocks-${LUAROCKS_VERSION}.tar.gz -o /tmp/luarocks.tar.gz \
 +  \
 +  && cd /tmp \
 +  && echo "${LUAROCKS_SRC_SHA256} *luarocks.tar.gz" | sha256sum -c - \
 +  && tar -xzf luarocks.tar.gz \
 +  \
 +  && cd luarocks-* \
 +  && ./configure \
 +    --prefix=${OPENRESTY_PREFIX}/luajit \
 +    --lua-suffix=${LUA_SUFFIX} \
 +    --with-lua=${OPENRESTY_PREFIX}/luajit \
 +    --with-lua-lib=${OPENRESTY_PREFIX}/luajit/lib \
 +    --with-lua-include=${OPENRESTY_PREFIX}/luajit/include/luajit-${LUAJIT_VERSION} \
 +  && make build \
 +  && make install \
 +  \
 +  && rm -rf /tmp/luarocks* \
 +  && rm -rf ~/.cache/luarocks \
 +## Post install
 +  && ln -sf ${NGINX_PREFIX}/sbin/nginx /usr/local/bin/nginx \
 +  && ln -sf ${NGINX_PREFIX}/sbin/nginx /usr/local/bin/openresty \
 +  && ln -sf ${OPENRESTY_PREFIX}/bin/resty /usr/local/bin/resty \
 +  && ln -sf ${OPENRESTY_PREFIX}/luajit/bin/luajit-* ${OPENRESTY_PREFIX}/luajit/bin/lua \
 +  && ln -sf ${OPENRESTY_PREFIX}/luajit/bin/luajit-* /usr/local/bin/lua  \
 +  && ln -sf ${OPENRESTY_PREFIX}/luajit/bin/luarocks /usr/local/bin/luarocks \
 +  && ln -sf ${OPENRESTY_PREFIX}/luajit/bin/luarocks-admin /usr/local/bin/luarocks-admin \
 +  && echo user=root >> /etc/dnsmasq.conf \
 +## Install lua-resty-openidc
 +  && cd ~/ \
 +  # Fix for https://github.com/zmartzone/lua-resty-openidc/issues/213#issuecomment-432471572
 +#  && luarocks install lua-resty-hmac \
 +  && luarocks install lua-resty-openidc ${LUA_RESTY_OPENIDC_VERSION} \
 +## Install lua-resty-xacml-pep
 +#  && curl -fsSL https://raw.githubusercontent.com/zmartzone/lua-resty-xacml-pep/master/lib/resty/xacml_pep.lua -o /opt/openresty/lualib/resty/xacml_pep.lua \
 +## Cleanup
 +  && apk del .build-dependencies 2>/dev/null
 +
 +WORKDIR $NGINX_PREFIX
 +
 +CMD dnsmasq; openresty -g "daemon off; error_log /dev/stderr info;"
 +</code>\\
 +
 +==== ConfigMap для openresty ====
 +Создано по мотивам https://developers.redhat.com/blog/2018/10/08/configuring-nginx-keycloak-oauth-oidc/ и https://daenney.github.io/2019/10/05/beyondcorp-at-home-authn-authz-openresty \\
 +\\
 +Описаны два виртуальных сервра (оба - прокси). \\
 +  * Первый - для аутентификации в **kibana** (аутентифицирует пользователя с помощью **OIDC**). 
 +  * Второй - для авторизации запросов от **kibana** в **elasticsearch**. Проверяется содержимое заголовков (список групп пользователя) в запросах от **Kibana** и разрешения определенных методов при доступе к **elasticsearch**. 
 +<code>kind: ConfigMap
 +apiVersion: v1
 +metadata:
 +  name: openresty-oidc-config
 +  namespace: elasticsearch
 +data:
 +  nginx.conf: |
 +    worker_processes  1;
 +    events {
 +        worker_connections  1024;
 +    }
 +    http {
 +        include       mime.types;
 +        default_type  application/octet-stream;
 +        sendfile        on;
 +        keepalive_timeout  65;
 +        gzip  on;
 +        ##
 +        # LUA options
 +        ##
 +        lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
 +        lua_package_path '~/lua/?.lua;;';
 +        resolver 192.168.77.1;
 +        # cache for discovery metadata documents
 +        lua_shared_dict discovery 1m;
 +        # cache for JWKs
 +        lua_shared_dict jwks 1m;
 +        # allow the server to close connection on non responding client, this will free up memory
 +        reset_timedout_connection on;
 +    
 +        server {
 +        listen 80 default_server;
 +        server_name kibana.autosys.tk;
 +        #access_log /dev/stdout;
 +        #error_log /dev/stdout;
 +        access_by_lua '
 +          local opts = {
 +            redirect_uri = "/redirect_uri",
 +            accept_none_alg = true,
 +            discovery = "https://sso.autosys.tk/auth/realms/Autosys/.well-known/openid-configuration",
 +            client_id = "kibana",
 +            client_secret = "af903747-905c-4342-a62c-83033d3289cc",
 +            redirect_uri_scheme = "https",
 +            logout_path = "/logout",
 +            redirect_after_logout_uri = "https://sso.autosys.tk/auth/realms/Autosys/protocol/openid-connect/logout?redirect_uri=https://kibana.autosys.tk/",
 +            redirect_after_logout_with_id_token_hint = false,
 +            session_contents = {id_token=true}
 +          }
 +          -- call introspect for OAuth 2.0 Bearer Access Token validation
 +          local res, err = require("resty.openidc").authenticate(opts)
 +          if err then
 +            ngx.status = 403
 +            ngx.say(err)
 +            ngx.exit(ngx.HTTP_FORBIDDEN)
 +          end
 +          -- set data from the ID token as HTTP Request headers
 +          ngx.req.set_header("X-Auth-Username", res.id_token.preferred_username)
 +          ngx.req.set_header("X-Auth-Groups", res.id_token.groups)
 +          -- ngx.req.set_header("X-Auth-Audience", res.id_token.aud)
 +          -- ngx.req.set_header("X-Auth-Email", res.id_token.email)
 +          -- ngx.req.set_header("X-Auth-ExpiresIn", res.id_token.exp)
 +          -- ngx.req.set_header("X-Auth-Roles", res.id_token.roles)
 +          -- ngx.req.set_header("X-Auth-Name", res.id_token.name)
 +          -- ngx.req.set_header("X-Auth-Subject", res.id_token.sub)
 +          -- ngx.req.set_header("X-Auth-Userid", res.id_token.preferred_username)
 +          -- ngx.req.set_header("X-Auth-Locale", res.id_token.locale)
 +          -- Output headers to nginx err log
 +          --ngx.log(ngx.ERR, "Got header X-Auth-Userid: "..res.id_token.preferred_username..";")
 +        ';
 +        expires           0;
 +        add_header        Cache-Control private;
 +        location / {
 +          proxy_connect_timeout 5s;
 +          proxy_pass http://kibana-kibana.elasticsearch.svc.cluster.local:5601;
 +        }
 +      }
 +      server {
 +        listen 9200;
 +        access_log /dev/stdout;
 +        error_log /dev/stdout;
 +        access_by_lua '
 +        local restrictions = {
 +          elasticsearch_ro = {
 +            ["^/$"                            = { "GET" },
 +            ["^/?[^/]*/?[^/]*/_mget"          = { "GET", "POST" },
 +            ["^/?[^/]*/?[^/]*/_doc"           = { "GET" },
 +            ["^/?[^/]*/?[^/]*/_search"        = { "GET", "POST" },
 +            ["^/?[^/]*/?[^/]*/_msearch"       = { "GET" },
 +            ["^/?[^/]*/?[^/]*/_validate/query"] = { "GET" },
 +            ["/_aliases"                      = { "GET" },
 +            ["/_cluster.*"                    = { "GET" }
 +          },
 +          elasticsearch_rw = {
 +            ["^/$"                            = { "GET" },
 +            ["^/?[^/]*/?[^/]*/_search"        = { "GET", "POST" },
 +            ["^/?[^/]*/?[^/]*/_msearch"       = { "GET", "POST" },
 +            ["^/?[^/]*/traffic*"              = { "GET", "POST", "PUT", "DELETE" },
 +            ["^/?[^/]*/?[^/]*/_validate/query"] = { "GET", "POST" },
 +            ["/_aliases"                      = { "GET" },
 +            ["/_cluster.*"                    = { "GET" }
 +          },
 +          elasticsearch_full = {
 +            ["^/?[^/]*/?[^/]*/_bulk"         = { "GET", "POST" },
 +            ["^/?[^/]*/?[^/]*/_refresh"      = { "GET", "POST" },
 +            ["^/?[^/]*/?[^/]*/?[^/]*/_create"] = { "GET", "POST" },
 +            ["^/?[^/]*/?[^/]*/?[^/]*/_update"] = { "GET", "POST" },
 +            ["^/?[^/]*/?[^/]*/?.*"           = { "GET", "POST", "PUT", "DELETE" },
 +            ["^/?[^/]*/?[^/]*$"              = { "GET", "POST", "PUT", "DELETE" },
 +            ["/_aliases"                     = { "GET", "POST" }
 +          }
 +        }
 +        local groups = ngx.req.get_headers()["x-auth-groups"]
 +        local authenticated_group = nil
 +        local ngx_re = require "ngx.re"
 +        if ( type(groups) == "string" and string.len(groups) >= 1 ) then
 +          groups = string.lower(groups)
 +          ngx.log(ngx.ERR, "Got header X-Auth-Groups: "..groups..";")
 +          local groups_table = ngx_re.split(groups, ", ")
 +          local groups_number = table.getn(groups_table)
 +          ngx.log(ngx.ERR, "Groups number : "..groups_number..";")
 +          for i=1,groups_number do
 +            local group = groups_table[i]:gsub("/","")
 +            group = group:gsub("%s","_")
 +            ngx.log(ngx.ERR, "Check group: "..group..";")
 +            if (restrictions[group] ~= nil) then
 +              ngx.log(ngx.ERR, "User belongs to Authenticated Group: "..group..";")
 +              authenticated_group = group
 +              break
 +            end
 +          end
 +          -- exit 403 when no matching role has been found
 +          if authenticated_group == nil then
 +            ngx.header.content_type = "text/plain"
 +            ngx.log(ngx.ERR, "Unauthenticated request... User - "..ngx.req.get_headers()["x-auth-username"]..";")
 +            ngx.status = 403
 +            ngx.say("403 Forbidden: You don\'t have access to this resource.")
 +            return ngx.exit(403)
 +          end
 +          
 +          -- get URL
 +          local uri = ngx.var.uri
 +          ngx.log(ngx.DEBUG, uri)
 +          -- get method
 +          local method = ngx.req.get_method()
 +          ngx.log(ngx.DEBUG, method)
 +          
 +          local allowed  = false
 +          
 +          for path, methods in pairs(restrictions[authenticated_group]) do
 +          
 +            -- path matched rules?
 +            local p = string.match(uri, path)
 +            
 +            local m = nil
 +            
 +            -- method matched rules?
 +            for _, _method in pairs(methods) do
 +              m = m and m or string.match(method, _method)
 +            end
 +            
 +            if p and m then
 +              allowed = true
 +              ngx.log(ngx.NOTICE, method.." "..uri.." matched: "..tostring(m).." "..tostring(path).." for "..authenticated_group)
 +              break
 +            end
 +          end
 +
 +          if not allowed then
 +            ngx.header.content_type = "text/plain"
 +            ngx.log(ngx.WARN, "Group ["..authenticated_group.."] not allowed to access the resource ["..method.." "..uri.."]")
 +            ngx.status = 403
 +            ngx.say("403 Forbidden: You don\'t have access to this resource.")
 +            return ngx.exit(403)
 +          end
 +        end
 +        ';
 +        location / {
 +          proxy_connect_timeout 15s;
 +          proxy_pass http://elasticsearch-master.elasticsearch.svc.cluster.local:9200;
 +        }
 +      }
 +    }
 +</code>
 +В конфигурации первого прокси аутентификацию выполняет блок **access_by_lua**. В **local opts** прописано следующее:
 +  * **redirect_uri** - всегда будет **"/redirect_uri"**
 +  * **discovery** - ссылка с конфигурацией реалма. Обычно нужно заменить адрес хоста **keycloak** и имя реалма.
 +  * **client_id** - задается в **keycloak** при создании клиента.
 +  * **client_secret** - генерируется в **keycloak**
 +  * **redirect_uri_scheme** - http или https 
 +  * **logout_path** - всегда будет **"/logout"**
 +
 +В конфигурации второго прокси авторизацию также выполняет блок **access_by_lua**:
 +  * В блоке **local restrictions = {** содержится список таблиц (с именами групп), а в таблицах - список **uri** и методов разрешенных членам группы для этих **uri**. Имена групп в **LDAP**  могут включать в себя пробелы и большие буквы (при проверке имена групп из **LDAP** будут приведены к нижнему регистру, а пробелы будут преобразованы в символ подчеркивания).
 +  * В данном алгоритме группы, которые нашлись в заголовке **x-auth-groups** проверяются по порядку. Если нашлось совпадение имени группы из заголовка **x-auth-groups** с именем таблицы из **local restrictions**, то пользователю будут даны права в соотвествии с первой найденной группой.
 +==== openresty-oidc_deployment.yaml ====
 +<code>apiVersion: apps/v1
 +kind: Deployment
 +metadata:    
 +  name: openresty-oidc
 +  namespace: elasticsearch
 +spec:
 +  replicas: 1
 +  selector:  
 +    matchLabels:
 +      app: openresty-oidc
 +  template:  
 +    metadata:
 +      labels:
 +        app: openresty-oidc
 +    spec:
 +      imagePullSecrets:
 +      - name: autosys-regcred    
 +      containers:    
 +        - name: openresty-oidc
 +          image: registry.autosys.tk/openresty-oidc
 +          volumeMounts:
 +            - name: openresty-oidc-config-volume
 +              mountPath: /opt/openresty/nginx/conf/nginx.conf
 +              subPath: nginx.conf
 +      volumes:
 +        - name: openresty-oidc-config-volume
 +          configMap:
 +            name: openresty-oidc-config
 +</code>
 +
 +==== openresty-oidc-service ====
 +<code>apiVersion: v1
 +kind: Service
 +metadata:
 +  name: openresty-oidc-http
 +  namespace: elasticsearch
 +spec:
 +  ports:
 +  - name: http
 +    port: 80
 +    protocol: TCP
 +    targetPort: 80
 +  - name: elasticsearch
 +    port: 9200
 +    protocol: TCP
 +    targetPort: 9200
 +  selector:
 +    app: openresty-oidc
 +  sessionAffinity: None
 +  type: ClusterIP
 +</code>\\
 +==== kibana-values.yaml ====
 +В конфигурации **kibana** нужно внести следующие изменения с помощью **helm upgrade**:
 +  * Включить **elasticsearch.requestHeadersWhitelist**, чтобы **kibana** добавляла в свои запросы к **elasticsearch** заголовки с группами пользователя.
 +  * Указать в качестве **elasticsearchHosts** адрес авторизующего **proxy**.
 +<code>elasticsearchHosts: "http://openresty-oidc-http.elasticsearch.svc.cluster.local:9200"
 +kibanaConfig:
 +  kibana.yml: |
 +    server.name: kibana
 +    server.host: "0"
 +    xpack.monitoring.ui.container.elasticsearch.enabled: true
 +    elasticsearch.requestHeadersWhitelist:
 +    - authorization
 +    - x-auth-groups
 +    - x-auth-username
 +</code>
 +
 +==== kibana-ingress.yaml ====
 +<code>
 +apiVersion: extensions/v1beta1
 +kind: Ingress
 +metadata:
 +  annotations:
 +    cert-manager.io/cluster-issuer: letsencrypt
 +    kubernetes.io/ingress.class: nginx
 +  name: kibana-ingress
 +  namespace: elasticsearch
 +spec:
 +  rules:
 +  - host: kibana.autosys.tk
 +    http:
 +      paths:
 +      - backend:
 +          serviceName: openresty-oidc-http
 +          servicePort: 80
 +        path: /
 +  tls:
 +  - hosts:
 +    - kibana.autosys.tk
 +    secretName: kibana-autosys-tk-tls
 +
 +</code>\\
 +
 +
 +==== RBAC using Oidc proxy ====
 +  - С одной стороны - можно продолжать наполнять таблицу привилегий **OIDC**-proxy. Недостатки - большая и сложная таблица, медленная работа. 
 +  - C другой стороны - в коде OIDC-proxy средствами **ES API** можно реализовать механизм проверки наличия пользователя в базе **ES**, создания ее в случае отсутствия, а также назначения ролей встроенных в **ES**, в соответствии с группами.
 +  
 +API - https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html  \\
 +ROles - https://www.elastic.co/guide/en/elasticsearch/reference/current/built-in-roles.html
 +| API | RO | RW | FULL |
 +| /_cluster | GET | GET | GET PUT POST DELETE |
 +| /_cat | GET | GET | GET |
 +| /_nodes | GET | GET | GET POST |
 +| /_remote | GET | GET | GET |
 +| /_tasks  | GET | GET | GET |
 +| /_ccr  | GET | GET | GET |
 +| * | GET | GET | GET | PUT |
 +====== Разные проблемы ======
 +===== Произвольная деаутентификация клиента openresty oidc proxy =====
 +Меня зачпокала ситуация, когда вроде всё работает, но в любой момент в логе **nginx oidc-proxy** может появиться ошибка<code>2020/04/07 18:46:22 [alert] 12#0: *12 ignoring stale global SSL error (SSL: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt) while SSL handshaking to upstream, client: 10.244.0.71, server: kibanadomain.local, request: "GET /built_assets/css/plugins/ml/application/index.light.css HTTP/1.1", upstream: "https://10.104.117.4:5601/built_assets/css/plugins/ml/application/index.light.css", host: "kibana.domain.local", referrer: "https://kibana.domain.local/app/kibana</code>и дальше **cookie** сессии сбрасываются, клиент деаутетифицируется и последующие запросы уже идут с кодом 302 и редиректом на **redirect_uri**. \\
 +
 +==== Причина ====
 +Причина и решение описаны тут: https://github.com/bungle/lua-resty-session/issues/23 \\
 +Судя по всему - причина кроется в механизмах шифрования-расшифровывания. А именно - в сессионном ключе. Если явно не задано значение переменной **$session_secret**, то каждый **worker** сгенерирует свой собственный секрет и они не смогут расшифровать данные зашифрованные разными ключами. Поэтому - глюк плавающий. Он повторяется рандомно. \\
 +Решение - либо добавить в секцию **server** значение **$session_secret** длинной 32 байта:<code>server {
 +    ...
 +    set $session_secret T62DGscdGyb4So4tLsXhNIRdlEpt4J2k;</code>
 +либо использовать единственный **worker**.
 +
 +
 +
 +===== Elasticsearch operator и OSS docker images =====
 +При попытке использовать имиджи **Elasticsearch OSS 7.6.2**, для разворачивания кластера средствами **elasticsearch operator** все поды всегда уходили в бесконечный **InitError**.\\
 +==== Диагностика ====
 +Смотрим describe любого пода и видим:<code>Events:
 +  Type     Reason     Age                From                       Message
 +  ----     ------     ----               ----                       -------
 +  Normal   Scheduled  44s                default-scheduler          Successfully assigned default/elasticsearch-es-master-2 to kub-dev-master01
 +  Normal   Pulled     22s (x3 over 41s)  kubelet, kub-dev-master01  Container image "registry.rdleas.ru:5000/elasticsearch/elasticsearch-oss:7.6.2" already present on machine
 +  Normal   Created    21s (x3 over 40s)  kubelet, kub-dev-master01  Created container elastic-internal-init-filesystem
 +  Normal   Started    21s (x3 over 40s)  kubelet, kub-dev-master01  Started container elastic-internal-init-filesystem
 +  Warning  BackOff    7s (x5 over 36s)   kubelet, kub-dev-master01  Back-off restarting failed container</code>
 +Смотрим логи контейнера **elastic-internal-init-filesystem** и видим:<code>$ kubectl logs elasticsearch-es-master-0 -c elastic-internal-init-filesystem
 +unsupported_distribution</code>
 +Легкое гугление приводит нас сюда: https://github.com/sebgl/cloud-on-k8s/commit/c1a88cee00bc583dc28217747d4a39160904f013 , где написано, что OSS имиджи не поддерживаются оператором: <code>The operator only works with the official ES distributions to enable the security
 +available with the basic (free), gold and platinum licenses in order to ensure that
 +all clusters launched are secured by default.
 +
 +A check is done in the prepare-fs script by looking at the existence of the
 +Elastic License. If not present, the script exit with a custom exit code.
 +
 +Then the ES reconcilation loop sends an event of type warning if it detects that
 +a prepare-fs init container terminated with this exit code.</code>
 +
 +===== Ошибки SSL =====
 +Кластер развернут с помощью **elasticsearch operator 1.0.1**.
 +<code>$ kubectl exec -it elasticsearch-es-master-0 -- bin/elasticsearch-setup-passwords interactive
 +19:52:29.467 [main] WARN  org.elasticsearch.common.ssl.DiagnosticTrustManager - failed to establish trust with server at [10.244.0.210]; the server provided a certificate with subject name [CN=elasticsearch-es-http.default.es.local,OU=elasticsearch] and fingerprint [74c38cee7c20e080613da16e86c9d5570d238717]; the certificate has subject alternative names [DNS:elasticsearch-es-http.default.es.local,DNS:elasticsearch-es-http,DNS:elasticsearch-es-http.default.svc,DNS:elasticsearch-es-http.default]; the certificate is issued by [CN=elasticsearch-http,OU=elasticsearch]; the certificate is signed by (subject [CN=elasticsearch-http,OU=elasticsearch] fingerprint [5d410bb81a7da9148cf71156ee07e36fa85ee5ef] {trusted issuer}) which is self-issued; the [CN=elasticsearch-http,OU=elasticsearch] certificate is trusted in this ssl context ([xpack.security.http.ssl])
 +java.security.cert.CertificateException: No subject alternative names matching IP address 10.244.0.210 found
 +....
 +....
 +....
 +SSL connection to https://10.244.0.210:9200/_security/_authenticate?pretty failed: No subject alternative names matching IP address 10.244.0.210 found
 +Please check the elasticsearch SSL settings under xpack.security.http.ssl.
 +
 +ERROR: Failed to establish SSL connection to elasticsearch at https://10.244.0.210:9200/_security/_authenticate?pretty. 
 +command terminated with exit code 78</code>
 +
 +В кластере единственный master. При попытке подключения утилита обращается по IP-адресу, а не по имени. Видно, что сертификат у сервера есть. \\
 +Как написано тут: https://www.elastic.co/guide/en/elasticsearch/reference/master/trb-security-setup.html достаточно привести конфиг к такому виду:
 +<code>  xpack.security.enabled: true
 +  xpack.security.http.ssl.verification_mode: certificate</code>
 +
 +===== Ошибка при начальной смене паролей bin/elasticsearch-setup-passwords =====
 +При выполнении команды **bin/elasticsearch-setup-passwords** появляется ошибка : <code>$ kubectl exec -it elasticsearch-es-master-0 -- bin/elasticsearch-setup-passwords auto
 +Failed to authenticate user 'elastic' against https://10.244.0.219:9200/_security/_authenticate?pretty
 +Possible causes include:
 + * The password for the 'elastic' user has already been changed on this cluster
 + * Your elasticsearch node is running against a different keystore
 +   This tool used the keystore at /usr/share/elasticsearch/config/elasticsearch.keystore</code> 
 +Это значит, что пароли встроенных учеток уже заданы **elasticsearch operator** в процессе установки **elasticsearch**, они хранятся в секретах **elasticsearch-es-internal-users** и **elasticsearch-es-elastic-user**:
 +  kubectl get secrets elasticsearch-es-internal-users -o=jsonpath='{.data.elastic-internal}' | base64 --decode
 +и
 +  kubectl get secret elasticsearch-es-elastic-user -o=jsonpath='{.data.elastic}' | base64 --decode
 +Если на перед создание экземпляра кластера **elasticsearch** создать секреты **elasticsearch-es-internal-users** и **elasticsearch-es-elastic-user**, то встроенным учетным записям будут назначены указанные там пароли. Это написано тут (там же написано как заскриптовать смену пароля): https://github.com/elastic/cloud-on-k8s/issues/967 \\
 +А вообще - в ближайшее время **elasticsearch operator** позволит задавать пароли для встроенных учеток прямо в конфиге: https://github.com/elastic/cloud-on-k8s/pull/2682
  
  • devops/deploy_elk_using_helm.txt
  • Last modified: 2021/10/24 12:06
  • by admin