Всем привет! Сегодня хочу поделиться нашими мыслями относительно того, как можно защитить свою разработку от некоторых потенциальных рисков в современных условиях. Собственно, что мы имеем ввиду? Речь идёт о том, что в крупных проектах часто есть единые точки отказа в процессах CI/CD, это может быть как простой репозиторий кодом, так и различные конвеерные системы сборки кода и доставки его в рабочие окружения. Если мы говорим про системный софт, то его можно просто перестать обновлять, запретить ему ходить "наружу", но в случае с внешними репозиториями нас могут ожидать неприятные сюрпризы.

От чего страхуемся

На повестке дня у нас стоят следующие потенциальные риски:

  • есть случаи внесения в публичных репозитория пакетов "вредоносного" (в различных смыслах) кода, например, уже, было замечено в npm, но, могут быть еще прецеденты, никто не застрахован, даже, если фиксировать версии пакетов, никто не гарантирует, что не изменится их содержимое на публичных серверах;

  • может быть нарушена связность с "внешним миром" по тем или иным причинам.

В чём крутим

Крутим мы все нижеописанные решения в докере, при помощи docker-compose, немного про установку на debian/ubuntu

ну, конечно, ставим docker:

apt-get install docker.io

далее ставим docker-compose:

apt-get install python3-pip
pip3 install docker-compose

для демонизации используем systemd-юнит:

/etc/systemd/system/docker-compose.service

[Unit] 
Description=Docker-compose

[Service] 
WorkingDirectory=/etc 
Type=simple 
ExecStart=/usr/local/bin/docker-compose up 
ExecStop=/usr/local/bin/docker-compose down 
Restart=always 
RestartSec=5s

[Install] 
WantedBy=multi-user.target

Добавляем его в автозагрузку:

systemctl enable docker-compose.service

а также создаём рабочий каталог, который мы будем монтировать в будущие контейнеры для персистетности данных:

mkdir /var/data

допустим условность - IP-адрес машинки, где крутится докер-контейнер, пускай будет:

10.0.0.1

Общий смысл

Общий смысл всех этих решений будет заключаться в том, чтобы наши сборщики ходили в Интернет через прокси-сервер, который будет кэшировать пакеты/модули, таким образом, при последующих обращениях к прокси-серверу, пакеты/модули будут отдаваться из кэша, таким образом, мы можем зафиксировать версии, а также, в случае недоступности внешних каналов, мы сможем продолжать разработку какое-то время автономно.

NodeJS / NPM

Здесь мы использовали систему Verdaccio. Мы используем тег 5.6.0 осознанно, вы можете использовать тег более свежий по своему желанию.

/etc/docker-compose.yaml

version: "3.7"

services:
  verdaccio:
    image: verdaccio/verdaccio:5.6.0
    ports:
      - 4873:4873
    volumes:
      - /var/data:/verdaccio/storage

Запускаем демона:

systemctl start docker-compose.service

в логах должно появиться следующее:

# docker logs etc_verdaccio_1 -n 100
 warn --- config file  - /verdaccio/conf/config.yaml
 warn --- Plugin successfully loaded: verdaccio-htpasswd
 warn --- Plugin successfully loaded: verdaccio-audit
 warn --- http address - http://0.0.0.0:4873/ - verdaccio/5.6.0

после этого "фронтендерам" нужно:

создать файл .npmrc в котором указать registry=http://10.0.0.1:4783

Более подробно тут.

Также есть хабр-статья от Яндекса - вот она.

Вот еще статья на хабре.

И еще.

Python

Для всеми любимого python будем использовать devpi.

Здесь придётся немного "покрутиться". Дело в том, что процесс разбит на два этапа:

  • инициализация;

  • кэширование.

Создадим Dockerfile для сборки контейнера (можно смело копировать и выполнять):

mkdir /root/docker-devpi
cd /root/docker-devpi

cat > Dockerfile <<EOD
FROM python:3.8
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN pip install devpi-server devpi-web devpi-client && devpi-init && chmod +x /docker-entrypoint.sh
COPY pip.conf /etc/pip.conf
ENTRYPOINT ["/docker-entrypoint.sh"]
EOD

