TGViewer
Channel Public Channel
Сочный DevOps

Сочный DevOps

@andtree_sec

От CI/CD до SIEM, SOC и безопасности — здесь делюсь опытом, мыслями и полезняшками на стыке DevOps и безопасности. Всё сочное — в «Сочном DevOps».
Subscribers
420
Photos
8
Videos
0
Links
52
Recent Posts 20 shown
Post #223 131
Давненько не было сообщений. Скорее всего в будущем они уйдут в telegraf, который я еще не освоил, так как есть много всего, что хотелось бы описать, но все это не влезает в разрешенное кол-во символов в ТГ канал.

А пока делал вечерами еще одно приложение для android - FoodFreshTracker. Это трекер срока годности продуктов, умеет сканировать дату камерой, добавлять фото к продукту, описание. Есть настраиваемые push уведомления об истечении срока годности добавленного продукта.

Предполагаемый сценарий использования такой - при разборе сумок из магазина, попутно сканируем сроки годности, заносим продукты в приложение. Далее можно удобно отслеживать сроки годности и знать что нужно приготовить в первую очередь.
RuStore FoodFreshTracker - трекер срока годности продуктов в каталоге RuStore 🚀 FoodFreshTracker - трекер срока годности продуктов — Контроль сроков годности продуктов: сканер даты, напоминания, категории. 📱 Скачайте бесплатно на смартфон, ТВ или планшет. Официальная версия (1.0) в RuStore — до 1 тыс установок, рейтинг 4,9★. Безопасно…
  • 🔥 3
Post #222 271
Последнее время на работе часто приходится работать с apache flink и даже что-то писать на java. Это немного заставило меня вспомнить kotlin и разработку под android, которой я в свое время активно увлекался.

Поэтому решил написать еще одно приложение под android - PassKeep. Это менеджер паролей, полностью оффлайн, даже разрешения на интернет нет в AndroidManisfest. Данные (пароли) хранятся в зашифрованном виде (AES-256) в БД.
Есть возможность включения доступа в приложение по биометрии, темная тема.

По стеку - паттер MVVM, room для локальной базы, hilt для DI. UI - jetpack compose.
RuStore PassKeep – менеджер паролей в каталоге RuStore 🚀 PassKeep – менеджер паролей — PassKeep – менеджер паролей, работает полностью локально. 📱 Скачайте бесплатно на смартфон, ТВ или планшет. Официальная версия (1.2) в RuStore — до 1 тыс установок, рейтинг 5,0★. Безопасно для 0+.
Post #221 267
Немножко про молекула тесты...

У нас принято использовать в переменных lookup плагин для доступа к переменным из волт. Пример
kafka_server_username: "{{ lookup('community.hashi_vault.vault_kv2_get', vault_secret_path ~ '/kafka/env', engine_mount_point=vault_mount, token=vault_token, url=vault_url).secret['kafka_server_username'] }}"


Помним что данный плагин всегда выполняется на control plane хосте, а не на конечном узле, как например либой ansible модуль. Если молекула у нас запускается в docker. Тут будет проблема доступа до контейнера vault. Дело в том, что в контейнере с раннером, внутри которого запускаются контейнеры с хостами под полекула тесты доступа до ip адреса волт контейнера у нас нет. Получим ошибку timeout.

Проблему можно решить, если у нас проброшен docker.sock. Сначала определяем динамически docker GW так:

export MOLECULE_VAULT_URL="http://$(python3 -c "
data = open('/proc/net/route').readlines()
for line in data:
parts = line.split()
if parts[1] == '00000000': # default route
gw = int(parts[2], 16)
print(f'{gw&0xff}.{(gw>>8)&0xff}.{(gw>>16)&0xff}.{(gw>>24)&0xff}')
break
"):8200"

Это можно выполнить в before_script в сиай, который запускает молекула.

Затем используем эту переменную в тестах, например:
vault_url: "{{ lookup('env', 'MOLECULE_VAULT_URL') | default('http://172.17.0.1:8200') }}"


Решить подобным образом проблему с доступность, если у нас dind не получится, там еще один сетевой слой. В таком случае лучше вообще не использовать lookup, а читать переменные через модуль в самих тасках.

#molecule
  • ❤ 3
  • 👍 1
Post #220 266
Очередной небольшой проект.
Веб-приложение, которое агрегирует через стандартный механиз gitlab webhooks, который можно настроить на уровне любого проекта данные об МРах в UI и отправляет уведомления через HTTP например в telegram, mattermost, etc.

В чем смысл?

