Kubernetes, доступ по сертификату пользователя

В принципе — это шпаргалка, что бы не забыть как все на самом деле происходит 🙂

Создание SSL ключа и сертификата пользователя

Создаём временную директорию:

# mkdir tmp
# cd tmp

Генерируем SSL ключ пользователя:

# openssl genrsa -out artur.key 4096

Создаём конфигурационный файл для openssl — csr.cnf следующего содержания:

[ req ]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn

[ dn ]
CN = artur
O = dev

[ v3_ext ]
authorityKeyIdentifier=keyid,issuer:always
basicConstraints=CA:FALSE
keyUsage=keyEncipherment,dataEncipherment
extendedKeyUsage=serverAuth,clientAuth

Генерируем запрос на сертификат:

# openssl req -config csr.cnf -new -key artur.key -nodes -out artur.csr

Формируем запрос на сертификат в формате base64 и помещаем его в переменную среды окружения:

# export BASE64_CSR=$(cat artur.csr | base64 | tr -d '\n')

Создаём yaml файл csr.yaml с запросом на подпись следующего содержания:

    apiVersion: certificates.k8s.io/v1beta1
    kind: CertificateSigningRequest
    metadata:
      name: artur_csr
    spec:
      groups:
      - system:authenticated
      request: ${BASE64_CSR}
      usages:
      - digital signature
      - key encipherment
      - server auth
      - client auth

Помещаем запрос в кубернетес:

# cat csr.yaml | envsubst | kubectl apply -f -

Контролируем наличие запроса:

# kubectl get csr

Подписываем и генерируем сертификат:

# kubectl certificate approve artur_csr

Копируем во временную директорию файл ~/.kube/config и в разделе users удаляем из него строки client-certificate-data и client-key-data.

Добавляем в config строку с сертификатом клиента в формате base64:

# echo "client-certificate-data: $(kubectl get csr artur_csr -o jsonpath={.status.certificate})" >> config

В редакторе делаем правильное выравнивание строки. Также везде меняем пользователя на artur.

Добавляем в config файл ключ пользователя в формате base64:

# kubectl --kubeconfig ./config config set-credentials artur --client-key=artur.key --embed-certs=true

Смотрим получившийся конфигурационный файл:

# kubectl --kubeconfig ./config config view
apiVersion: v1
    clusters:
    - cluster:
        certificate-authority-data: DATA+OMITTED
        server: https://192.168.0.45:6443
      name: cluster.local
    contexts:
    - context:
        cluster: cluster.local
        user: artur
      name: artur@cluster.local
    current-context: artur@cluster.local
    kind: Config
    preferences: {}
    users:
    - name: artur
      user:
        client-certificate-data: REDACTED
        client-key-data: REDACTED

Передаём файл config пользователю или копируем его в директорию ~/.kube пользователя.

Шпаргалка сделана по мотивам записок доброго человека.

Fluent-bit и PodSecurityPolicy

Жили мы жили. И вроде не плохо. Но на тесте решили поставить Kubernetes при помощи kubespray v2.11.

Накатили в новый кластер fluent-bit, с отлаженным ранее деплоем. И… обломс.

Ругается при деплое DaemonSet на:

securityContext:
          privileged: true
volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true

В итоге оказалось, что в новом облачке включена PodSecurityPolicy. И поскольку наш DaemonSet не использует (подключен) к существующим политикам случился облом.

Решение проблемы – Добавить PodSecurityPolicy и применить её к DaemonSet.

Определяем новую политику:

---
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
  name: psp.td-agent
spec:
  allowPrivilegeEscalation: true
  allowedCapabilities:
  - '*'
  allowedUnsafeSysctls:
  - '*'
  fsGroup:
    rule: RunAsAny
  hostIPC: true
  hostNetwork: true
  hostPID: true
  hostPorts:
  - max: 65535
    min: 0
  privileged: true
  runAsUser:
    rule: RunAsAny
  seLinux:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  volumes:
  - 'hostPath'
  - 'configMap'
  - 'emptyDir'
  - 'secret'

Стандартно создаем ServiceAccount:

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluent-bit
  namespace: logging

Определяем ClusterRole и в ней определяем используемый PodSecurityPolicy (выделено болдом):

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: psp:fluent-bit-read
rules:
- apiGroups:
  - '*'
#  - policy
  resources:
#  - podsecuritypolicies
  - '*'
  resourceNames:
  - psp.td-agent
  verbs:
#  - use
  - '*'

Я еще не до конца разобрался какие apiGroups, rules и resources необходимо использовать. Поэтому написал все (что разумеется не правильно). Если вы имеете, что сказать по этому поводу, скажите это в комментах.

И в конце ClusterRoleBinding:

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: psp:fluent-bit-read
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: psp:fluent-bit-read
subjects:
- kind: ServiceAccount
  name: fluent-bit
  namespace: logging