cat > docker-entrypoint.sh <<EOD
#!/bin/sh
export PIP_CONFIG_FILE=/etc/pip.conf # задание конфигурации для pip
echo "[RUN]: Launching devpi-server"
exec devpi-server --restrict-modify root --host 0.0.0.0 --port 3141
echo "[RUN]: Builtin command not provided [devpi]"
echo "[RUN]: $@"
exec "$@"
EOD

cat > pip.conf <<EOD
[global]
index-url = http://localhost:3141/root/pypi/+simple/
[search]
index = http://localhost:3141/root/pypi/
EOD

после создания Dockerfile, нам нужно его собрать:

cd /root/docker-devpi
docker build -t devpi:latest .

далее создаём временный /etc/docker-compose.yaml:

version: "3.7"

services:
  devpi:
    image: devpi:latest
    volumes:
      - /var/data:/root/.devpi-tmp      

запускаем:

docker-compose up -d

смотрим логи, чтобы убедиться, что всё стартануло:

docker logs etc_devpi_1 |head

# docker logs etc_devpi_1 |head
[RUN]: Launching devpi-server
2022-03-25 09:04:16,913 INFO  NOCTX Loading node info from /root/.devpi/server/.nodeinfo
2022-03-25 09:04:16,914 INFO  NOCTX wrote nodeinfo to: /root/.devpi/server/.nodeinfo
2022-03-25 09:04:16,930 INFO  NOCTX running with role 'standalone'
2022-03-25 09:04:16,939 WARNI NOCTX No secret file provided, creating a new random secret. Login tokens issued before are invalid. Use --secretfile option to provide a persistent secret. You can create a proper secret with the devpi-gen-secret command.
2022-03-25 09:04:18,583 INFO  NOCTX Found plugin devpi-web-4.0.8.
2022-03-25 09:04:18,746 INFO  NOCTX Using /root/.devpi/server/.indices for Whoosh index files.
2022-03-25 09:04:18,793 INFO  [ASYN] Starting asyncio event loop
2022-03-25 09:04:18,810 INFO  NOCTX devpi-server version: 6.5.0
2022-03-25 09:04:18,810 INFO  NOCTX serverdir: /root/.devpi/server

идём в контейнер чтобы скопировать initial-данные в персистентную папку:

docker exec -ti etc_devpi_1 bash

apt update
apt install rsync
rsync -av /root/.devpi/ /root/.devpi-tmp/

теперь можно погасить временный контейнер:

docker-compose down

поправить /etc/docker-compose.yaml:

version: "3.7"

services:
  devpi:
    image: devpi:latest
    ports:
      - 3141:3141
    volumes:
      - /var/data:/root/.devpi

теперь стартуем уже как демон:

systemctl start docker-compose.service

При старте сервера начнется индексация всех существующих пакетов на pypi.org. Процесс занимает 1.5 часа и идет в фоне.

Настройка для разработчиков, нужно создать файл /etc/pip.conf:

[global]
index-url = http://10.0.0.1:3141/root/pypi/+simple/
[search]
index = http://10.0.0.1:3141/root/pypi/

Теперь утилита pip будет ходить на кэширующий сервер, тот в свою очередь будет отдавать либо закешированные данные, либо будет ходить в Интернет и кэшировать новые данные.

Golang

Для кеширования пакетов Golang мы использовали решение Athens.

Создаём /etc/docker-compose.yaml:

version: "3.7"

services:
  athens:
    image: gomods/athens
    ports:
      - 3000:3000
    environment:
      - ATHENS_DISK_STORAGE_ROOT=/var/data
      - ATHENS_STORAGE_TYPE=disk
      - ATHENS_GO_BINARY_ENV_VARS=GOPROXY=proxy.golang.org,direct
    volumes:
      - /var/data:/var/data

запускаем демон:

systemctl start docker-compose.service

смотрим логи:

docker logs etc_athens_1

INFO[7:30AM]: Exporter not specified. Traces won't be exported
2022-03-29 07:30:19.231447 I | Starting application at port :3000

теперь протестируем работоспособность:

export GOPROXY=10.0.0.1
go get github.com/spf13/cobra

в логах увидим наше обращение к прокси-серверу:

INFO[7:35AM]: exit status 1: go list -m: github.com/spf13@latest: invalid github.com/ import path "github.com/spf13"
        http-method=GET http-path=/github.com/spf13/@v/list kind=Not Found module= operation=download.ListHandler ops=[download.ListHandler pool.List protocol.List vcsLister.List] request-id=6614c138-083c-416a-9bc2-2e49968d367b version=