Обычно в SRE/DevOps командах ревью МРов это всегда боль, трата времени на поиск сообщений в корп. мессенджере, забывание вмержить МР, если в этот момент как обычно случился "разрыв контекста" и пришлось переключиться на другую задачу.

Приложения решает (или пытаестя) эту проблему тем, что агрегирует октрыте МРы в одном месте. Также отправляет нотификацию в мессенджер, предполагается, что для этого выделяется отдельный канал в корп. мессенджере.

Проект написан на nextJS, это react фреймворк, позволяет также строить и бэк. База данных - postgres, prisma как ORM.

На github подробная инструкция в README.

#pet
#gitlab
Post #219 263
Включаем профилирование в ansible

Профилирование, это вывод даты, времени запуска и итогового саммари по запускам тасок с подсчетом времени выполнения.

Включается в ansible.cfg
callbacks_enabled = ansible.posix.profile_tasks, ansible.posix.timer
stdout_callback = yaml

[callback_profile_tasks]
task_output_limit = 20
sort_order = descending
summary_only = false


stdout_callback - ставим более читаемый вывод вместо дефолтного.

task_ouput_limit - топ N самых долгих тасок в summary.

summary_only - если true, то только итоговый summary без времени каждой таски.

Итого получаем более расширенное логирования с временем когда была запущена таска и временем выполнения.

#ansible
  • 🔥 3
Post #218 316
Так выглядит общий dashbord работы с инцидентами.
Post #217 286
Мой очередной pet-проект - система управления инцидентами - induty.

Полный код с README на github.

Система предназначена для организации работы с инцидентами и внедрения on-call дежурст.

Модель доступа. Пользователи могут зарегистрироваться сами, либо их может зарегать кто-то еще, но с обязательной галкой Require password change on first login. Это заставит пользователя сменить пароль при первом входе в систему.
Далее глобальный админ создает "команды" внутри. И добавляет первых пользователей в эти команды, делая их owner-ами команд. Owner-ы команд имеет расширенные права по работе с командами, например добавление участников, создание расписаний и прочее.

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

Публичное API (бэк) написан на golang с использованием gin в качестве веб-сервера и СУБД postgres.

Фронт - react + vite.

Если кратко про архитектуру.
Бэк построен на микросервисах, их всего 5:
1. auth. Отвечает за авторизацию по JWT.
2. teams. Команды. Тут также поднят TLS сервер на доп порту, который по mTLS принимает внутренние запросы от других микросервисов.
3. incidents. Работа с инцидентами. Общается с teams по mTLS.
4. schedules. Работа с расписанием и шифтами. Также общается с teams по mTLS.
5. gateway. Прокся на go до других микросервисов.

Фронт отдает nginx, который проксирует все запросы в gateway, который их проксирует в микросервисы.
Внутренние ручки под mTLS начинаются с /internal и не доступны публично.

Система предполагает установку в self-hosted варианте. Ставится через docker-compose. Внутри репозитория есть Makefile для удобства инсталляции.

PS.
Скрин не влезает. Будет вторым сообщением)

#pet
#induty
  • 👍 1
  • 🔥 1
Post #216 248
Сочный DevOps Все бы ничего пересоздали и ладно, вот только при пересоздании меняется dataview id, а на dataview id могут быть завязаны rules в kibana. Следовательно они все перестанут работать.
Если вдруг кто-то пользуется или планирует, то данное поведение было пофикшено мной в этом PR.

Теперь добавление нового значения в поле namespaces у dataview не приводит к пересозданию. Оно обновляется без этого через spaces API кибаны.
  • ❤ 1
  • 🔥 1
Post #215 933
Написал небольшой exporter на go. C его помощью можно реализовать мониторинг истечения срока дейтсвия access token в проектах. Обычно они сплошь и рядом используются в CI, когда проектов много, забываешь что и где добавлял. Экспортер умеет собирать все токены и отдвать метрики в виде prometheus по gitlab группе или отдельным проектам.
Если будете использовать групповой токен, например чтобы собирать одним токеном сразу по всем проектам группы, то "сам себя" он видеть не будет, у группового токена другое API. Но мониторить все же можно, например задать статично переменную gitlab с timestamp вида:
<gitlab_domain_name> : <timestamp> ,<gitlab_domain_name> : <timestamp>, <gitlab_domain_name> : <timestamp>

И далее получить так:
vector(${gitlab:value} - time())


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

Код тут.