И естественно все собираем вместе:

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: psp:fluent-bit-read
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: psp:fluent-bit-read
subjects:
- kind: ServiceAccount
  name: fluent-bit
  namespace: logging

В DaemonSet указываем созданный нами serviceAccountName:

serviceAccountName: fluent-bit

Как удалить report в kibana

Сформированные по вашему запросу или автоматически отчёты, как и многое другое, хранятся непосредственно в индексах elasticsearch.

Удалить конкретный report из индекса нельзя. Несмотря на запросы от пользователей, разработчики эластика отказываются предоставить такой инструмент и предлагают удалять индекс целиком.

Индексы с отчетами начинаются с .reporting и удаляются либо в kibana в разделе Index Management либо при помощи утилиты curator.

Пример action удаления индекса для curator:

actions:
  1:
    action: delete_indices
    options:
      ignore_empty_list: True
      disable_action: True
    filters:
    - filtertype: pattern
      kind: prefix
      value: .reporting-
    - filtertype: age
      source: name
      direction: older
      timestring: '%Y.%m.%d'
      unit: days
      unit_count: 30

Elasticsearch переполнение файловой системы

После переполнения файловой системы, elastic автоматически переводит индекс в режим read only.

После увеличения свободного пространства на диске отключить это режим для конкретного индекса можно при помощи следующей команды:

PUT app-index-2019.08.12/_settings
{
  "index": {
    "blocks": {
      "read_only_allow_delete": "false"
    }
  }
}

Если в режим только для чтения переведены и системные индексы, например .kibana. Тогда команда будет выглядеть так:

PUT _settings
{
  "index": {
    "blocks": {
      "read_only_allow_delete": "false"
    }
  }
}

Эти команды можно ввести в Kibana в разделе Dev Tools

Или в командной строке:

curl -XPUT -H "Content-Type: application/json" http://localhost:9200/_all/_settings -d '{"index.blocks.read_only_allow_delete": false}'

Fluent Bit — особенности использования multiline в плагине tail

Input plugin tail имеет возможность обрабатывать многострочные лог файлы. Достаточно создать PARSER, при помощи которого можно определить первую строку такого сообщения. Все вроде бы работает, но мне захотелось не просто разобрать сообщение по тегам, но и в отдельном теге сохранить всё исходное сообщение.

По идее регулярное выражение, используемое в парсере для лога openfire

2018.04.16 10:48:15 ERROR [pool-2-thread-171]: org.jivesoftware.openfire.filetransfer.proxy.ProxyConnectionManager - Error processing file transfer proxy connection
java.io.IOException: Only SOCKS5 supported
        at org.jivesoftware.openfire.filetransfer.proxy.ProxyConnectionManager.processConnection(ProxyConnectionManager.java:156) ~[xmppserver-4.3.2.jar:4.3.2]
        at org.jivesoftware.openfire.filetransfer.proxy.ProxyConnectionManager.access$200(ProxyConnectionManager.java:53) ~[xmppserver-4.3.2.jar:4.3.2]
        at org.jivesoftware.openfire.filetransfer.proxy.ProxyConnectionManager$1$1.run(ProxyConnectionManager.java:125) [xmppserver-4.3.2.jar:4.3.2]
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [?:1.8.0_202]
        at java.util.concurrent.FutureTask.run(FutureTask.java:266) [?:1.8.0_202]
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_202]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_202]
        at java.lang.Thread.run(Thread.java:748) [?:1.8.0_202]

должно выглядеть как то так:

^(?<message>(?<time>\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2}) (?<loglevel>\w+) {1,2}(?<process>\[[^ ]*\]): .*)

Оказалось, что всё не так просто. Fluent bit находит первую строку, разбирает её, формирует теги. Вроде всё нормально.

Но остальные строки он тупо дописывает к последнему тегу регулярного выражения. И вместо того, что бы поместить их (как я ожидал) в тег message, он добавляет их к тегу process.

Поэтому, после обработки приходится «терять» теги time, loglevel и process используя следующее выражение вместо первого:

^(?<time>\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2}) (?<loglevel>\w+) {1,2}(?<process>\[[^ ]*\]): (?<message>.*)

Остальные строки многострочного лога будут добавляться к тегу message, но он не будет содержать время, loglevel и процесс.

Итого, первое регулярное выражение отлично работает с однострочными логами, но не будет работать с многострочными. Вот такая фича.

Fluent bit, timestamps и timezone — баги, они есть везде.

Fluent bit очень легкий агент по сбору логов и прочей информации для Ealstic Stack. Но, как и у всех программ, у него встречаются баги.

Например, существует проблема по формированию @timestamp различными input плагинами. Плагины tail и syslog считают, что текущее время компьютера находится во временной зоне UTC и добавляют к нему разницу между UTC и текущей тайм зоной. Это происходит даже в том случае, если текущее время системы сконфигурировано с учетом вашей временной зоны. А, например, плагин exec правильно читает текущее время системы.

