Сочный DevOps


Channel's geo and language: Russia, Russian
Category: Technologies


От CI/CD до SIEM, SOC и безопасности — здесь делюсь опытом, мыслями и полезняшками на стыке DevOps и безопасности. Всё сочное — в «Сочном DevOps».
https://devopsoffer.ru - подготовка к собеседованиям.
https://andtree.ru - статейки.

Related channels

Channel's geo and language
Russia, Russian
Statistics
Posts filter


Последнее время на работе часто приходится работать с apache flink и даже что-то писать на java. Это немного заставило меня вспомнить kotlin и разработку под android, которой я в свое время активно увлекался.

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

По стеку - паттер MVVM, room для локальной базы, hilt для DI. UI - jetpack compose.


Немножко про молекула тесты...

У нас принято использовать в переменных 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


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

В чем смысл?

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

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

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

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

#pet
#gitlab


Включаем профилирование в 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


Так выглядит общий dashbord работы с инцидентами.


Мой очередной 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


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

Теперь добавление нового значения в поле namespaces у dataview не приводит к пересозданию. Оно обновляется без этого через spaces API кибаны.


Написал небольшой exporter на go. C его помощью можно реализовать мониторинг истечения срока дейтсвия access token в проектах. Обычно они сплошь и рядом используются в CI, когда проектов много, забываешь что и где добавлял. Экспортер умеет собирать все токены и отдвать метрики в виде prometheus по gitlab группе или отдельным проектам.
Если будете использовать групповой токен, например чтобы собирать одним токеном сразу по всем проектам группы, то "сам себя" он видеть не будет, у группового токена другое API. Но мониторить все же можно, например задать статично переменную gitlab с 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:


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


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

#monitoring


Петля в DNS в k8s

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


Далее сростил ноду, на которой запущен под 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


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


Вот так можно посмотреть сработки kyverno политики по всем ns-ам. Бывает полезно, когда вводишь новую политику в режиме аудита и собираешь инфу. Тут вывод по статусу fail, т.е. там, где политика что-либо заблокировала бы.

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

#kyverno


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


Иногда так бывает, что в инфре нужно поддерживать дистрибутивы 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
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


Проблема 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


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


Debian 13 и ключи GPG

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

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 "` `"
chmod 644 "` `"
args:
creates: "` `"
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:
types: deb
uris: "` `"
suites: apt
components: main
signed_by: "` url`.`key `"
state: present
loop: "{{ | default ([]) }}"
when:
- ansible_distribution == 'Debian'

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

#ansible


Полезно ли будет открыть комментарии к постам?
Poll
  •   Да
  •   Нет
34 votes


Бэкапирование terraform.tfstate в CI

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

Создание конфигурационного файла:
.terraform_rclone_generate_config:
script:
- |
cat > /tmp/rclone.conf


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_" {
mount = ""
name = ""
}

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

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

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

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

Code: 403. Errors:

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

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

#terraform


Новая статья:
HTB. Assessment по File Upload Attacks

https://andtree.ru/?p=1448

20 last posts shown.