INFO[7:35AM]: incoming request  http-method=GET http-path=/github.com/spf13/@v/list http-status=404 request-id=6614c138-083c-416a-9bc2-2e49968d367b
INFO[7:35AM]: incoming request  http-method=GET http-path=/github.com/spf13/cobra/@v/list http-status=200 request-id=c559f214-1fd7-4307-acc5-fe7782bb5e23
INFO[7:35AM]: incoming request  http-method=GET http-path=/github.com/spf13/cobra/@v/v1.4.0.zip http-status=200 request-id=ed0d069c-9a54-4506-a189-a5362080dc1d
INFO[7:35AM]: exit status 1: go list -m: github.com@latest: unrecognized import path "github.com": parse https://github.com/?go-get=1: no go-import meta tags ()
        http-method=GET http-path=/github.com/@v/list kind=Not Found module= operation=download.ListHandler ops=[download.ListHandler pool.List protocol.List vcsLister.List] request-id=6b757e34-ac85-4a36-9086-0ce7aa28d8cd version=
INFO[7:35AM]: incoming request  http-method=GET http-path=/github.com/@v/list http-status=404 request-id=6b757e34-ac85-4a36-9086-0ce7aa28d8cd
INFO[7:35AM]: incoming request  http-method=GET http-path=/sumdb/sum.golang.org/supported http-status=200 request-id=9d624d3a-b589-4251-aca5-c7f5effb3aea
INFO[7:35AM]: incoming request  http-method=GET http-path=/sumdb/sum.golang.org/lookup/github.com/cpuguy83/go-md2man/v2@v2.0.1 http-status=200 request-id=02957958-e5d4-43eb-ace7-8d60ee42fc8f
...
...

Таким образом, используя GOPROXY=10.0.0.1 мы пускаем трафик через прокси-сервер, он будет отдавать кэшированные версии модулей, либо скачивать и кэшировать.

PHP

В этом случае мы используем решение RepMan, требует чуть больше внимания и ресурсов, т.к. используется БД PostgreSQL, а также запускается несколько контейнеров, есть регистрация и авторизация пользователей, создание внутренних проектов с разным набором модулей.

Для начала клонируем репозиторий в каталог /var/data:

git clone https://github.com/repman-io/repman.git /var/data

Для данного решения пришлось немного видоизменить systemd-юнит (сменить рабочий каталог и задать переменную окружения PWD):

[Unit] 
Description=Docker-compose

[Service] 
WorkingDirectory=/var/data 
Environment=PWD=/var/data 
Type=simple 
ExecStart=/usr/local/bin/docker-compose up 
ExecStop=/usr/local/bin/docker-compose down 
Restart=always 
RestartSec=5s

[Install] 
WantedBy=multi-user.target

Для комфортной работы здесь придётся завести dns-имя для веб-приложения repman, допустим это будет repman.example.com.

Если вы используете bind9, то нужно прописать в DNS имена:

$ORIGIN example.com.
repman IN A 10.0.0.1
*.repman CNAME repman

правим файл /var/data/.env.docker:

APP_HOST=repman.example.com

если есть GitLab CE, тогда правим еще опцию:

APP_GITLAB_API_URL=https://git.example.com

Для отладки можно поправить опцию APP_DEBUG=1.

Для отправки почты тоже правим настройки, допустим на 10.0.0.10 у нас настроен какой-то MTA (Exim4, Postfix, не важно):

MAILER_DSN=smtp://10.0.0.10:25?verify_peer=false
MAILER_SENDER=repman@example.com

Мы готовы к запуску, стартуем демон:

systemctl start docker-compose.service
Hidden text