В результате в индексах эластика записям присваивается не правильный @timestamp. И некоторые логи в индексе попадают в будущее 🙂

Бороться с этим можно только при помощи парсеров. В сообщениях, обрабатываемых fluent bit обязательно должно быть поле, содержащее время когда было сформировано событие. Обычно в лог файлах такое поле присутствует. Если его там нет, постарайтесь настроить программы так, что бы они помещали его в логи.

При определении секции [INPUT] используйте параметр Parser для однострочных логов. Или Parser_Firstline для многосторочных логов.

В секции PARSER, для проблемных плагинов явным образом указывайте параметр Time_Offset.

Например, для обработки информации получаемой из syslog используют стандартный парсер syslog-rfc3164. Поскольку input плагин syslog содержит баг, то придется в парсере добавить параметр Time_Offset. В результате конфигурация будет выглядеть как то так:

[INPUT]
    Name syslog
    Tag syslog
    Parser syslog-rfc3164
    Listen 127.0.0.1
    Port 5140
    Mode tcp

[PARSER]
    Name syslog-rfc3164
    Format regex
    Regex /^\<(?<pri>[0-9]+)\>(?<time>[^ ]* {1,2}[^ ]* [^ ]*) (?<host>[^ ]*) (?<ident>[a-zA-Z0-9_\/\.\-]*)(?:\[(?<pid>[0-9]+)\])?(?:[^\:]*\:)? *(?<message>.*)$/
    Time_Key time
    Time_Format %b %d %H:%M:%S
    Time_Format %Y-%m-%dT%H:%M:%S.%L
    Time_Keep On
    Time_Offset +0300

Итого, если вы видите, что события в эластике формируются в будущем, начинайте писать свой PARSER 🙂

Kibana и Apache

Kibana конечно сама хорошо работает с клиентами. Но лучше спрятать её за нормальным http сервером. Там где мне пришлось развертывать ELK пользуются только Apache, поэтому в качеcтве примера используется данный web сервер.

Сначала в конфигурационном файле kibana.yml определим два параметра:

server.basePath: "/kibana"
server.rewriteBasePath: true

Поскольку Apache выступает фронтендом для разных приложений, доступ к Kibana сделаем через путь http://any.body.com/kibana. В конфиге Apache определим Location.

<Location /kibana>
  ProxyPreserveHost On
  ProxyPass  http://any.ip.com:5601/kibana
  ProxyPassReverse  http://any.ip.com:5601/kibana
</Location>

Собственно, все. Дальше можно включать необходимые вам плюшки web сервера.

Новая книга Андрея Маркелова

Андрей, в прошлом известный преподаватель по RedHat. На данный момент, работающий в народном хозяйстве Швеции в компании Ericsson. Написал новую книгу Введение в технологии контейнеров и Kubernetes. Книга доступна как в печатном виде, так и в формате pdf. Рекомендую.

З.Ы. Регулярно покупаю его книги по OpenStack.

З.З.Ы. Попробовал воспроизвести почти все примеры, которые Андрей описал в книге. Всё работает. Но есть небольшие нюансы, которые в книге не учтены.

При конфигурации кубернетес, в разделе «Подготовка операционной системы» необходимо добавить, что требуется явная загрузка модуля ядра br_netfilter.

В случае CentOS, создать конфиг файл, в котором добавить имя модуля, что бы он загружался при старте системы.

vim /etc/modules-load.d/k8s.conf
  br_netfilter

и сразу руками загрузить его, что бы не делать перезагрузку системы.

modprobe br_netfilter

Иначе у вас не сработает запись единицы в файл /proc/sys/net/bridge/bridge-nf-call-iptables

И соответственно будет ругаться kubeadm init, при инициализации управляющей ноды.

З.З.З.Ы. Проблема PDF версии книги: символы тире в примерах yaml файлов — это типографские широкие тире. Простой копи-паст не получится.

Еще один пример использования нескольких таблиц маршрутизации

Добавил новый материал в раздел Материалы к курсам.

Два провайдера. Правильная конфигурация таблицы маршрутизации.

Или, что следует сделать в первую очередь при подключении двух и более провайдеров к Linux роутеру.

Странное rancher/docker vs LXC

Решил попробовать модный в кругах девопсов кубернейт/docker. Самое простое развертывание кластера, как мне показалось — использовать rancher, который ставит кубернейт из «коробки». Да и сам запускается в контейнере docker.

Поставил на двух виртуальных машинах. Собрал, запустил. Запустил внутри приложение WordPress (nginx, php, mariadb). Немного удивился, сколько ресурсов съела эта связка. Давно у меня машинка swap на 100% не кушала. Конечно я еще не настолько влез во внутренности. Но всё же. Последний раз такой жор ресурсов с приложением по умолчанию я видел у 1С ERP 🙂

При этом, на той же самой виртуалке LXC контейнер со всем этим добром работает не напрягаясь. В общем буду разбираться. Придётся еще 16 гигов памяти прикупить 🙂