Также приложу статичный scrape для витории на всякий случай
- job_name: gitlab-access-token-exporter
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
action: keep
regex: monitoring;gitlab-access-token-exporter;http
- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name]
replacement: ${1}/${2}
target_label: kubernetes_service
- target_label: gitlab
replacement: <you gitlab domain name>


И вырожение для grafana alerts:
gitlab_project_access_token_expires_in_seconds > 0 and gitlab_project_access_token_expires_in_seconds < 14 * 24 * 3600


Тут алерт придет по токену, которому осталось 14 дней.

#monitoring
  • 👍 2
Post #214 257
Петля в DNS в k8s

Мы на работе используем стек victoriaMetrics. vmstorage вынесен отдельно на ВМ. В логах vmselect увидел стандартную ошибку резолва DNS
dial tcp4: lookup <host>


Далее сростил ноду, на которой запущен под vmselect с подов nodelocaldns, запущенной на той же ноде. Там была ошибка
[FATAL] plugin/loop: Loop (169.254.25.10:59951 -> 169.254.25.10:53) detected for zone ".", see https://coredns.io/plugins/loop#troubleshooting. Query: "HINFO 3948066235463357779.1875856472781764904."


Такое бывает если у нас DNS петля. В конфигурации nodelocaldns:
.:53 {
errors
cache 30
reload
loop
bind 169.254.25.10
forward . /etc/resolv.conf
prometheus :9253
}


Те зону . мы ворфардим в /etc/resolv.conf на ноде. А в нем в свою очередь, помимо основных DNS естественно указан адрес 169.254.25.10. В итоге часть внешних запросов могла уйти обратно в локальный DNS-кеш и образовать петлю. Это проявлялось не всегда, потому что forward при нескольких upstream по умолчанию выбирает их случайно.

Тут еще важно заметить, что plugin/loop детектит такую проблему на своем стартовом HINFO probe и завершает процесс, поэтому nodelocaldns может уходить в рестарты.

Для починки, нужно в forward передать основные используемые DNS адреса.

#k8s
  • 👍 1
  • 🤔 1
Post #213 191
Molecule в сиай

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

Предположим, что в роли в molecule у нас есть несколько сценариев.
default(debian), centos, rocky.
Можно добавить сколько угодно своих или разделить еще более явно сценарии по версиям дистрибутивов.

Основной .gitlab-ci:
workflow:
auto_cancel:
on_new_commit: interruptible

variables:
ANSIBLE_MAIN_VERSION: "ansible-2.15"
ANSIBLE_HOST_KEY_CHECKING: "false"
ANSIBLE_RETRY_FILES_ENABLED: "false"
ANSIBLE_SSH_PIPELINING: "true"
ANSIBLE_DISPLAY_SKIPPED_HOSTS: "false"
ANSIBLE_FORCE_COLOR: "true"

stages:
- primary_test
- additional_tests