Mar 29 07:51:30 localhost systemd[1]: Started Docker-compose.
Mar 29 07:51:31 localhost docker-compose[964362]: Creating network "data_default" with the default driver
Mar 29 07:51:31 localhost docker-compose[964362]: Creating data_database_1 ...
Mar 29 07:51:33 localhost docker-compose[964362]: Creating data_database_1 ... done
Mar 29 07:51:33 localhost docker-compose[964362]: Creating data_app_1 ...
Mar 29 07:51:35 localhost docker-compose[964362]: Creating data_app_1 ... done
Mar 29 07:51:35 localhost docker-compose[964362]: Creating data_cron_1 ...
Mar 29 07:51:35 localhost docker-compose[964362]: Creating data_nginx_1 ...
Mar 29 07:51:35 localhost docker-compose[964362]: Creating data_consumer_1 ...
Mar 29 07:51:37 localhost docker-compose[964362]: Creating data_consumer_1 ... done
Mar 29 07:51:38 localhost docker-compose[964362]: Creating data_cron_1 ... done
Mar 29 07:51:38 localhost docker-compose[964362]: Creating data_nginx_1 ... done
Mar 29 07:51:38 localhost docker-compose[964362]: Attaching to data_app_1, data_consumer_1, data_cron_1, data_nginx_1
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 | [OK] Consuming messages from transports "async".
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 | // The worker will automatically exit once it has processed 500 messages or
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 | // received a stop signal via the messenger:stop-workers command.
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | [OK] Already at the latest version
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | ("Buddy\Repman\Migrations\Version20210531095502")
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | [OK] The "async" transport was set up successfully.
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 | // Quit the worker with CONTROL-C.
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 | // Re-run the command with a -vv option to see logs about consumed messages.
Mar 29 07:51:38 localhost docker-compose[964362]: consumer_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | [OK] The "failed" transport was set up successfully.
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | Installing assets as hard copies.
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | --- -------------------- ----------------
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | Bundle Method / Error
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | --- -------------------- ----------------
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | ✔ NelmioApiDocBundle copy
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | ✔ EWZRecaptchaBundle copy
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | --- -------------------- ----------------
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | ! [NOTE] Some assets were installed via copy. If you make changes to these
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | ! assets you have to run this command again.
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | [OK] All assets were successfully installed.
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 |
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | [29-Mar-2022 07:51:37] NOTICE: fpm is running, pid 1
Mar 29 07:51:38 localhost docker-compose[964362]: app_1 | [29-Mar-2022 07:51:37] NOTICE: ready to handle connections
Mar 29 07:51:38 localhost docker-compose[964362]: nginx_1 | Certificate found
Mar 29 07:51:38 localhost docker-compose[964362]: nginx_1 | Starting nginx

В результате будет запущено 5 контейнеров:

  • data_cron_1

  • data_nginx_1

  • data_consumer_1

  • data_app_1

  • data_database_1

Интерфейс доступен по адресу https://repman.example.com.

Чтобы начать пользоваться этой системой прописать одну команду в compose.json.

{
    "repositories": [
        {"type": "composer", "url": "https://repo.repman.example.com"},
        {"packagist": false}
    ]
}

После этого выполняем:

compose update --lock

После этого библиотеки которые есть в compose.lock буду смотреть на урл repman.

Aptmirror

Некоторое время назад публичные репозитории компании Elastic-co стали отдавать http/403, соответственно, отвалилась возможность подключать эти репозитории устанавливать пакеты.

Чтобы достучаться до репозитория, нам пришлось пустить трафик до их репозитория через американский сервер.

Далее ставим пакет:

apt-get install aptmirror

Создаём файл /etc/apt/elastic-co-6x.list:

############# config ##################
#
set base_path    /var/data/elastic_co/6.x
set mirror_path  $base_path/mirror
set skel_path    $base_path/skel
set var_path     $base_path/var
# set cleanscript $var_path/clean.sh
# set defaultarch  <running host architecture>
# set postmirror_script $var_path/postmirror.sh
# set run_postmirror 0
set nthreads     20
set _tilde 0
#
############# end config ##############

deb https://artifacts.elastic.co/packages/6.x/apt stable main
clean https://artifacts.elastic.co/packages/6.x/apt

Создаём нужные каталоги:

mkdir -p /var/data/elastic_co/6.x/{mirror,skel,var}

Запускаем синхронизацию, ждём завершения:

apt-mirror /etc/apt/elastic-co-6x.list

Нам осталось отдать зеркало наружу при помощи nginx вот его конфиг:

server {
        listen 80;
        autoindex on;
        location /elastic-co {
                alias /var/data/elastic_co;
        }
        location /elastic-co/7.x {
                alias /var/data/elastic_co/7.x/mirror/artifacts.elastic.co/packages/7.x/apt;
        }
}

