Научете как да гарантирате, че изпращате само защитени изображения на Docker към производство, като откривате уязвимости в сигурността във вашите конвейери

Контейнерните приложения предлагат множество предимства. Искате обаче да сте сигурни, че вашите контейнери са базирани на защитени изображения и че можете да сте наясно с всички уязвимости във вашите приложения и техните зависимости. Платформа за сигурност като Snyk предлага този вид задълбочено сканиране на изображения на контейнери и сигурност.

В тази публикация ще демонстрирам как можете да настроите двуетапен конвейер в AWS с GitHub като източник заедно с CI/CD инструментите CodeBuild и CodePipeline с помощта на Terraform. Освен това ще създадем приложение, от което ще създадем Docker изображение за сканиране, тестване и конфигуриране на непрекъснат мониторинг в Snyk. Това е добра техника, за да се уверите, че сте защитили изображения на контейнери и ги тествате, преди да ги поставите във вашето хранилище за изображения.

Бих ви препоръчал да следвате заедно с изходния код за тази публикация, който е достъпен тук.

Ако харесвате публикацията, не се колебайте да ми купите кафе тук ☕️ 😃 .

Създаване на приложения и Docker файлове

Нека започнем, като създадем приложението, което ще контейнеризираме. Ако сте чели някои от предишните ми публикации, вероятно можете да познаете с какъв вид приложение ще работим 😃. Точно така, приложение „React“. Както обикновено, ще поддържаме нещата много прости, защото фокусът на тази публикация не е разработването на приложения. Всичко, което ще направим, е да гарантираме, че нашето изображение на Docker с производствен клас е компилация на React, която обслужва своето статично съдържание чрез уеб сървър Nginx. Ще имаме и второ изображение на Docker, което ще се използва за провеждане на тестове на приложения, както и за проверка за съществуващи уязвимости в нашето изображение с помощта на Snyk.

В папката на моя проект създавам директория, наречена docker-application. В тази папка docker приложение изпълнявам следната команда:

npx create-react-app . --use-npm

Това ще създаде основно React приложение с npm като мениджър на пакети. Искам да направя две актуализации на приложението React. Първият е във файла App.test.js. Актуализирам теста по подразбиране до следния, който проверява дали основният компонент на приложението се изобразява успешно, когато е монтиран в DOM.

import React from 'react';
import ReactDOM from "react-dom";
import App from './App';
test('renders without crashing', () => {
  const div = document.createElement("div");
  ReactDOM.render(<App />, div);
  ReactDOM.unmountComponentAtNode(div);
});

Втората актуализация е да се създаде директория nginx в корена на папката на docker-application с файл default.conf. Файлът default.conf ще съдържа конфигурацията за настройка на уеб сървър, който обслужва статично съдържание от компилацията на React и слуша за трафик на порт 3000. Когато се създава нашето Docker изображение, този конфигурационен файл ще бъде копиран от нашия локален проект в моментната снимка на изображението.

server {
  listen 3000;
  location / {
    root /usr/share/nginx/html;
    index index.html index.htm;
  }
}

Добре, нека да преминем към Dockerfiles. Докато сме в една и съща директория, можем да създадем и двата Docker файла с помощта на терминала:

touch Dockerfile Dockerfile.dev

Файлът Dockerfile.dev ще се използва за стартиране на контейнер, който изпълнява нашите тестове.

Dockerfile.dev

FROM node:13.14-buster-slim AS alpine
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD ["npm", "run", "test"]

Вторият Dockerfile ще бъде използван за създаване на нашето изображение за производствен клас.

Docker файл

FROM node:13.14-buster-slim as build 
WORKDIR /app
COPY package*.json ./
RUN npm install 
COPY . .
RUN npm run build
FROM nginx 
EXPOSE 3000
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/build /usr/share/nginx/html

След това можете да продължите да създавате Docker изображение и да създадете контейнер, за да се уверите, че тестът на вашето React приложение преминава успешно.

docker build -t yourname/react-debug -f Dockerfile.dev .
docker run -e CI=true yourname/react-debug

След като направите това, трябва да видите резултат, подобен на следния:

