Я тут недавно пытался попасть на бесплатные курсы от Aston, но получил отказ с такой формулировкой.
Денис, добрый день!После изучения вашей анкеты, к сожалению, мы не готовы пригласить вас к дальнейшему рассмотрению на обучение в компании. У нас очень большой поток кандидатов на курсы. В данный момент взяли в рассмотрение участников с показателями выше. Ваша анкета будет сохранена, возможно, вернемся к вашей кандидатуре, при наборе на следующий поток.Благодарим за внимание к нашей компании.
Хотя я даже не понял, как смогли оценить мои показатели и в чем они измерялись, если не давали ни тестового задания, ни собеседования. В анкете у них несколько вопросов и уровень образования. Но да ладно, это тема отдельной статьи.
Сегодня хочу попробовать пройти тестовое задание от клауд.ру. Оно охватывает тот самый стек для devops, linux, docker, git, kubernetes.
На первый взгляд, задание кажется не таким уж и сложным, однако, дьявол кроется в деталях и есть неоднозначное право выбора, которое будет зависеть от ситуации.
Тестовое задание состоит из 4 задач, начнем с первой.
Web приложение на Python/Golang
ЦельНаписать простейший echo-server на Python или GoЗадачаТребуется написать простое веб-приложение на Python/Go, которое слушает входящие соединения на порту 8000 и предоставляет страницу с информацией: имя хоста ip адрес хоста имя автора, которое передаётся через переменную окружения $AUTHORДля приложения написать Dockerfile и запушить образ в докер хаб* (запушить в приватный регистри - пройти регистрацию в докер хаб, сделать регистри приватным).В ответе к задаче приложить: исходный код Dockerfile команды, которые использовались для пуша образа в регистриПолученный артефакты положить в папку /01-application
На первом пункте уже кроется подвох. Python или Go , здесь нужно знать контекст, потому что при грамотной сборке на го, образ получится около 2 мб, вместо около 20 на пайтоне. С другой стороны, на питоне докерфайл писать легче, потому как мультистэйдж сборка здесь не прокатит.
Для чистоты эксперимента, я сделаю 2 варианта, начну с го.
Создаем папку 01-application, в ней 2 файла main.go, go.mod.
// main.gopackage mainimport ("fmt""net""net/http""os")func envOr(key, fallback string) string {if v := os.Getenv(key); v != "" {return v}return fallback}// IP контейнера в его собственном network namespace.func getContainerIP() string {conn, err := net.Dial("udp", "8.8.8.8:80")if err != nil {return "unknown"}defer conn.Close()return conn.LocalAddr().(*net.UDPAddr).IP.String()}func handler(w http.ResponseWriter, r *http.Request) {containerHost, _ := os.Hostname() // имя из UTS namespace = ID контейнераcontainerIP := getContainerIP() // адрес из network namespace// Данные ХОСТА не добываются — они приходят снаружи.hostHost := envOr("HOST_HOSTNAME", "(not injected)")hostIP := envOr("HOST_IP", "(not injected)")author := envOr("AUTHOR", "anonymous")w.Header().Set("Content-Type", "text/html; charset=utf-8")fmt.Fprintf(w, `<!DOCTYPE html><html><head><title>Echo Server</title></head><body style="font-family:monospace"> <h1>Echo Server</h1> <ul> <li><b>Container hostname:</b> %s</li> <li><b>Container IP:</b> %s</li> <li><b>Host hostname:</b> %s</li> <li><b>Host IP:</b> %s</li> <li><b>Author:</b> %s</li> </ul></body></html>`, containerHost, containerIP, hostHost, hostIP, author)}func healthz(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "ok")}func main() {http.HandleFunc("/", handler)http.HandleFunc("/healthz", healthz)http.ListenAndServe(":8000", nil)}
// go.modmodule echo-servergo 1.23
Далее напишем докерфайл, используя лучшие практики, мультистэйдж сборки, выкинем все лишнее, чтобы образ весил минимальное количество мегабайт, а также создадим пользователя, чтобы контейнер не был запущен от рута.
# ---- Этап 1: сборка ----FROM golang:1.23-alpine AS builderWORKDIR /srcCOPY go.mod ./COPY main.go ./# CGO_ENABLED=0 — статическая линковка, чтобы бинарник# жил в scratch без единой системной библиотеки.# -ldflags="-s -w" срезает отладочные символы (меньше вес).RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /echo-server .RUN echo 'appuser:x:10001:10001::/:' > /etc/passwd# ---- Этап 2: финальный образ ----FROM scratchCOPY --from=builder /echo-server /echo-serverCOPY --from=builder /etc/passwd /etc/passwdUSER appuserEXPOSE 8000ENV AUTHOR="alfabuster"ENTRYPOINT ["/echo-server"]
Далее зарегаемся на докерхабе, если еще нет аккаунта, создаем репозиторий, делаем его приватным и создаем токен доступа.
# 1. Логинимся (введёт логин и Access Token — не пароль, пароли уже немодно)docker login -u <ваш_логин># 2. Собираем с правильным тегом: <логин>/<репозиторий>:<версия>docker build -t <ваш_логин>/echo-server:1.0 .# 3. Проверяем локально, что оно вообще дышитdocker run --rm -p 8001:8000 \ -e AUTHOR="alfabuster" \ -e HOST_HOSTNAME="$(hostname)" \ -e HOST_IP="$(ip -4 route get 1.1.1.1 | grep -oP 'src \K\S+')" \ <ваш_логин>/echo-server:1.0# открываем http://localhost:8001 — должны увидеть hostname/IP/author
Если все сделали правильно, то увидим инфу о контейнере и хосте.