before_script:
- export PATH="/home/gitlab-runner/.local/bin:$PATH"
- pip install --no-cache-dir --upgrade --ignore-installed tox
- |
for req in molecule/*/requirements.yml; do envsubst < "$req" > /tmp/req.yml.tmp && mv /tmp/req.yml.tmp "$req"
done

# Базовый тест: main ansible version × все сценарии
molecule-test:
stage: primary_test
script:
- tox -e ${ANSIBLE_MAIN_VERSION}
after_script:
- >
if [ $CI_JOB_STATUS == 'canceled' ]; then
molecule destroy --scenario-name ${MOLECULE_SCENARIO} || true
fi
parallel:
matrix:
- MOLECULE_SCENARIO:
- default
- centos
- rocky
interruptible: true
tags:
- molecule-test

# Дополнительные тесты: остальные ansible versions × все сценарии
tox-tests:
stage: additional_tests
script:
- tox -e ${ANSIBLE}
after_script:
- >
if [ $CI_JOB_STATUS == 'canceled' ]; then
molecule destroy --scenario-name ${MOLECULE_SCENARIO} || true
fi
parallel:
matrix:
- ANSIBLE:
- ansible-2.16
- ansible-2.17
- ansible-2.18
MOLECULE_SCENARIO:
- default
- centos
- rocky
interruptible: true
tags:
- molecule-test


В основном тесте прогоняем все сценарии по всем версиям дистрибутивов но с ansible-core 2.15. Остальное запускаем через matrix и tox.

Пример tox.ini:
[tox]
envlist = ansible-2.{15,16,17,18}
skipsdist = true
recreate = true
parallel_show_output = true

[testenv]
commands =
python --version
ansible --version
ansible-lint --version
molecule --version
molecule test --scenario-name {env:MOLECULE_SCENARIO:default}

setenv =
TOX_ENVNAME={envname}
PY_COLORS=1
ANSIBLE_FORCE_COLOR=1
ANSIBLE_HOST_KEY_CHECKING="false"
ANSIBLE_RETRY_FILES_ENABLED="false"
ANSIBLE_SSH_PIPELINING="true"
ANSIBLE_DISPLAY_SKIPPED_HOSTS="false"

passenv = *

[testenv:ansible-2.15]
basepython = python3.11
deps =
-rrequirements.txt
ansible-core==2.15.*
ansible-lint==24.*

[testenv:ansible-2.16]
basepython = python3.12
deps =
-rrequirements.txt
ansible-core==2.16.*
ansible-lint==24.*

[testenv:ansible-2.17]
basepython = python3.12
deps =
-rrequirements.txt
ansible-core==2.17.*
ansible-lint==24.*

[testenv:ansible-2.18]
basepython = python3.12
deps =
-rrequirements.txt
ansible-core==2.18.*
ansible-lint==24.*

Итого на стадии additional_tests мы прогоняем тесты по всей матрице. Разные версии ansible + сценарии. Естественно раннер должен быть в таком случае привелигированным и правильно настроенным. Но это уже отдельная история.

Если используйте tox 4.x.x версии, то лучше использовать именование без точки, иначе может быть конфликт. Например в matrix можно передать так значения:
- ANSIBLE:         
- py311-ansible215
- py312-ansible216
- py312-ansible217
- py312-ansible218


Соответственно в tox.ini:
[tox]
envlist =
py311-ansible215
py312-ansible216
py312-ansible217
py312-ansible218


И далее заменить именование по файлу.

#ansible
#molecule
  • ❤ 2
  • 👌 1
Post #212 236
Вот так можно посмотреть сработки kyverno политики по всем ns-ам. Бывает полезно, когда вводишь новую политику в режиме аудита и собираешь инфу. Тут вывод по статусу fail, т.е. там, где политика что-либо заблокировала бы.

kubectl get policyreport -A -o json | \
jq '.items[] | select(.results[] | .policy == "<policy_name>" and .result == "fail") | {ns: .metadata.namespace, resource: .scope, results: [.results[] | select(.policy == "<policy_name>" and .result == "fail") | {rule: .rule, message: .message, timestamp: .timestamp.seconds | todate}]}'


#kyverno
  • 👍 1
  • 🤯 1
Post #211 238
Terraform провайдер для управления объектами ELK

Мы используем официальный - https://registry.terraform.io/providers/elastic/elasticstack/latest

Это небольшая заметка по особенностям API kibana, как следствие поведение провайдера.
Предположим есть некий json dataview:
{
"data_view": {
"name": "dataview_name",
"title": "dataview_title",
"allowNoIndex": true,
"allowHidden": true,
"namespaces": ["default"]
}
}


Мы его обрабатываем в коде и накладываем на схеме провайдера в данном случае вот так:
locals {
dataview_root = "${var.json_base_path}/dataviews"
dataview_files = fileset(local.dataview_root, "**/*.json")

dataview_map = {
for f in local.dataview_files :
replace(basename(f), ".json", "") => jsondecode(file("${local.dataview_root}/${f}"))
}
}

resource "elasticstack_kibana_data_view" "kibana_dataview" {
for_each = local.dataview_map

data_view = {
title = try(each.value.data_view.title, each.value.title)
name = try(each.value.data_view.name, each.value.name, each.key)
time_field_name = try(each.value.data_view.timeFieldName, each.value.timeFieldName, null)
namespaces = try(each.value.data_view.namespaces, each.value.namespaces, null)
allow_no_index = try(each.value.data_view.allowNoIndex, each.value.allowNoIndex, null)
type = try(each.value.data_view.type, each.value.type, null)

field_attrs = {
for field_name, attrs in try(each.value.data_view.fieldAttrs, each.value.fieldAttrs, {}) :
field_name => {
count = try(attrs.count, null)
custom_label = try(attrs.customLabel, null)
}
}

field_formats = {
for field_name, format in try(each.value.data_view.fieldFormats, each.value.fieldFormats, {}) :
field_name => jsonencode(format)
}

runtime_field_map = try(jsonencode(each.value.data_view.runtimeFieldMap), jsonencode(each.value.runtimeFieldMap), null)

source_filters = [
for filter in try(each.value.data_view.sourceFilters, each.value.sourceFilters, []) : {
value = filter.value
}
]
}
space_id = try(each.value.space_id, "default")
}