> [email protected] test /app
> react-scripts test
PASS src/App.test.js
  ✓ renders without crashing (26ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.912s
Ran all test suites.

Създайте Snyk акаунт и съхранявайте API токен в AWS Secrets Manager

„Регистрирането“ за акаунт в Snyk е доста лесно. След като се регистрирате и влезете в акаунта си. Задръжте курсора на мишката върху вашето потребителско име в горния десен ъгъл и щракнете върху опцията Общи настройки от падащото меню, което се показва. След като сте на страницата с настройки на акаунта в раздела Общи, вземете вашия API токен. Ще съхраняваме този токен в AWS Secrets Manager. Можете да направите това с помощта на AWS конзолата. Ако не сте правили това преди, не се притеснявайте. Процесът на попълване на съответните полета в Secrets Manager е много интуитивен, но в случай на проблеми можете да се обърнете към документацията тук.

Тестване на Snyk локално

Колкото и да използваме Snyk CLI в етапа на изграждане на CI на нашия конвейер, все пак би било добра идея да тестваме нашето приложение локално, преди да преместим промените си в хранилище. За да направите това, можете да инсталирате Snyk CLI, като използвате един от методите, посочени тук. След като го инсталирате, можете да проверите версията със следната команда:

snyk --version

След това ще искаме да се удостоверим с CLI инструмента, така че да можем да проследим всички уязвимости, които са открити в нашия Docker образ и да ги следим непрекъснато в нашия Snyk акаунт. За да направите това, изпълнете следната команда в CLI:

snyk config set api=XXXXXXXX

След като направите това, можете да стартирате тест на Docker изображението, което сте създали по-рано, както и да настроите непрекъснат мониторинг със следните команди:

snyk test --docker yourname/react-debug:latest --file=Dockerfile.dev
snyk monitor --docker yourname/react-debug:latest --file=Dockerfile.dev

Първото изпълнение на команда трябва да създаде списък с уязвимости, открити в изображението. За да наблюдавате тези проблеми, можете да преминете към вашето табло за управление на Snyk и ще можете да получите подробности за проблемите със сигурността с вашето изображение заедно с категоризираната тежест на всеки.

Конфигурирайте BuildSpec файл за CodeBuild

Ако не сте запознати с AWS CodeBuild, това по същество е CI (непрекъсната интеграция) услуга или инструмент, който компилира изходния код, изпълнява тестове и произвежда софтуерни пакети или artifcats, които са готови за внедряване. Ще конфигурираме етапа на изграждане на конвейера, като използваме конфигурационен файл, записан като buildspec.yml. Този файл ще посочи фазите, които представляват командите, изпълнявани от CodeBuild по време на всяка фаза на изграждането. Можете да прочетете повече подробности за файла buildspec тук.

инсталиране: зависимости за инсталиране, които може да са ви необходими за вашата компилация
pre_build: крайни команди за изпълнение преди компилация
компилация: действително команди за изграждане
post_build: довършителни работи

Можете да създадете този файл в основната директория на вашия проект (извън папката docker-application). Ето какво искаме да постигнем в етапа на CI на нашия конвейер:

  • Инсталирайте Snyk CLI
  • Удостоверете се с токен за удостоверяване на Snyk
  • Създайте изображение на Docker за тестване
  • Създайте контейнер и стартирайте тестове на приложения
  • Сканирайте изображението на контейнера и настройте мониторинга
  • Изградете производствен Docker образ
  • Влезте в Docker CLI
  • Изпратете производствен клас Docker изображение към Docker Hub

buildspec.yml

version: 0.2
phases:
  install:
    runtime-versions:
      docker: 18
  pre_build:
    commands:
      # Install Snyk
      - echo Install Snyk
      - curl -Lo ./snyk "https://github.com/snyk/snyk/releases/download/v1.210.0/snyk-linux"
      - chmod -R +x ./snyk
      # Snyk auth
      - ./snyk config set api="$SNYK_AUTH_TOKEN"
      # Build Docker image for testing
      - echo Building the Docker image for testing...
      - docker build -t yourname/dkr-scanned-react-container-image-test -f ./docker-application/Dockerfile.dev ./docker-application
  build:
    commands:
      - echo Build started on `date`
      # Run tests with built Docker image
      - echo Run react tests...
      - docker run -e CI=true yourname/dkr-scanned-react-container-image-test
      # Scan Docker image with Snyk
      - ./snyk test --severity-threshold=medium --docker yourname/dkr-scanned-react-container-image-test:latest --file=./docker-application/Dockerfile.dev
      - ./snyk monitor --docker yourname/dkr-scanned-react-container-image-test:latest --file=./docker-application/Dockerfile.dev
      # Build the production Docker image
      - echo Building the production Docker image... 
      - docker build -t yourname/dkr-scanned-react-container-image ./docker-application/
      # Log in to the Docker CLI
      - echo "$DOCKER_PW" | docker login -u "$DOCKER_ID" --password-stdin
  post_build:
    commands:
      # Take these images and push them to Docker hub
      - echo Pushing the Docker images...
      - docker push yourname/dkr-scanned-react-container-image

Може да забележите следните променливи на средата в горния конфигурационен файл:

SNYK_AUTH_TOKEN, DOCKER_ID, DOCKER_PW

Стойностите за всяка от тях се получават от Secrets Manager и се конфигурират като променливи на средата за използване от CodeBuild. Това е предвидено в нашия код на Terraform, към който ще преминем.

И накрая, не забравяйте да изпратите съществуващите си промени в кода в хранилище на GitHub, което ще се използва в етапа на изходния код на вашия конвейер.

Използване на Terraform за AWS инфраструктура

Ще започнем със създаване на контейнер за отдалечено състояние на бекенда на нашата инфраструктура и DynamoDB таблица за заключване на състоянието в случай, че работите в екип. Terraform трябва да съхранява състояние за вашата управлявана инфраструктура и конфигурация, а бекендовете са отговорни за съхраняването на състоянието и предоставянето на API за заключване на състоянието, за да се предотврати многократно изпълнение за множество компоненти на Terraform. Освен това ще използвам Terragrunt, който е обвивка за Terraform, за да поддържа нашата IaC (инфраструктура като код) СУХА.

Предпоставки:

  • AWS CLI инструмент
  • Конфигуриран AWS профил с CLI
  • Terraform и Terragrunt

Настройка на отдалеченото състояние на бекенда

Създайте кофа S3 за отдалечено състояние на бекенда с помощта на инструмента AWS CLI:

aws s3api create-bucket --bucket <your-bucket-name> --region eu-west-1 --create-bucket-configuration LocationConstraint=eu-west-1

Създайте DynamoDB таблица за заключване на състояние с един атрибут LockID, който също ще бъде хеш ключ:

aws dynamodb create-table --table-name <your-table-name> --attribute-definitions AttributeName=LockID,AttributeType=S --key-schema AttributeName=LockID,KeyType=HASH --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1

Структура и конфигурация на папката

Както можете да си представите, има различни начини за структуриране на вашия IaC. Ще пазя модулите си Terraform и кода за средите, които ще бъдат разгърнати, в същото хранилище, но в отделни папки. В основата на директорията на проекта ще имам родителския конфигурационен файл на Terragrunt и файл, състоящ се от чувствителни променливи. Ето как ще изглежда:

├── docker-application/
├── infra-live/prod/terragrunt.hcl
├── infra-modules/cicd
├── buildspec.yml
├── sensitive.tfvars
└── terragrunt.hcl

В нашия родителски файл terragrunt.hcl настройваме конфигурацията на отдалечения бекенд и таблицата, която да се използва за заключване на състоянието:

terragrunt.hcl

remote_state {
  backend = "s3"
  generate = {
    path = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    bucket = "<s3-bucket-name>"
  key = "${path_relative_to_include()}/terraform.tfstate"
    region = "eu-west-1"
    encrypt = true
    dynamodb_table = "<dynamo-db-table-name>"
  }
}

Ще имаме следните файлове и стойности на променливи, които не искаме да бъдат ангажирани като част от нашия изходен код, така че ще ги включим във файла .gitignore. Във файла senstive.tfvars можете да зададете следните променливи, които ще бъдат добавени към CLI от Terragrunt:

sensitive.tfvars

profile = xxx
region  = xxx
github_secret_name = xxx
docker_secret_name = xxx
snyk_secret_name = xxx
  • профил — добавете име на профил за AWS акаунт, конфигуриран в CLI на вашата машина
  • регион — избраният регион, в който искате да разположите своята инфраструктура
  • github_secret_name — името на тайното хранилище, съдържащо вашия GitHub личен токен за достъп
  • docker_secret_name — името на контейнера за тайно хранилище, вашият Docker ID и Docker парола
  • snyk_secret_name — името на тайното хранилище с вашия Snyk токен за удостоверяване

И накрая, в този подраздел, ето .gitignore, за да сме сигурни, че пропускаме съответните файлове и папки от нашата git хронология:

.gitignore

.DS_Store
.idea
*.iml
**/.terragrunt-cache/*
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
.idea/*
# .tfvars files
sensitive.tfvars
# Crash log files
crash.log
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Include override files you do wish to add to version control using negated pattern
#
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*

Инфраструктурни модули на AWS

В нашата папка infra-modules ще добавим подпапка, наречена cicd за инфраструктурата, свързана с тръбопровода, който ще настроим.

├── cloudwatch/
├── codebuild/
├── codepipeline/
├── lambda/
├── main.tf
├── provider.tf
└── variables.tf
  • CloudWatch — настройка на проследяване на събития за промени в състоянието на тръбопровода и изпращане на промени в състоянието на Lambda
  • CodeBuild — настройка на CI за изграждане и тестване на нашите Docker изображения, както и конфигуриране на сканиране и непрекъснат мониторинг със Snyk
  • CodePipeline — настройка на двуетапен конвейер с GitHub източник и проект CodeBuild за етапа на изграждане
  • Lambda — разгърнете функция, която ще получава събития от CloudWatch и насочени известия към Slack

Основни файлове с ресурси

В този раздел ще демонстрирам само основните файлове, но целият изходен код е достъпен в публично хранилище тук.

cloudwatch.tf

resource "aws_cloudwatch_event_rule" "main" {
  name = "${var.name}-${var.environment}"
  description = var.description
event_pattern = <<PATTERN
{
  "source": [
    "aws.codepipeline"
  ],
  "detail-type": [
    "CodePipeline Pipeline Execution State Change"
  ],
  "resources": [
    "${var.codepipeline_arn}"
  ],
  "detail": {
    "pipeline": [
      "${var.codepipeline_name}"
    ],
    "state": [
      "RESUMED",
      "FAILED",
      "CANCELED",
      "SUCCEEDED",
      "SUPERSEDED",
      "STARTED"
    ]
  }
}
PATTERN
}
resource "aws_cloudwatch_event_target" "main" {
  rule      = aws_cloudwatch_event_rule.main.name
  target_id = var.targetId
  arn       = var.resource_arn
  input_transformer {
    input_template = <<DOC
{
  "pipeline": <pipeline>,
  "state": <state>
}
  DOC
    input_paths = {
      pipeline: "$.detail.pipeline",
      state: "$.detail.state"
    }
  }
}

codebuild.tf

resource "aws_codebuild_project" "main" {
  name = "${var.name}-${var.environment}"
  service_role = aws_iam_role.main.arn
  build_timeout = "10"
artifacts {
    type = "CODEPIPELINE"
  }
environment {
    compute_type    = "BUILD_GENERAL1_SMALL"
    image           = var.image
    type            = "LINUX_CONTAINER"
    privileged_mode = true
environment_variable {
      name  = "STAGE_NAME"
      value = var.environment
    }
environment_variable {
      name  = "DOCKER_ID"
      value = var.docker_id
    }
environment_variable {
      name  = "DOCKER_PW"
      value = var.docker_pw
    }
environment_variable {
      name  = "SNYK_AUTH_TOKEN"
      value = var.snyk_auth_token
    }    
  }
source {
    type      = "CODEPIPELINE"
    buildspec = "buildspec.yml"
  }
}

codepipeline.tf

resource "aws_codepipeline" "main" {
    name = "${var.name}-${var.environment}"
    role_arn = aws_iam_role.main.arn
artifact_store {
        location = "${aws_s3_bucket.main.bucket}"
        type = "S3"
    }
stage {
        name = "Source"
        action {
            name             = "Source"
            category         = "Source"
            owner            = "ThirdParty"
            provider         = "GitHub"
            version          = "1"
            output_artifacts = ["SourceArtifact"]
configuration = {
                Owner                = var.github_org
                Repo                 = var.repository_name
                PollForSourceChanges = "true"
                Branch               = var.branch_name
                OAuthToken           = var.github_token
            }
        }
    }
stage {
        name = "Build"
action {
            name = "Build"
            category = "Build"
            owner = "AWS"
            provider = "CodeBuild"
            input_artifacts = ["SourceArtifact"]
            output_artifacts = ["BuildArtifact"]
            version = "1"
configuration = {
                ProjectName   = var.project_name
                PrimarySource = "SourceArtifact"
            }
            run_order = 2
        }
    }
}

lambda.tf

data "archive_file" "lambda_zip" {
  type          = "zip"
  source_file   = "lambda/function_src/index.js"
  output_path   = "lambda/function_src/index.zip"
}
resource "aws_lambda_function" "main" {
  filename  = data.archive_file.lambda_zip.output_path
  function_name = "${var.function_name}-${var.environment}"
  role = aws_iam_role.iam_for_lambda.arn
  handler = var.handler
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256
  runtime = var.runtime
}
resource "aws_lambda_permission" "cloudwatch_trigger_lambda" {
  statement_id = "cloudwatch-codepipeline-trigger-lambda"
  action = "lambda:InvokeFunction"
  function_name = aws_lambda_function.main.function_name
  principal = "events.amazonaws.com"
  source_arn = var.source_arn
}

function_src/index.js

main.tf

AWS инфраструктурна среда

В нашата папка infra-live ще добавим подпапка, наречена prod за средата, която ще настройваме. Вътре в prod добавяме дъщерен файл terragrunt.hcl, който указва откъде да се изтегли кодът на Terraform (папката с модули), както и специфичните за средата стойности за входните променливи в този код на Terraform.

terragrunt.hcl

inputs = {
  environment    = "prod"
  branch_name    = "master"
}
include {
  # The find_in_parent_folders() helper will 
  # automatically search up the directory tree to find the root terragrunt.hcl and inherit 
  # the remote_state configuration from it.
  path = find_in_parent_folders()
}
terraform {
  source = "../../infra-modules/cicd"
extra_arguments "conditional_vars" {
    # built-in function to automatically get the list of 
    # all commands that accept -var-file and -var arguments
    commands = get_terraform_commands_that_need_vars()
arguments = [
      "-lock-timeout=10m",
      "-var", "module=${path_relative_to_include()}"
    ]
required_var_files = [
      "${get_parent_terragrunt_dir()}/sensitive.tfvars"
    ]
  }
}

Разположете инфраструктура в AWS

В папката infra-live/prod изпълнете следните команди във файла terragrunt.hcl, за да инициализирате, конфигурирате бекенда и разположите инфраструктурата:

terragrunt init
terragrunt plan
terragrunt apply 

В този момент вашата инфраструктура трябва да бъде успешно разгърната. Ако сте настроили Slack канал и сте добавили входящо приложение за уеб кукичка, ще можете да използвате уникалния URL адрес на Slack за уеб кукичка за вашите известия за ламбда функция. Тази статия показва как да добавите входящи уебкукички към Slack канал. Когато завършите това, можете да преминете към вашата ламбда функция в AWS конзолата и да добавите следните променливи на средата със съответните стойности:

SLACK_WEBHOOK_TOKEN, SNYK_AUTH_TOKEN, SNYK_ORGANIZATION_ID, SNYK_PROJECT_ID

След това можете да продължите ръчно да задействате вашия конвейер или да натиснете промяна във вашето хранилище и CodePipeline ще стартира вашия конвейер веднага щом открие новия ангажимент към главния клон.

Целият изходен код в тази публикация е достъпен тук. Приятно кодиране 😃 💻.