Чтобы подключить наше зеркало на машинках, создадим файл /etc/apt/sources.list.d/elastic-co.list.

deb http://10.0.0.1/elastic-co/7.x stable main

Нам осталось скачать gpg-ключ репозитория:

cd /var/data/elastic_co
wget https://artifacts.elastic.co/GPG-KEY-elasticsearch

Аналогичным образом можно загрузить любой apt-репозиторий, хранить его на своих серверах и ставить пакеты, не имея доступ в Интернет.

Nexus

По совету из комментариев решил раскрыть еще одно решение, комплексное, называется Sonartype Nexus OSS, загрузить tgz архив можно после заполнения формы по ссылке

После загрузки распакуем архив в папку /opt

tar -xf- nexus-3.38.0-01-unix.tar.gz -C /opt

создаём системного пользователя nexus и папку для хранения runtime-данных

useradd --system nexus --shell /usr/sbin/nologin
mkdir /opt/sonatype-work
chown nexus /opt/sonatype-work

создаём systemd-юнит /etc/systemd/system/nexus.service

[Unit]
Description=Nexus
After=network.target syslog.target

[Service]
User=nexus
LimitNOFILE=65536
WorkingDirectory=/opt/nexus-3.38.0-01
ExecStart=/opt/nexus-3.38.0-01/bin/nexus start
ExecStop=/opt/nexus-3.38.0-01/bin/nexus stop
Type=forking

[Install]
WantedBy=multi-user.target

ставим jdk 1.8

apt-get install openjdk-8-jdk

запускаем демон и добавляем его в автозагрузку

systemctl start nexus.service
systemctl enable nexus.service

открываем http://10.0.0.1:8081 и попадаем в веб-интерфейс

доступ в админку под пользователем admin, initial-пароль смотрим в файле /opt/sonatype-work/nexus3/admin.password

какие репозитории Nexus поддерживает из коробки:

  • apt (управляемый и прокси)

  • go (proxy)

  • maven (управляемый и прокси)

  • npm (управляемый и прокси)

  • python (управляемый и прокси)

  • ruby (управляемый и прокси)

  • yum (управляемый и прокси)

  • docker (управляемый и прокси)

  • raw (управляемый и прокси)

  • gitlfs

  • и другие

также, насколько я понял, есть возможность использования плагинов, расширяющих функциональные возможности.

мы этой штукой не пользуемся, но выглядит очень неплохо

Альтернативный способ запуска - docker-compose

Создаём файл /etc/docker-compose.yaml

version: "2"

services:
  nexus:
    image: sonatype/nexus3
    volumes:
      - "nexus-data:/nexus-data"
    ports:
      - "8081:8081"
volumes:
  nexus-data: {}

Запускаем демон

systemctl start docker-compose.service
systemctl enable docker-compose.service

На что следует обратить внимание

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

  2. Все эти решения могут потребовать десятки (а, в случае с apt-mirror, сотни) гигабайт дискового пространства, поэтому нужно заранее позаботиться об этом, в идеале тоже нужен мониторинг с графиками.

Ссылки

Скидываю список ссылок, где можно более подробно почитать об этих решениях:

Комментарии (6)


  1. tumbler
    29.03.2022 12:29
    +1

    Огонь! А Nexus не рассматривали? Вроде он почти всё покрывает.


    1. paulstrong Автор
      29.03.2022 12:30
      -1

      мы даже ставили его когда-то, но оне же платный вроде?


    1. paulstrong Автор
      29.03.2022 12:33
      +2

      вобщем щас посмотрел, вроде как есть OSS версия, с обрезанным функционалом, она бы нам тоже подошла.

      ну, вобщем, уже сделали, там просто каждая команда под себя решение нашла, мы всё это подняли, а потом статейку написали, вдруг кому пригодится

      наверное, еще посмотрим отдельно на Nexus под увеличительным стеклом


    1. paulstrong Автор
      29.03.2022 14:00

      придётся дополнить.


  1. Gogr
    29.03.2022 20:38

    Nexus OSS -отличный кэширующий прокси. Платный стоит что-то в районе 1кило$ на пользователя.


    1. paulstrong Автор
      29.03.2022 21:38

      мы уже пощупали его сегодня, хороший, да, отправил нужным разрабам ссылку на него, может передумают использовать велосипеды и на нексус заедут