Применяем в кластер, все ок, при смене name, title тоже все ок, dataview обновиться, а вот при смене значения например у namespace или поля allowNoIndex объект будет пересоздан. Дело в том, что так работает API kibana, как следствие провайдер, который кстати использует их официальную go либу.
Все бы ничего пересоздали и ладно, вот только при пересоздании меняется dataview id, а на dataview id могут быть завязаны rules в kibana. Следовательно они все перестанут работать.

#terraform
Post #210 237
Иногда так бывает, что в инфре нужно поддерживать дистрибутивы EL8 (RedHat семейство 8 версии). Если захотите запустить на них любой модуль работы с пакетами на Ansible версии выше 2.17, то получите ошибку вида:
SyntaxError: future feature annotations is not defined


В чём суть. EL8 поставляется с Python 3.6. Модули Ansible 2.17 внутри используют синтаксис from __future__ import annotations, который появился только в Python 3.7. Отсюда несовместимость. Причём упадут все модули - package_facts, package, yum, dnf, command, setup и другие. Механизм один и тот же: Ansible упаковывает любой модуль через AnsiballZ-фреймворк и запускает его локальным Python-интерпретатором на хосте.
Единственный модуль который не использует Python на хосте вообще — raw. Он передаёт команду напрямую через SSH. Его и будем использовать для фиксов.

Сначала определяем ОС через raw:
- name: "Detect OS family via raw"
ansible.builtin.raw: |
if [ -f /etc/redhat-release ]; then
major=$(rpm -E '%{rhel}' 2>/dev/null || grep -oP '\d+' /etc/redhat-release | head -1)
echo "RedHat:${major}"
else
echo "other"
fi
register: os_detection
changed_when: false


Выставляем флаг:
- name: "Set EL8 flag"
ansible.builtin.set_fact:
is_el8: "{{ os_detection.stdout | trim is search('^RedHat:8') }}"


ansible.builtin.setup запускаем только для не-EL8:
name: "Populate ansible facts"
ansible.builtin.setup:
when: not is_el8


Для EL8 собираем нужные факты через raw:
- name: "Populate ansible facts via raw (EL8)"
# We already now that's 8 major version via is_el8 var
ansible.builtin.raw: |
python3 -c "
import platform, json, subprocess
print(json.dumps({
'ansible_distribution': open('/etc/system-release').read().split()[0],
'ansible_distribution_major_version': '8',
'ansible_os_family': 'RedHat',
'ansible_architecture': platform.machine(),
'ansible_hostname': platform.node(),
}))
" 2>/dev/null
register: raw_facts
changed_when: false
when: is_el8

- name: "Set ansible facts from raw (EL8)"
ansible.builtin.set_fact:
ansible_distribution: "{{ (raw_facts.stdout | trim | from_json).ansible_distribution }}"
ansible_distribution_major_version: "{{ (raw_facts.stdout | trim | from_json).ansible_distribution_major_version }}"
ansible_os_family: "{{ (raw_facts.stdout | trim | from_json).ansible_os_family }}"
ansible_architecture: "{{ (raw_facts.stdout | trim | from_json).ansible_architecture }}"
ansible_hostname: "{{ (raw_facts.stdout | trim | from_json).ansible_hostname }}"
when: is_el8


Все операции с пакетами для EL8 делаем тоже через raw:
- name: "Install package (EL8)"
ansible.builtin.raw: dnf -y install <package_name>
when: is_el8

- name: "Gather package list (EL8)"
ansible.builtin.raw: rpm -qa --qf '%{NAME}\n'
register: rpm_list
changed_when: false
when: is_el8


Можно конечно установить/скомпилировать свежий Python, но иногда нужно работать с тем что есть.

#ansible
  • 👍 2
  • ❤ 1
Post #209 249
Проблема 8 DNS запросов в k8s

В чем суть проблемы. В базовой инсталяции кластера запустим тестовый под
kubectl run test-coredns -t -i --rm --image centosadmin/utils -- bash
tcpdump -neli eth0 port 53


В другой консоли:
kubectl exec -it test-coredns -- bash
curl ya.ru


