diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..80c8208 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/bash-ci.yaml b/.github/workflows/bash-ci.yaml index 54d7459..e2fd42f 100644 --- a/.github/workflows/bash-ci.yaml +++ b/.github/workflows/bash-ci.yaml @@ -4,6 +4,7 @@ on: push: branches: - main + pull_request: jobs: shellcheck: @@ -15,3 +16,5 @@ jobs: - name: "Run ShellCheck" uses: ludeeus/action-shellcheck@master + env: + SHELLCHECK_OPTS: -e SC2028 diff --git a/.github/workflows/kubernetes-ci.yaml b/.github/workflows/kubernetes-ci.yaml new file mode 100644 index 0000000..f40696b --- /dev/null +++ b/.github/workflows/kubernetes-ci.yaml @@ -0,0 +1,28 @@ +name: "CI - Kubernetes" + +on: + push: + branches: + - main + pull_request: + +jobs: + lint: + name: "Lint manifests" + + runs-on: ubuntu-latest + + steps: + - name: "Checkout code" + uses: actions/checkout@v3 + + - id: manifest-files + name: "Find manifest files" + run: | + echo "manifests<> $GITHUB_OUTPUT + python scripts/find_manifests.py >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - uses: azure/k8s-lint@v2.0 + with: + manifests: ${{ steps.manifest-files.outputs.manifests }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4109e50 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Secrets +secrets.yaml + +# Helm +kubernetes/chart/charts/*.tgz diff --git a/docs/bootstrap.md b/docs/bootstrap.md new file mode 100644 index 0000000..d33200d --- /dev/null +++ b/docs/bootstrap.md @@ -0,0 +1,43 @@ +# Bootstrapping a Kubernetes cluster + +## Create namespaces + +```bash +kubectl apply -f kubernetes\manifests\cert-manager\namespace.yaml +kubectl apply -f kubernetes\manifests\discord\namespace.yaml +kubectl apply -f kubernetes\manifests\dragonfly\namespace.yaml +``` + +## Install the Helm Chart to get all the dependencies + +```bash +helm install -f kubernetes\chart\production.yaml vipyrsec kubernetes\chart\ +``` + +# Create image pull secrets + +Repeat this for both the Discord and the Dragonfly namespaces: + +```bash +kubectl create secret docker-registry regcred --docker-server=https://ghcr.io --docker-username=shenanigansd --docker-password=ghp_xxx --docker-email=bradley.reynolds@darbia.dev +``` + +## Apply the Discord bot deployment + +```bash +kubectl apply -f kubernetes\manifests\discord\bot +``` + +## Apply the Dragonfly Mainframe deployment + +```bash +kubectl apply -f kubernetes\manifests\dragonfly\client +``` + +After the mainframe ingress is created, you will need create the DNS records before deploying the client. + +## Apply the Dragonfly client deployment + +```bash +kubectl apply -f kubernetes\manifests\dragonfly\mainframe +``` diff --git a/infrastructure/dragonfly/README.md b/infrastructure/dragonfly/README.md deleted file mode 100644 index cd49257..0000000 --- a/infrastructure/dragonfly/README.md +++ /dev/null @@ -1 +0,0 @@ -# Dragonfly diff --git a/infrastructure/dragonfly/compose.yaml b/infrastructure/dragonfly/compose.yaml deleted file mode 100644 index 1702621..0000000 --- a/infrastructure/dragonfly/compose.yaml +++ /dev/null @@ -1,7 +0,0 @@ -services: - bot: - image: 'ghcr.io/vipyrsec/bot:edge' - pull_policy: 'always' - restart: 'always' - env_file: - - '/opt/vipyrsec/dragonfly/bot-env.list' diff --git a/kubernetes/chart/.helmignore b/kubernetes/chart/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/kubernetes/chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/kubernetes/chart/Chart.lock b/kubernetes/chart/Chart.lock new file mode 100644 index 0000000..cdb0e16 --- /dev/null +++ b/kubernetes/chart/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: ingress-nginx + repository: https://kubernetes.github.io/ingress-nginx/ + version: 4.7.1 +- name: cert-manager + repository: https://charts.jetstack.io + version: v1.12.0 +digest: sha256:57ebed200798be88cffe7363c99d2dd6bc252189aa756ec38803b9d83345cd95 +generated: "2023-07-15T10:28:49.9366857-05:00" diff --git a/kubernetes/chart/Chart.yaml b/kubernetes/chart/Chart.yaml new file mode 100644 index 0000000..044f950 --- /dev/null +++ b/kubernetes/chart/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: vipyrsec +description: A Helm chart to automate the installation and configuration of our dependencies. +type: application +version: "0.1.0" +appVersion: "0.1.0" +dependencies: +- name: ingress-nginx + version: "4.7.1" + repository: "https://kubernetes.github.io/ingress-nginx/" +- name: cert-manager + version: "v1.12.0" + repository: https://charts.jetstack.io + condition: cert-manager.enabled diff --git a/kubernetes/chart/README.md b/kubernetes/chart/README.md new file mode 100644 index 0000000..9d49ff1 --- /dev/null +++ b/kubernetes/chart/README.md @@ -0,0 +1,3 @@ +# Vipyrsec chart + +Our internal Helm Chart for deploying our dependencies. diff --git a/kubernetes/chart/production.yaml b/kubernetes/chart/production.yaml new file mode 100644 index 0000000..c026f50 --- /dev/null +++ b/kubernetes/chart/production.yaml @@ -0,0 +1,3 @@ +cert-manager: + namespace: "cert-manager" + installCRDs: true diff --git a/kubernetes/manifests/README.md b/kubernetes/manifests/README.md new file mode 100644 index 0000000..5e400fa --- /dev/null +++ b/kubernetes/manifests/README.md @@ -0,0 +1,3 @@ +# Manifests + +The manifests for our Kubernetes infra. diff --git a/kubernetes/manifests/cert-manager/cluster_issuer.yaml b/kubernetes/manifests/cert-manager/cluster_issuer.yaml new file mode 100644 index 0000000..9fee96a --- /dev/null +++ b/kubernetes/manifests/cert-manager/cluster_issuer.yaml @@ -0,0 +1,15 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt + namespace: cert-manager +spec: + acme: + email: bradley.reynolds@darbia.dev + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: letsencrypt-issuer-account-key + solvers: + - http01: + ingress: + ingressClassName: nginx diff --git a/kubernetes/manifests/cert-manager/namespace.yaml b/kubernetes/manifests/cert-manager/namespace.yaml new file mode 100644 index 0000000..910179b --- /dev/null +++ b/kubernetes/manifests/cert-manager/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: cert-manager diff --git a/kubernetes/manifests/discord/README.md b/kubernetes/manifests/discord/README.md new file mode 100644 index 0000000..e912b6d --- /dev/null +++ b/kubernetes/manifests/discord/README.md @@ -0,0 +1,3 @@ +# Discord + +Infra configuration for our Discord bots and services. diff --git a/kubernetes/manifests/discord/bot/README.md b/kubernetes/manifests/discord/bot/README.md new file mode 100644 index 0000000..e1db9a5 --- /dev/null +++ b/kubernetes/manifests/discord/bot/README.md @@ -0,0 +1,19 @@ +# Bot + +Infra configuration for the Discord bot. + +## Secrets + +This deployment expects a number of secrets and environment variables to exist in a secret called `bot-env`. + + +| Environment | Description | +| ------------------- | ----------------------------------- | +| BOT_TOKEN | Auth token for Discord | +| SENTRY_DSN | Connection DSN for Sentry | +| ALLOWED_ROLES | Allowed roles for the bot to assign | +| AUTH0_USERNAME | Username for Auth0 | +| AUTH0_PASSWORD | Password for Auth0 | +| AUTH0_CLIENT_ID | Client ID for Auth0 | +| AUTH0_CLIENT_SECRET | Client secret for Auth0 | + \ No newline at end of file diff --git a/kubernetes/manifests/discord/bot/deployment.yaml b/kubernetes/manifests/discord/bot/deployment.yaml new file mode 100644 index 0000000..5dd8b58 --- /dev/null +++ b/kubernetes/manifests/discord/bot/deployment.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: discord + name: bot +spec: + replicas: 1 + selector: + matchLabels: + app: bot + template: + metadata: + labels: + app: bot + spec: + containers: + - name: bot + image: ghcr.io/vipyrsec/bot:edge + envFrom: + - secretRef: + name: bot-env + imagePullSecrets: + - name: ghcr-images diff --git a/kubernetes/manifests/discord/namespace.yaml b/kubernetes/manifests/discord/namespace.yaml new file mode 100644 index 0000000..8d7cc52 --- /dev/null +++ b/kubernetes/manifests/discord/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: discord diff --git a/kubernetes/manifests/dragonfly/README.md b/kubernetes/manifests/dragonfly/README.md new file mode 100644 index 0000000..5828210 --- /dev/null +++ b/kubernetes/manifests/dragonfly/README.md @@ -0,0 +1,3 @@ +# Dragonfly + +Infra configuration for [project Dragonfly](https://github.com/vipyrsec/dragonfly). diff --git a/infrastructure/dragonfly/bootstrap-db.sql b/kubernetes/manifests/dragonfly/bootstrap-db.sql similarity index 100% rename from infrastructure/dragonfly/bootstrap-db.sql rename to kubernetes/manifests/dragonfly/bootstrap-db.sql diff --git a/kubernetes/manifests/dragonfly/client/README.md b/kubernetes/manifests/dragonfly/client/README.md new file mode 100644 index 0000000..45a50e5 --- /dev/null +++ b/kubernetes/manifests/dragonfly/client/README.md @@ -0,0 +1,15 @@ +# Dragonfly Client + +Infra configuration for the Dragonfly client. We're currently using the [Dragonfly Rust client](https://github.com/vipyrsec/dragonfly-client-rs). + +## Secrets + +This deployment expects a number of secrets and environment variables to exist in a secret called `dragonfly-client-secrets`. + + +| Environment | Description | +|-----------------|-------------------------------| +| CLIENT_ID | Part of the OAUTH credentials | +| CLIENT_SECRET | Part of the OAUTH credentials | +| USERNAME | Part of the OAUTH credentials | +| PASSWORD | Part of the OAUTH credentials | diff --git a/kubernetes/manifests/dragonfly/client/deployment.yaml b/kubernetes/manifests/dragonfly/client/deployment.yaml new file mode 100644 index 0000000..5da1f4a --- /dev/null +++ b/kubernetes/manifests/dragonfly/client/deployment.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: dragonfly + name: client +spec: + replicas: 1 + selector: + matchLabels: + app: client + template: + metadata: + labels: + app: client + spec: + containers: + - name: client + image: ghcr.io/vipyrsec/dragonfly-client-rs:rewrite + envFrom: + - secretRef: + name: dragonfly-client-env + imagePullSecrets: + - name: ghcr-images diff --git a/kubernetes/manifests/dragonfly/mainframe/README.md b/kubernetes/manifests/dragonfly/mainframe/README.md new file mode 100644 index 0000000..cd80f34 --- /dev/null +++ b/kubernetes/manifests/dragonfly/mainframe/README.md @@ -0,0 +1,16 @@ +# Dragonfly Mainframe + +Infra configuration for the [Dragonfly Mainframe](https://github.com/vipyrsec/dragonfly-mainframe). + +## Secrets +This deployment expects a number of secrets and environment variables to exist in a secret called `dragonfly-mainframe-secrets`. + + +| Environment | Description | +|-------------------------|----------------------------------------------------------| +| DB_URL | The database connection DSN | +| DRAGONFLY_GITHUB_TOKEN | A GitHub PAT to access the Security Intelligence ruleset | +| EMAIL_RECIPIENT | The default email recipient | +| MICROSOFT_TENANT_ID | Part of the credentials for the mailer | +| MICROSOFT_CLIENT_ID | Part of the credentials for the mailer | +| MICROSOFT_CLIENT_SECRET | Part of the credentials for the mailer | diff --git a/kubernetes/manifests/dragonfly/mainframe/deployment.yaml b/kubernetes/manifests/dragonfly/mainframe/deployment.yaml new file mode 100644 index 0000000..ec0e196 --- /dev/null +++ b/kubernetes/manifests/dragonfly/mainframe/deployment.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: dragonfly + name: mainframe +spec: + replicas: 1 + selector: + matchLabels: + app: mainframe + template: + metadata: + labels: + app: mainframe + spec: + containers: + - name: mainframe + image: ghcr.io/vipyrsec/dragonfly-mainframe:edge + envFrom: + - secretRef: + name: dragonfly-mainframe-secrets + imagePullSecrets: + - name: ghcr-images diff --git a/kubernetes/manifests/dragonfly/mainframe/ingress.yaml b/kubernetes/manifests/dragonfly/mainframe/ingress.yaml new file mode 100644 index 0000000..549c265 --- /dev/null +++ b/kubernetes/manifests/dragonfly/mainframe/ingress.yaml @@ -0,0 +1,24 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + namespace: dragonfly + name: dragonfly-ingress + annotations: + cert-manager.io/cluster-issuer: "letsencrypt" +spec: + ingressClassName: nginx + tls: + - hosts: + - dragonfly.vipyrsec.com + secretName: dragonfly-tls + rules: + - host: dragonfly.vipyrsec.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: mainframe + port: + number: 8000 diff --git a/kubernetes/manifests/dragonfly/mainframe/service.yaml b/kubernetes/manifests/dragonfly/mainframe/service.yaml new file mode 100644 index 0000000..f79e3e7 --- /dev/null +++ b/kubernetes/manifests/dragonfly/mainframe/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + namespace: dragonfly + name: mainframe +spec: + selector: + app: mainframe + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 diff --git a/kubernetes/manifests/dragonfly/namespace.yaml b/kubernetes/manifests/dragonfly/namespace.yaml new file mode 100644 index 0000000..93d2947 --- /dev/null +++ b/kubernetes/manifests/dragonfly/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: dragonfly diff --git a/scripts/bootstrap-db.sql b/scripts/bootstrap-db.sql new file mode 100644 index 0000000..2ddb594 --- /dev/null +++ b/scripts/bootstrap-db.sql @@ -0,0 +1,14 @@ +-- Script to bootstrap the database with necessary roles and users + +-- Dragonfly +-- Create the database +CREATE DATABASE dragonfly OWNER dragonfly; +-- Create an admin role +CREATE ROLE dragonfly_admin; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO dragonfly_admin; +-- Create a read-only role +CREATE ROLE dragonfly_read; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO dragonfly_read; +-- Create initial user roles +CREATE ROLE bradley WITH PASSWORD 'shadow' IN ROLE dragonfly_admin LOGIN; +CREATE ROLE robin WITH PASSWORD 'shadow' IN ROLE dragonfly_read LOGIN; diff --git a/infrastructure/scripts/bootstrap-vps.sh b/scripts/bootstrap-vps.sh similarity index 93% rename from infrastructure/scripts/bootstrap-vps.sh rename to scripts/bootstrap-vps.sh index 492e3b0..34a3aa8 100644 --- a/infrastructure/scripts/bootstrap-vps.sh +++ b/scripts/bootstrap-vps.sh @@ -1,4 +1,5 @@ #!/bin/bash +# Script to bootstrap a new VPS with the necessary users and groups groupadd vipyrsec diff --git a/scripts/find_manifests.py b/scripts/find_manifests.py new file mode 100644 index 0000000..e31c83d --- /dev/null +++ b/scripts/find_manifests.py @@ -0,0 +1,19 @@ +"""Search for Kubneretes manifests in the kubernetes/manifests/ directory.""" + +from pathlib import Path + + +def run() -> None: + """Search for Kubneretes manifests in the kubernetes/manifests/ directory.""" + likely_manifests = [ + str(file) + for file in Path("./kubernetes/manifests/").glob("**/*.yaml") + if "apiVersion:" in file.read_text() # File is likely a k8s manifests + and not file.stem.startswith("_") # Ignore manifests that start with _ + ] + + print("\n".join(likely_manifests)) + + +if __name__ == "__main__": + run()