# 4. Пушим в приватный регистриdocker push <ваш_логин>/echo-server:1.0
Теперь проделаем, все тоже самое, но для пайтона.
В папке созаем server.py с таким кодом.
#!/usr/bin/env python3import osimport socketfrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServerdef env_or(key, fallback): return os.getenv(key) or fallbackdef get_container_ip(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect(("8.8.8.8", 80)) return s.getsockname()[0] except Exception: return "unknown" finally: s.close()class Handler(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/healthz": self.send_response(200) self.send_header("Content-Type", "text/plain") self.end_headers() self.wfile.write(b"ok\n") return container_host = socket.gethostname() # ID контейнера container_ip = get_container_ip() host_host = env_or("HOST_HOSTNAME", "(not injected)") host_ip = env_or("HOST_IP", "(not injected)") author = env_or("AUTHOR", "anonymous") body = f"""<!DOCTYPE html><html><head><title>Echo Server</title></head><body style="font-family:monospace"> <h1>Echo Server (Python)</h1> <ul> <li><b>Container hostname:</b> {container_host}</li> <li><b>Container IP:</b> {container_ip}</li> <li><b>Host hostname:</b> {host_host}</li> <li><b>Host IP:</b> {host_ip}</li> <li><b>Author:</b> {author}</li> </ul></body></html>""" self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() self.wfile.write(body.encode("utf-8")) def log_message(self, *args): passif __name__ == "__main__": ThreadingHTTPServer(("0.0.0.0", 8003), Handler).serve_forever()
Напишем простой докерфайл. А теперь ключевой момент, ради которого всё затевалось. Помните элегантный финал Go-версии — FROM scratch, образ из чистой пустоты весом в мегабайты? С Python так не выйдет никогда. Python — язык интерпретируемый: чтобы запустить server.py, в образе обязан жить сам интерпретатор со всей его свитой. В пустоту его не положишь, multi-stage тут бесполезен — выкидывать нечего, рантайм нужен целиком.
FROM python:3.12-alpineWORKDIR /appRUN adduser -D appuser COPY --chown=appuser:appuser app.py .COPY server.py .EXPOSE 8003ENV AUTHOR="alfabuster"ENTRYPOINT [ "python3" , "server.py"]
Однако писать такой файл проще, всего 8 строк, которые по гайду из докера, напишет даже человек без профильного образования в IT.
Теперь сравним размер полученных из билда образов.
Как видно разница примерно в 10 раз. Здесь однозначно выигрывает go.
А что на счет безопасности? Образ на scratch — это крепость без дверей: там нет sh, нет apt, нет curl, нет вообще ничего, кроме бинарника. Злоумышленник, пробравшийся в такой контейнер, оказывается в пустой бетонной коробке — ни закрепиться, ни осмотреться, ни подтянуть инструменты. Однако scratch не является защитой от RCE, при определенных условиях можно использовать загруженные/встроенные бинарники или эксплуатировать уязвимости ядра/процесса.
В python:3.12-alpine sh присутсвует. Также содержит сам Python, поэтому при RCE атакующий может использовать Python как инструмент, даже если каких-то Unix-команд нет.
Справедливости ради — у Python есть и козыри. Dockerfile проще (никакой стадии сборки), правки на лету без перекомпиляции, а внутрь можно зайти и потыкать палкой, когда что-то пошло не так. Go-scratch за свою неприступность расплачивается тем, что отлаживать его — как чинить запечатанные часы: удобной дверцы нет. Это классический размен безопасность против удобства.
Поскольку я учусь на безопасника, для меня выбор очевиден, однако в рамках тестового задания, не знаю, какой вариант правильный, возможно они оба верные. Просто для прода, нужно знать контекст и действовать по ситуации.
Тем временем первая задача выполнена, переходим ко второй.
Ansible playbook или роль
ЦельАвтоматизация конфигурации на AnsibleЗадачаТребуется поднять Виртуальную Машину (VM) с ОС Ubuntu 22.04. Возможные сценарии: локально через Virtual Box (или любое другое средство виртуализации), либо воспользовавшись Free-tier предложением от cloud.ru - https://cloud.ru/offers/free-tier.Выполнить следующие задачи, используя Ansible: На VM установить пакет docker-ce; Запустить на docker контейнеры c подготовленным Web-приложением из первой задачи в 3-х экземплярах. Для развёртывания использовать приватный регистри из первой задачи; Настроить балансировщик на nginx, который будет отвечать при обращении к хосту и прокидывать запрос на запущенные контейнеры. Выбрать алгоритм балансировки для приложения и обосновать свой выбор.В ответе к задаче приложить: исходный код Ansible текстовое обоснование выбранного алгоритма балансировкиПолученные артефакты положить в папку /02-ansible
Ок, приступим. Поскольку хостовая система у меня Void Linux, вручную качать и устанавливать ubuntu, я не буду, воспользуюсь утилитой vagrant.
Для установки, установлю пакетный менеджер Brew.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Он рассчитан на bash, но у меня оболочка fish, поэтому, то что советует установщик в выводе в моем случае не сработает.
Нужно добавить Homebrew в PATH fish
fish_add_path /home/linuxbrew/.linuxbrew/binfish_add_path /home/linuxbrew/.linuxbrew/sbin
После этого можно устанавливать сам Vagrant.
brew tap hashicorp/tapbrew install hashicorp/tap/vagrant
На хосте создаю любую папку, в ней создаю файл Vagrantfile.
Vagrant.configure("2") do |config| config.vm.box = "ubuntu/jammy64" config.vm.network "private_network", ip: "10.0.0.7", virtualbox__intnet: "intnet" config.vm.provider "virtualbox" do |vb| vb.memory = 2048 vb.cpus = 2 endend
Этим файлом я создал машину во внтуренней сети, которая у меня уже была.
Пингуем сервер, чтобы убедиться, что вагрант все верно настроил, доступ по ssh, настраиваем через ключи.
Теперь можно создавать плейбук через Ansible.
В папке 02-ansible создаем пару файлов ansible.cfg и inventory.ini.
[defaults]inventory = inventory.iniremote_user = vagrantprivate_key_file = ~/.ssh/id_ed25519
[web]web-vm ansible_host=10.0.0.7[web:vars]ansible_python_interpreter=/usr/bin/python3
В этих файлах указываем, нашу созданную убунту через вагрант, ее ip ssh ключ и переменные.
Для данной задачи хватит и плейбука, можно конечно городить и роль, но для данной задачи хватит и плейбука.
Нам нужно, установить docker, залогиниться в приватный докер репозиторий, запустить 3 контейнера и накрыть балансировщиком на nginx.
Возможно руками, залезь на 1 сервак, там установить докер и запустить 3 контейнера будет быстрее. Но задание требует именно ansible, поэтому файл будет достаточно большим.
Начало плейбука
---- name: Deploy echo-server behind an nginx load balancer hosts: web become: true # выполнять с sudo — иначе apt ругнется на permission vars: docker_image: "<ваш_логин>/echo-server:1.0" author_name: "alfabuster" container_count: 3 base_port: 8000 # контейнеры сядут на 8001, 8002, 8003 registry_user: "<ваш_логин>" registry_password: "{{ vault_registry_token }}" # прячем в vault, создадим далее
Далее установим сам докер
tasks: - name: Install prerequisites apt: name: [ca-certificates, curl, gnupg, python3-pip] state: present update_cache: true - name: Create apt keyrings directory file: path: /etc/apt/keyrings state: directory mode: '0755' - name: Download Docker's GPG key get_url: url: https://download.docker.com/linux/ubuntu/gpg dest: /etc/apt/keyrings/docker.asc mode: '0644' - name: Add Docker apt repository apt_repository: repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" filename: docker state: present - name: Install docker-ce apt: name: [docker-ce, docker-ce-cli, containerd.io] state: present update_cache: true - name: Install Docker SDK for Python pip: name: docker state: present - name: Ensure docker is running and enabled service: name: docker state: started enabled: true
Обратите внимание на ansible_distribution_release — это факт, который Ansible сам собирает о хосте перед стартом (для 22.04 подставит jammy).
Модуль docker_container работает не через CLI, а через Python-библиотеку docker. Поэтому мы ставим pip install docker — без неё модуль выдаст загадочную ошибку.
Приватный реджистри и запуск трех контейнеров
- name: Log in to the private registry community.docker.docker_login: username: "{{ registry_user }}" password: "{{ registry_password }}"- name: Pull the application image community.docker.docker_image: name: "{{ docker_image }}" source: pull- name: Run echo-server containers community.docker.docker_container: name: "echo-{{ item }}" image: "{{ docker_image }}" state: started restart_policy: unless-stopped published_ports: - "127.0.0.1:{{ item }}:8000" env: AUTHOR: "{{ author_name }}" loop: - 8001 - 8002 - 8003
published_ports привязан к 127.0.0.1, а не к 0.0.0.0. Контейнеры слушают только локально — снаружи к ним не достучаться, единственная дверь наружу — через nginx.
Nginx и его конфиг через шаблон.
- name: Install nginx apt: name: nginx state: present - name: Deploy the load-balancer config template: src: nginx-lb.conf.j2 dest: /etc/nginx/sites-available/echo-lb.conf notify: Reload nginx - name: Enable the site file: src: /etc/nginx/sites-available/echo-lb.conf dest: /etc/nginx/sites-enabled/echo-lb.conf state: link notify: Reload nginx - name: Remove the default nginx site file: path: /etc/nginx/sites-enabled/default state: absent notify: Reload nginx - name: Ensure nginx is running and enabled service: name: nginx state: started enabled: true handlers: - name: Reload nginx service: name: nginx state: reloaded
handlers — механизм, который отличает Ansible от тупого шелл-скрипта. notify: Reload nginx не перезагружает nginx немедленно. Он ставит галочку «надо будет перечитать конфиг» и делает это один раз в конце, и только если конфиг реально изменился.
Шаблон templates/nginx-lb.conf.j2
upstream echo_backend { server 127.0.0.1:8001; server 127.0.0.1:8002; server 127.0.0.1:8003;}server { listen 80 default_server; server_name _; location / { proxy_pass http://echo_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}
Обоснование алгоритма
А теперь ради чего всё затевалось. У nginx есть несколько алгоритмов балансировки, и вот полный расклад, кто есть кто:
-
round-robin (по умолчанию) — раздаёт запросы по кругу: первый на 8001, второй на 8002, третий на 8003, четвёртый снова на 8001.
-
least_conn — отдаёт тому бэкенду, у кого меньше активных соединений.
-
ip_hash — привязывает клиента к бэкенду по хешу его IP (липкие сессии).
-
hash / random — экзотика для особых случаев.
Поскольку у нас простое stateless приложение, нет ни куков, ни личного кабинета, ни сессий. Все 3 контейнера равнозначны и идентичны. Поэтому равномерное распределение по кругу будет оптимальным вариантом в данной ситуации.
Least_conn здесь не станет преимуществом, потому как нагрзука в условии не указана. ip_hash в данной топологии противопоказан, так как весь трафик приходит с localhost и был бы прикован к одному контейнеру.
Поэтому выбираем round-robin.
Создаем vault и запускаем плейбук
Создание vault и пароля к нему.
ansible-vault create group_vars/web/vault.yml
Внутри одна строка с токеном из докера
vault_registry_token: "docker_token"
Далее можем запускать наш плейбук.
ansible-playbook playbook.yaml --ask-vault-pass
Тут не все прошло гладко, вылезла ошибка.
fatal: [web-vm]: FAILED! => {"changed": false, "msg": "Error connecting: Error while fetching server API version: Not supported URL scheme http+docker"}
Это произошло потому что, я давно не обновлял коллекции из ansible. Чиним.
ansible-galaxy collection install community.docker --upgrade
Запускаем плейбук еще раз и все заработало.
Проверяем сервак.
Балансировщик работает, второе задание выполнено.
Kubernetes manifest
ЦельДеплой приложения в KubernetesЗадачаТребуется написать манифест для запуска приложения из первой задачи в Kubernetes в отдельном неймспейсе в виде Deployment с 3 репликами и сервиса с типом ClusterIP.Выполнить следующие задачи: Для приложения организовать проброс значения переменной AUTHOR; Реализовать readiness- и liveness- пробы; Использовать образ из приватного регистри из задачи 1; опционально: поднять ingress-controller и создать правило ingress; опционально: использовать helm-chart, а не raw манифест.В ответе к задаче приложить: исходный код всех манифестовПолученные артефакты положить в папку /03-kubernetes
В задании не сказано, что нужно установливать кубер на машину. Однако для наглядности нужно понимать и тестировать написанные манифесты.
У меня на убунту для тестов как раз стоит кубер и 2 воркер ноды версии v1.31.14. Поэтому я применю написанные манифесты.
Недавно проходил курс Certified Kubernetes Administrator от kodekloud, там подсмотрел полезную тему для генерации манифестов.
Во первых, пропишем алиас, чтобы каждый раз не вводить kubectl.
alias k=kubectl
Во вторых, почти любой объект kubectl create умеет напечатать готовый манифест вместо создания.
--dry-run=client -o yaml
Дальше можно воспользоваться перенаправлением в файл и у нас будет готовый шаблон манифеста, с правильными отступами по канонам yaml.
Создаем namespace
k create ns cloud --dry-run=client -o yaml
В выводе будет лежать готовый манифест.
apiVersion: v1kind: Namespacemetadata: creationTimestamp: null name: cloudspec: {}status: {}
Конечно врядли, кто пишет манифест для создания namespace, его проще создать командой k create ns name. Однако в задании нужен именно манифест.
Секрет
Задача усложняется тем, что реджистри приватный. Для него необходимо создавать секрет. Иначе получим вечный ImagePullBackOff.
Создаётся секрет императивно, чтобы не хранить токен в git.
kubectl create secret docker-registry regcred \ --docker-server=https://index.docker.io/v1/ \ --docker-username=<ваш_логин> \ --docker-password=<ваш_access_token> \ --namespace=cloud
Секрет живёт внутри неймспейса cloud — в другом неймспейсе он невидим.
Configmap
Можно было впихнуть AUTHOR прямо в манифест как value: “alfabuster”. Но задание просит «организовать проброс», а по-взрослому переменные конфигурации живут в ConfigMap — отдельно от кода деплоя:
apiVersion: v1kind: ConfigMapmetadata: creationTimestamp: null name: cloud-configmap namespace: clouddata: AUTHOR: alfabuster
Deployment
Собственно дальше идет деплоймент с 3 репликами и пробами. Генерируем шаблон, а дальше правим руками.
k create deploy cloud-deploy --image=alfabuster/echo-server:1.0 --replicas=3 --port=8000 -n cloud --dry-run=client -o yaml
Итоговый манифест получился такой.
apiVersion: apps/v1kind: Deploymentmetadata: creationTimestamp: null labels: app: cloud-deploy name: cloud-deploy namespace: cloudspec: replicas: 3 selector: matchLabels: app: cloud-deploy strategy: {} template: metadata: creationTimestamp: null labels: app: cloud-deploy spec: imagePullSecrets: - name: regcred containers: - image: alfabuster/echo-server:1.0 name: echo-server ports: - containerPort: 8000 env: - name: AUTHOR valueFrom: configMapKeyRef: name: cloud-configmap key: AUTHOR livenessProbe: httpGet: path: /healthz port: 8000 initialDelaySeconds: 10 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: httpGet: path: /healthz port: 8000 periodSeconds: 5
-
livenessProbe отвечает на вопрос «пациент вообще жив?». Если проба падает — Kubernetes перезапускает контейнер. Это лекарство от зависших процессов и дедлоков.
-
readinessProbe отвечает на вопрос «пациент готов принимать посетителей?». Если проба падает — под не перезапускается, его просто исключают из эндпоинтов Service, то есть перестают слать трафик, пока он не оклемается.
Service
Service в Kubernetes — это тоже балансировщик. ClusterIP раскидывает входящие соединения по всем готовым подам.
apiVersion: v1kind: Servicemetadata: name: cloud-deploy namespace: cloudspec: type: ClusterIP selector: app: cloud-deploy ports: - port: 80 targetPort: 8000 protocol: TCP
Ingress
Создаем уже привычным способом, через генератор.
k create ingress cloud-ingress --rule="cloud.local/*=cloud-deploy:80" -n cloud --dry-run=client -o yaml
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: creationTimestamp: null name: cloud-ingress namespace: cloudspec: rules: - host: cloud.local http: paths: - backend: service: name: cloud-deploy port: number: 80 path: / pathType: Prefixstatus: loadBalancer: {}
Домен cloud.local надо прописать в /etc/hosts, указав на IP кластера (minikube ip или адрес ноды). Без этой записи браузер будет искать cloud.local в интернете.
По итогу должно получиться, тоже самое приложение, но уже в кубере на своем домене, с 3 репликами и пробами.
Запереть контейнер изнутри*
ЦельПроверить навыки администрирования *nixЗадача Создать ВМ с ОС Ubuntu 22.04; Установить пакет docker-ce; Выполнить команду docker run -it --rm ubuntu -- bash; Не покидая контейнер, не меняя флаги запуска и настройки демона, выполнить задачи: 4.1. сделать apt-get update, показать, что команда выполняется успешно; 4.2. изнутри контейнера заблокировать ему выход в сеть интернет, команды блокировки с описанием приложить к задаче; 4.3. показать, что apt-get update больше не работает. Доп. задача - найти ещё +1 способ выполнить подобную блокировку изнутри контейнера.В ответе к задаче приложить: текстовое описание применённых конфигурацийПолученные артефакты положить в папку /04-unix
На первый взгляд задача несложная, однако не зря она со звездочкой. Для начала запустим контейнер с убунтой и проверим связь.
Вот в чём фокус: Docker по умолчанию запускает контейнеры с урезанным набором Linux capabilities, выбрасывая большинство привилегий, даже когда внутри вы root.
Root внутри контейнера не равняется рут на хосте.
Из очевидного в голову приходит установить iptables и запретить исходящий трафик. Установить можем, однако для правки таблицей маршрутизации рута недостаточно.
root@d79ba688c2c9:/# iptables -A OUTPUT -j DROPiptables v1.8.11 (nf_tables): Could not fetch rule set generation id: Permission denied (you must be root)
Контейнер работает под root, но демон docker по умолчанию сбрасывает все capabilities, кроме дефолтного набора. А в таком списке отсутсвует привелегия NET_ADMIN, без которой вы не можете ни трогать iptables, ни гасить интерфейсы, ни править таблицу маршрутизации.
Флаги запуска и настройки демона трогать нельзя, иначе можно было бы просто в запуске добавить нужную капу и задача решилась очень быстро.
Нам нужно заблокировать интернет именно для утилиты apt.
Способ №1 — nsswitch.conf
Начну не с самого очевидного варианта файла /etc/nsswitch.conf, я о нем сам недавно узнал, принимая участие в одном CTF.
NSS расшифровывается как Name Service Switch. Это механизм, встроенный в стандартную библиотеку C (glibc). Когда любая программа пытается узнать IP-адрес по доменному имени или проверить, существует ли в системе определенный пользователь, она обращается к glibc. А glibc первым делом читает /etc/nsswitch.conf, чтобы понять, где именно искать ответ.
В структуре этого файла есть строка hosts: files dns
Убрав dns из строки hosts:, вы вообще запрещаете системе обращаться к DNS — резолвиться будет только то, что явно прописано в /etc/hosts.
Кстати NSS управляет не только хостами. В файле прописаны правила для десятка системных баз данных.
Но в данном таске нас интересует dns, поэтому редактируем файл и получаем неработающий apt.
Способ №2 — resolv.conf
Второй способ, аналогичен первому, только на этот раз править будем файл /etc/resolv.conf.
Если /etc/nsswitch.conf решает, нужно ли вообще делать DNS-запрос, то /etc/resolv.conf указывает системе, к кому именно с ним обращаться. Это главный конфигурационный файл DNS-резолвера. Он часто генерируется networkmanager’ом, поэтому при следующем подключении вероятнее всего он затрется, но для демонстрации в контейнере, хватит обычной правки через vim или nano.
> /etc/resolv.conf
Снова получаем неработающий интернет.
Способ 3 — /etc/hosts
/etc/hosts — это простейший локальный справочник, который жестко связывает IP-адреса с доменными именами.
В него можно прописать локальный ip адрес серверов убунту и обновление не пройдет.
127.0.0.1 archive.ubuntu.com127.0.0.1 security.ubuntu.com127.0.0.1 ports.ubuntu.com
Способ 4 — apt proxy
У APT есть собственная настройка HTTP/HTTPS-прокси. Можно попробовать для всех HTTP-запросов использовать прокси 127.0.0.1:9.
Порт 9 используется для протокола Discard (RFC 863), который является сетевым эквивалентом черной дыры (/dev/null). Любые данные, отправленные на этот порт по TCP или UDP, просто уничтожаются сервером без какого-либо ответа или подтверждения.
cat > /etc/apt/apt.conf.d/99-block-network <<'EOF'Acquire::http::Proxy "http://127.0.0.1:9";Acquire::https::Proxy "http://127.0.0.1:9";EOF
Как итог
Получилось неплохое задание, мне понравилось его выполнять. Много чего в памяти освежил, некоторые вещи изучил. Задание не сказать, чтобы сильно простое, но и не сложное. Некоторые задания потребуют необычного подхода и глубокое знание linux.
Выполнил и выложил на гитхаб alfabuster.
ссылка на оригинал статьи https://habr.com/ru/articles/1082220/