Возвращаемся в консоль с tcpdump:
19:35:13.236710 a6:14:ac:ef:cd:7d > ee:ee:ee:ee:ee:ee, ethertype IPv4 (0x0800), length 91: 10.244.31.1.39006 > 10.96.0.10.53: 53058+ A? ya.ru.default.svc.cluster.local. (49)
19:35:13.236843 a6:14:ac:ef:cd:7d > ee:ee:ee:ee:ee:ee, ethertype IPv4 (0x0800), length 91: 10.244.31.1.39006 > 10.96.0.10.53: 53306+ AAAA? ya.ru.default.svc.cluster.local. (49)
19:35:13.237767 a6:14:ac:ef:cd:7d > ee:ee:ee:ee:ee:ee, ethertype IPv4 (0x0800), length 83: 10.244.31.1.37179 > 10.96.0.10.53: 54238+ A? ya.ru.svc.cluster.local. (41)
19:35:13.237810 a6:14:ac:ef:cd:7d > ee:ee:ee:ee:ee:ee, ethertype IPv4 (0x0800), length 83: 10.244.31.1.37179 > 10.96.0.10.53: 54541+ AAAA?
19:35:13.238249 a6:14:ac:ef:cd:7d > ee:ee:ee:ee:ee:ee, ethertype IPv4 (0x0800), length 79: 10.244.31.1.58835 > 10.96.0.10.53: 11867+ A? ya.ru.cluster.local. (37)
19:35:13.238287 a6:14:ac:ef:cd:7d > ee:ee:ee:ee:ee:ee, ethertype IPv4 (0x0800), length 79: 10.244.31.1.58835 > 10.96.0.10.53: 12098+ AAAA? ya.ru.cluster.local. (37)
19:35:13.238848 a6:14:ac:ef:cd:7d > ee:ee:ee:ee:ee:ee, ethertype IPv4 (0x0800), length 65: 10.244.31.1.38399 > 10.96.0.10.53: 29445+ A? ya.ru. (23)
19:35:13.238880 a6:14:ac:ef:cd:7d > ee:ee:ee:ee:ee:ee, ethertype IPv4 (0x0800), length 65: 10.244.31.1.38399 > 10.96.0.10.53: 29657+ AAAA? ya.ru. (23)


Из лога видим, что было отправлено 8 запросов к coreDNS. Проблема 8 запросов в CoreDNS возникает из-а того, что при разрешении DNS-запросов в k8s они могут дублироваться до 8 раз. Это связано с особенностями параметра ndots, который определяет, сколько точек должно быть в имени перед добавлением суффиксов поиска. Запросы могут отправляться с различными комбинациями суффиксов (например .svc.cluster.local), что приводит к множественным попыткам резолва. Эту проблема решает плагин autopath в CoreDNS.

Редактируем конфиг мапу:
kubectl edit configmap -n kube-system coredns


В открывшемся файле меняем pods insecure на pods verified  и дописываем под словом ready:
autopath @kubernetes


В итоге должно получиться что-то вроде:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
autopath @kubernetes
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods verified
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}


После применения повторяем тест:
19:39:20.657634 7e:a9:bf:07:8a:1b > ee:ee:ee:ee:ee:ee, ethertype IPv4 (0x0800), length 91: 10.244.152.3.46411 > 10.96.0.10.53: 55892+ A? ya.ru.default.svc.cluster.local. (49)
19:39:20.657762 7e:a9:bf:07:8a:1b > ee:ee:ee:ee:ee:ee, ethertype IPv4 (0x0800), length 91: 10.244.152.3.46411 > 10.96.0.10.53: 56213+ AAAA? ya.ru.default.svc.cluster.local. (49)


Также в решение данной проблемы помогает установка NodeLocal DNS. Он снижает нагрузку на CoreDNS, создавая локальный кеш DNS на каждой ноде. Ну и никто не мешает использовать комбинированное решение.

#kubernetes
  • 👍 3
  • ❤ 1
Post #208 358
RCE через nodes/proxy GET в Kubernetes

Исследователь Graham Helton опубликовал интересную находку: разрешение nodes/proxy GET в Kubernetes позволяет выполнять команды в любом поде кластера, включая привилегированные системные.
В чем суть - kubelet принимает решение об авторизации на основе начального HTTP-метода WebSocket handshake (GET), а не проверяет реальную операцию. При использовании WebSocket для /exec endpoint требуется только nodes/proxy GET, хотя должно требоваться CREATE.
Из интересного:
1. Затронуто 69 helm charts (Prometheus, Grafana, Datadog, Elastic Agent, Cilium, New Relic и др.)
2. Обход идет через прямое подключение к Kubelet API (port 10250)
3. AuditPolicy не логирует выполнение команд через прямое подключение к Kubelet
4. Kubernetes Security Team закрыли репорт как "Working as intended"
PoC:
websocat --insecure \
--header "Authorization: Bearer $TOKEN" \
--protocol v4.channel.k8s.io \
"wss://$NODE_IP:10250/exec/default/nginx/nginx?output=1&error=1&command=id"

