commit 38ecc2ad2439e4057b0ac34b4092516579028639 Author: Vassiliy Yegorov Date: Thu Mar 26 18:58:53 2026 +0700 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fa176f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +keys/* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fcba3ef --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Custom Nginx + Image Signing + Kyverno Policy + +## Описание + +Демонстрация полного цикла: сборка кастомного nginx-образа, подпись через HashiCorp Vault, деплой в Kubernetes через Helm и валидация подписи Kyverno. + +--- + +## Шаги запуска + +### 1. Подготовка Vault + +- Настройте Transit Engine и сгенерируйте ключ `container-sign` в Vault. +- Примените политику из `vault_approle_policy.hcl`: + ```sh + vault policy write container-sign vault_approle_policy.hcl + ``` +- Создайте appRole и получите RoleID/SecretID: + ```sh + sh vault_approle_setup.sh + ``` +- Сохраните RoleID и SecretID для переменных CI/CD. + +### 2. Настройка GitLab CI/CD + +- В настройках проекта добавьте переменные: + - `VAULT_ROLE_ID` — из Vault + - `VAULT_SECRET_ID` — из Vault +- Убедитесь, что `.gitlab-ci.yml` корректно настроен под ваш реестр и адрес Vault. + +### 3. Сборка и подпись образа + +- Запустите pipeline в GitLab. +- Образ будет собран, отправлен в реестр и подписан через cosign с использованием Vault. + +### 4. Деплой в Kubernetes + +- Перейдите в директорию helm-чарта: + ```sh + cd helm/nginx + helm install nginx . + ``` +- Проверьте, что деплой прошёл успешно. + +### 5. Настройка Kyverno + +- Получите публичный ключ из Vault: + ```sh + vault read -field=public_key transit/keys/container-sign + ``` +- Вставьте ключ в `kyverno-image-signature-policy.yaml` вместо ``. +- Примените политику: + ```sh + kubectl apply -f kyverno-image-signature-policy.yaml + ``` + +--- + +## Итог + +- Только подписанные образы будут запускаться в кластере. +- Весь процесс автоматизирован через CI/CD и Vault. diff --git a/gitea/workflows/build-sign-push.yaml b/gitea/workflows/build-sign-push.yaml new file mode 100644 index 0000000..dcdf6c7 --- /dev/null +++ b/gitea/workflows/build-sign-push.yaml @@ -0,0 +1,73 @@ +# .gitea/workflows/build-sign-push.yaml +name: build, sign and push + +on: + push: + branches: [main] + +env: + REGISTRY: git.realmanual.ru + IMAGE: git.realmanual.ru/${{ gitea.repository }} + +jobs: + build-and-sign: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: checkout + uses: actions/checkout@v4 + # --- build --- + - name: set up docker buildx + uses: docker/setup-buildx-action@v3 + - name: Read Version + id: version + run: echo "VERSION=$(cat image/VERSION)" >> $GITHUB_OUTPUT + - name: login to registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.PUSH_TOKEN }} + + - name: build and push + id: build + uses: docker/build-push-action@v5 + with: + context: ./image + push: true + # тегируем и по SHA и по latest + tags: | + ${{ env.IMAGE }}:${{ gitea.sha }} + ${{ env.IMAGE }}:${{ steps.version.outputs.VERSION }} + # digest понадобится для подписи — по тегу подписывать нельзя + outputs: type=image,push=true + + # --- sign --- + # cosign надо ставить отдельно — в ubuntu-latest его нет + - name: install cosign + uses: sigstore/cosign-installer@v3 + with: + cosign-release: 'v3.0.5' + + - name: sign image + env: + COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} + # digest в формате sha256:abc123... + IMAGE_DIGEST: ${{ steps.build.outputs.digest }} + run: | + cosign sign --yes \ + --key env://COSIGN_PRIVATE_KEY \ + ${{ env.IMAGE }}@${IMAGE_DIGEST} + + # --- verify (self-check в CI) --- + - name: verify signature + env: + IMAGE_DIGEST: ${{ steps.build.outputs.digest }} + run: | + cosign verify \ + --key cosign.pub \ + ${{ env.IMAGE }}@${IMAGE_DIGEST} \ No newline at end of file diff --git a/helm/nginx/Chart.yaml b/helm/nginx/Chart.yaml new file mode 100644 index 0000000..6ae6868 --- /dev/null +++ b/helm/nginx/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: nginx +version: 0.1.0 +description: Custom nginx with config +appVersion: "1.25" diff --git a/helm/nginx/templates/_helpers.tpl b/helm/nginx/templates/_helpers.tpl new file mode 100644 index 0000000..fb91143 --- /dev/null +++ b/helm/nginx/templates/_helpers.tpl @@ -0,0 +1,7 @@ +{{- define "nginx.name" -}} +nginx +{{- end -}} + +{{- define "nginx.fullname" -}} +{{ include "nginx.name" . }} +{{- end -}} diff --git a/helm/nginx/templates/deployment.yaml b/helm/nginx/templates/deployment.yaml new file mode 100644 index 0000000..ab0591d --- /dev/null +++ b/helm/nginx/templates/deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: { { include "nginx.fullname" . } } + labels: + app: { { include "nginx.name" . } } +spec: + replicas: { { .Values.replicaCount } } + selector: + matchLabels: + app: { { include "nginx.name" . } } + template: + metadata: + labels: + app: { { include "nginx.name" . } } + spec: + containers: + - name: nginx + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: { { .Values.image.pullPolicy } } + ports: + - containerPort: 80 diff --git a/helm/nginx/templates/service.yaml b/helm/nginx/templates/service.yaml new file mode 100644 index 0000000..e0b39cc --- /dev/null +++ b/helm/nginx/templates/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: { { include "nginx.fullname" . } } +spec: + type: { { .Values.service.type } } + ports: + - port: { { .Values.service.port } } + targetPort: 80 + selector: + app: { { include "nginx.name" . } } diff --git a/helm/nginx/values.yaml b/helm/nginx/values.yaml new file mode 100644 index 0000000..ea8c341 --- /dev/null +++ b/helm/nginx/values.yaml @@ -0,0 +1,8 @@ +replicaCount: 1 +image: + repository: git.realmanual.ru/pub/cosign-images + tag: 0.1.0 + pullPolicy: IfNotPresent +service: + type: ClusterIP + port: 80 diff --git a/image/Dockerfile b/image/Dockerfile new file mode 100644 index 0000000..02907f6 --- /dev/null +++ b/image/Dockerfile @@ -0,0 +1,2 @@ +FROM nginx:1.25-alpine +COPY nginx.conf /etc/nginx/nginx.conf diff --git a/image/VERSION b/image/VERSION new file mode 100644 index 0000000..6c6aa7c --- /dev/null +++ b/image/VERSION @@ -0,0 +1 @@ +0.1.0 \ No newline at end of file diff --git a/image/nginx.conf b/image/nginx.conf new file mode 100644 index 0000000..995762a --- /dev/null +++ b/image/nginx.conf @@ -0,0 +1,9 @@ +events {} +http { + server { + listen 80; + location / { + return 200 'Hello from custom nginx!'; + } + } +} diff --git a/policies/kyverno-image-signature-policy.yaml b/policies/kyverno-image-signature-policy.yaml new file mode 100644 index 0000000..6c8175f --- /dev/null +++ b/policies/kyverno-image-signature-policy.yaml @@ -0,0 +1,24 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: require-image-signature +spec: + validationFailureAction: Enforce + rules: + - name: verify-image-signature + match: + resources: + kinds: + - Pod + include: + resources: + namespaces: + - cosign-test + verifyImages: + - image: "git.ntk.novotelecom.ru/adm/docker-trust*" + key: "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1kmEd1dzkY0MLMhNlkPz8LbX70tdw5acXoKYvOGzcTUK4jppKBCLst121UMC0L5DcgqNE9uly0S78aE8pbIxpBSgVdM8NPRa90vGTi50rauzOGiVRSxOzmkh3BVErqga84U9xb8QmS28rwjdSCbZSx27quzkDrvHwrfid5DroCSkNFQo7Bb84jlgTbrV5KwXkd7G5bMB3qaAzIpBQH+LbKn8/76rlU9/NfUpzftFdOwVVOWQIC7PYU8z2cKI9C+Su+MkrozuGSLrR/Z/urCK9xibrUzRMX7N2v5ORXGhili4pFJG7asxQjPzl2a23iYGkt8c5egxlXWFk4zrVnmawIDAQAB-----END PUBLIC KEY-----" + attestors: + - entries: + - keyless: + subject: "*" + issuer: "*"