Детекция:
Автор предоставил скрипт для проверки всех service accounts в кластере.
Рекомендуемое решение от Kubernetes — внедрение KEP-2862 (Fine-Grained Kubelet Authorization), но оно пока в Beta и не решает базовую проблему.

Не забудьте проверить ваши кластера!

#kubernetes #rce #security
  • 👍 1
Post #207 382
Debian 13 и ключи GPG

Есть у нас ansible роль для раскатки агентов безопасности. Под debian13 для скачивания GPG ключа из репозитория использовался ansible модуль ansible.builtin.get_url. Как оказалось это неверно, ибо он качает в ASCII-armored, а 13 Debian требует бинарный формат GPG keyring.
Модуль ansible.builtin.apt_key по 13 дебиан не работает, кстати. Следовательно при прокатке можно увидеть ошибку вида:

<file> ignored as the file has an unsupported filetype.


Пофиксить легко, используем ansible.builtin.shell:
- name: "Add GPG key"
ansible.builtin.shell: |
curl -fsSL "{{ item.key }}" | gpg --dearmor -o "{{ <GPG key path> }}"
chmod 644 "{{ <GPG key path> }}"
args:
creates: "{{ <GPG key path> }}"
loop: "< loop by list >"


Скачивает через curl -> gpg —dearmor -> получаем валидный формат.

Альтернативный и более ansible way вариант перейти на модуль ansible.builtin.deb822_repository. Проверен с Debian 11, 12, 13.
- name: "Add GPG key and repository"
ansible.builtin.deb822_repository:
name: <file_name>
types: deb
uris: "{{ <url> }}"
suites: apt
components: main
signed_by: "{{ url.key }}"
state: present
loop: "{{ <loop> | default ([]) }}"
when:
- ansible_distribution == 'Debian'


Этот модуль создаст source файл и скачает ключ для него.

#ansible
  • 🤯 1
Post #205 340
Бэкапирование terraform.tfstate в CI

На работе мы храним terraform в хранилище типа s3 (не amazon). В нашем хранилище нет версионности, поэтому бэкап и lifecycle реализуем сами через утилиту rclone.

Создание конфигурационного файла:
.terraform_rclone_generate_config:
script:
- |
cat > /tmp/rclone.conf <<EOF
[s3]
type = s3
provider = Other
endpoint = ${S3_HOST}
access_key_id = ${AWS_ACCESS_KEY_ID}
secret_access_key = ${AWS_SECRET_ACCESS_KEY}
EOF


Шаблон для бэкапа:
.terraform_backup_state:
script:
- !reference [".terraform_rclone_generate_config", "script"]
- |
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_PATH="backups/terraform.tfstate_${BACKUP_DATE}"
rclone --config /tmp/rclone.conf copyto \
s3:${S3_BUCKET_NAME}/terraform.tfstate \
s3:${S3_BUCKET_NAME}/${BACKUP_PATH} -P \
&& echo -e "\033[0;32mState backup created: s3://${S3_BUCKET_NAME}/${BACKUP_PATH}\e[0m" \
|| echo -e "\033[0;33mNo state file to backup\e[0m"


Бэкап вызываем каждый раз при terraform init:
.terraform_init:
<<: *terraform_common
script:
- terraform init
- !reference [".terraform_backup_state", "script"]
artifacts:
name: "terraform-init-${CI_JOB_ID}"
paths:
- "**/.terraform"
- "**/.terraform.lock.hcl"
expire_in: 1 hours


Якорь terraform_common тут не сильно важен.

Далее нужно реализовать очистку бэкапа от старых файлов, сохраняя последние 10 бэкапов.
.terraform_cleanup_old_tfstate_backup:
<<: *terraform_common
script:
- !reference [".terraform_rclone_generate_config", "script"]
- |
set +o pipefail
if [[ "$CI_PROJECT_NAME" == "elk" ]]; then
PROJECT_NAME=$ELK_CLUSTER_NAME
fi;
echo -e "\033[0;36m=== Starting backup cleanup for: ${PROJECT_NAME} ===\e[0m"
BACKUPS=$(rclone --config /tmp/rclone.conf lsf \
s3:${S3_BUCKET_NAME}/backups/ \
--files-only 2> /dev/null |grep -E "terraform\.tfstate_[0-9]{8}_[0-9]{6}$" | sort -r
)
if [[ -z "$BACKUPS" ]];then
echo -e "\033[0;33mNo backups found in s3://${S3_BUCKET_NAME}/backups/\e[0m"
exit 0
fi;
TOTAL=$(echo "$BACKUPS"| wc -l)
KEEP=10

echo -e "\033[0;36mTotal backups found: ${TOTAL}\e[0m"
echo -e "\033[0;36mKeeping last: ${KEEP}\e[0m"

if [[ "$TOTAL" -gt "$KEEP" ]];then
TO_DELETE_COUNT=$((TOTAL - KEEP))
TO_DELETE=$(echo "$BACKUPS" | tail -n "$TO_DELETE_COUNT")
echo -e "\033[0;33mDeleting ${TO_DELETE_COUNT} old backups:\e[0m"
DELETED=0
while IFS= read -r backup; do
if [[ -n "$backup" ]];then
if rclone --config /tmp/rclone.conf delete \
"s3:${S3_BUCKET_NAME}/backups/${backup}" 2>/dev/null; then
echo " ✓ Deleted: ${backup}"
DELETED=$((DELETED +1))
else
echo " ✗ Failed to delete: ${backup}"
fi;
fi;
done <<< "$TO_DELETE"
echo -e "\033[0;32m=== Cleanup completed: ${DELETED}/${TO_DELETE_COUNT} backups deleted ===\e[0m"
else
echo -e "\033[0;32mNo cleanup needed (total: ${TOTAL}, keeping: ${KEEP})\e[0m"
fi;


terraform под ELK чуть отличается, поэтому имеем небольшой костыль в начале. Финальная джоба в проекте по очистке будет запускаться через schedule pipeline примерно так:
terraform:schedule:cleanup:backups:
stage: clean
extends: [".terraform_cleanup_old_tfstate_backup"]
parallel:
matrix:
- ELK_CLUSTER_NAME:
- <cluster_1>
- <cluster_2>
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TYPE == "backup_cleanup"'


Более полную картину по реализации управления объектами ELK через terraform опишу в статье на сайте несколько позже.

#terraform
Post #203 289
JWT роль в vault и vault provider terraform

Обнаружил тут интересную вещь при настройки CI для terraform.
Волтовый провайдер

terraform {
required_version = ">= 1.6.0"

required_providers {
elasticstack = {
source = "elastic/elasticstack"
version = "~> 0.12"
}
vault = {
source = "hashicorp/vault"
version = "~> 4.0"
}
}
}

provider "vault" {
address = var.vault_address
}


Далее я получаю секреты для ELK кластера так:

data "vault_kv_secret_v2" "elastic_<cluster_name>" {
mount = "<mount>"
name = "<path/vault/env>"
}

provider "elasticstack" {
dynamic "elasticsearch" {
for_each = length(var.elasticsearch_endpoints) == 0 ? [] : [1]
content {
endpoints = var.elasticsearch_endpoints
username = data.vault_kv_secret_v2.elastic_<cluster_name>.data["username"]
password = data.vault_kv_secret_v2.elastic_<cluster_name>.data["password"]
}
}

dynamic "kibana" {
for_each = length(var.kibana_endpoints) == 0 ? [] : [1]
content {
endpoints = var.kibana_endpoints
username = data.vault_kv_secret_v2.elastic_<cluster_name>.data["username"]
password = data.vault_kv_secret_v2.elastic_<cluster_name>.data["password"]
}
}
}


Локально все хорошо. В сиай мы используем ограниченную JWT роль, к который привязана политика на чтения конкретных секретов.

В итоге получаю ошибку:
URL: POST https://<vault_addr>:8200/v1/auth/token/create

Code: 403. Errors:


Проблема в том, что по дефолту провайдер волта пытается создать себе дочерний токен, чтобы его потом использовать. В политике, привязанной к роли это запрещено, чтобы использовать токен, полученный именно ей, нужно добавить в провайдер опцию skip_child_token = true:

provider "vault" {
address = var.vault_address
skip_child_token = true
}


#terraform
Older posts →

About this channel

How can I read @andtree_sec without a Telegram account?
TGViewer shows the public web preview Telegram publishes for Сочный DevOps: recent posts, photos, videos and the subscriber count, with no app, login or account.
How many subscribers does Сочный DevOps have?
Сочный DevOps (@andtree_sec) has 420 subscribers on Telegram, refreshed roughly every 30 minutes.
Does Сочный DevOps know I viewed it here?
No. Public channel previews carry no viewer identity, and TGViewer has no accounts or tracking of what you look up.
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →