diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 0000000..742866b --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,287 @@ +name: Build and Publish Docker Image + +on: + push: + branches: [master, development] + tags: ["v*"] + pull_request: + branches: [master] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + TEST_TAG: beacon-node:test + +jobs: + lint: + name: Lint Dockerfile + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Run Hadolint + uses: hadolint/hadolint-action@v3.3.0 + with: + dockerfile: docker/Dockerfile + failure-threshold: error + + test: + name: Build & Validate Image + runs-on: ubuntu-latest + needs: lint + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build image for testing + uses: docker/build-push-action@v6 + with: + context: ./docker/ + load: true + tags: ${{ env.TEST_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Verify custom modules are installed + run: | + echo "--- Checking crypto_auth_provider.py ---" + docker run --rm --entrypoint="" ${{ env.TEST_TAG }} \ + python -c "import crypto_auth_provider; print('crypto_auth_provider OK')" + + echo "--- Checking beacon_info_module.py ---" + docker run --rm --entrypoint="" ${{ env.TEST_TAG }} \ + python -c "import beacon_info_module; print('beacon_info_module OK')" + + - name: Verify constants.py patch (max size 1048576) + run: | + docker run --rm --entrypoint="" ${{ env.TEST_TAG }} \ + python -c "from synapse.api.constants import EventContentFields; print('synapse.api.constants imported OK')" + docker run --rm --entrypoint="" ${{ env.TEST_TAG }} \ + grep -q '1048576' /usr/local/lib/python3.13/site-packages/synapse/api/constants.py \ + && echo "constants.py patch OK" \ + || (echo "FAIL: constants.py patch not applied" && exit 1) + + - name: Verify entrypoint script exists and is executable + run: | + docker run --rm --entrypoint="" ${{ env.TEST_TAG }} \ + test -x /usr/local/bin/synctl_entrypoint.sh \ + && echo "entrypoint OK" + + - name: Verify worker configs are present + run: | + for w in main_process worker1 worker2 worker3 worker4; do + docker run --rm --entrypoint="" ${{ env.TEST_TAG }} \ + test -f /config/workers/${w}.yaml \ + && echo "${w}.yaml OK" \ + || (echo "FAIL: ${w}.yaml missing" && exit 1) + done + + - name: Verify pip dependencies + run: | + docker run --rm --entrypoint="" ${{ env.TEST_TAG }} \ + python -c "import psycopg2; print('psycopg2 OK')" + docker run --rm --entrypoint="" ${{ env.TEST_TAG }} \ + python -c "import pysodium; print('pysodium OK')" + + - name: Verify config files are present + run: | + for f in homeserver.yaml synapse.log.config shared_config.yaml; do + docker run --rm --entrypoint="" ${{ env.TEST_TAG }} \ + test -f /config/${f} \ + && echo "/config/${f} OK" \ + || (echo "FAIL: /config/${f} missing" && exit 1) + done + + - name: Verify systemd units are present + run: | + for f in synapse_master.service synapse_worker@.service matrix_synapse.target; do + docker run --rm --entrypoint="" ${{ env.TEST_TAG }} \ + test -f /etc/systemd/system/${f} \ + && echo "${f} OK" \ + || (echo "FAIL: ${f} missing" && exit 1) + done + + - name: Verify wait-for.sh is executable + run: | + docker run --rm --entrypoint="" ${{ env.TEST_TAG }} \ + test -x /usr/local/bin/wait-for.sh \ + && echo "wait-for.sh OK" + + - name: Verify /keys directory exists + run: | + docker run --rm --entrypoint="" ${{ env.TEST_TAG }} \ + test -d /keys \ + && echo "/keys OK" + + smoke-test: + name: KIND Smoke Test + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build image + uses: docker/build-push-action@v6 + with: + context: ./docker/ + load: true + tags: ${{ env.TEST_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Set up Helm + uses: azure/setup-helm@v4 + + - name: Create KIND cluster + uses: helm/kind-action@v1 + + - name: Load image into KIND + run: kind load docker-image ${{ env.TEST_TAG }} --name chart-testing + + - name: Add subchart repos & build deps + run: | + helm repo add kubelauncher https://kubelauncher.github.io/charts + helm dependency build charts/ + + - name: Lint chart + run: helm lint charts/ + + - name: Generate signing key + run: | + SIGNING_KEY="ed25519 a_test $(openssl rand -base64 32)" + echo "SIGNING_KEY=${SIGNING_KEY}" >> "$GITHUB_ENV" + + - name: Install chart + run: | + helm install beacon-node charts/ \ + --set image.repository=beacon-node \ + --set image.tag=test \ + --set image.digest="" \ + --set image.pullPolicy=Never \ + --set serverName=beacon-node-test.local \ + --set "signingKey=${SIGNING_KEY}" \ + --set postgresql.enabled=true \ + --set postgresql.auth.password=testpass \ + --set postgresql.auth.postgresPassword=testpass \ + --set redis.enabled=true \ + --set ingress.enabled=false \ + --set workers.enabled=true \ + --wait \ + --timeout 300s + + - name: Verify pods are running + run: | + kubectl get pods -o wide + kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=beacon-node --timeout=120s + + - name: Check beacon-node logs for errors + run: | + POD=$(kubectl get pod -l app.kubernetes.io/name=beacon-node -o jsonpath='{.items[0].metadata.name}') + kubectl logs "$POD" --tail=50 + if kubectl logs "$POD" | grep -i "FATAL" | grep -v "reserved for"; then + echo "FATAL errors found in logs" + exit 1 + fi + echo "No FATAL errors in logs" + + - name: Smoke test Synapse endpoints + run: | + POD=$(kubectl get pod -l app.kubernetes.io/name=beacon-node -o jsonpath='{.items[0].metadata.name}') + + echo "--- /.well-known/matrix/server ---" + RESPONSE=$(kubectl exec "$POD" -- curl -sf http://localhost:8008/.well-known/matrix/server) + echo "$RESPONSE" + echo "$RESPONSE" | grep -q "m.server" || (echo "FAIL: missing m.server" && exit 1) + + echo "--- /.well-known/matrix/client ---" + RESPONSE=$(kubectl exec "$POD" -- curl -sf http://localhost:8008/.well-known/matrix/client) + echo "$RESPONSE" + echo "$RESPONSE" | grep -q "m.homeserver" || (echo "FAIL: missing m.homeserver" && exit 1) + + echo "--- /_matrix/client/versions ---" + RESPONSE=$(kubectl exec "$POD" -- curl -sf http://localhost:8008/_matrix/client/versions) + echo "$RESPONSE" + echo "$RESPONSE" | grep -q "versions" || (echo "FAIL: missing versions" && exit 1) + + echo "--- /_matrix/federation/v1/version ---" + RESPONSE=$(kubectl exec "$POD" -- curl -sf http://localhost:8008/_matrix/federation/v1/version) + echo "$RESPONSE" + echo "$RESPONSE" | grep -q "Synapse" || (echo "FAIL: unexpected version response" && exit 1) + + echo "--- /_matrix/client/v3/login ---" + RESPONSE=$(kubectl exec "$POD" -- curl -sf http://localhost:8008/_matrix/client/v3/login) + echo "$RESPONSE" + echo "$RESPONSE" | grep -q "m.login.password" || (echo "FAIL: login flow missing" && exit 1) + + echo "All smoke tests passed" + + - name: Debug on failure + if: failure() + run: | + echo "=== Pod status ===" + kubectl get pods -o wide + echo "=== Events ===" + kubectl get events --sort-by='.lastTimestamp' + echo "=== Beacon node logs ===" + POD=$(kubectl get pod -l app.kubernetes.io/name=beacon-node -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + [ -n "$POD" ] && kubectl logs "$POD" --tail=100 + echo "=== PostgreSQL logs ===" + PG_POD=$(kubectl get pod -l app.kubernetes.io/name=postgresql -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + [ -n "$PG_POD" ] && kubectl logs "$PG_POD" --tail=50 + + publish: + name: Publish to GHCR + runs-on: ubuntu-latest + needs: [test, smoke-test] + if: github.event_name != 'pull_request' + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./docker/ + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml new file mode 100644 index 0000000..eb80942 --- /dev/null +++ b/.github/workflows/security-scan.yml @@ -0,0 +1,61 @@ +name: Security Scan + +on: + push: + branches: [master] + paths: + - "docker/**" + - ".github/workflows/security-scan.yml" + pull_request: + branches: [master] + paths: + - "docker/**" + - ".github/workflows/security-scan.yml" + schedule: + - cron: "0 6 * * 1" # Weekly Monday 06:00 UTC + +env: + TEST_TAG: beacon-node:scan + +jobs: + trivy: + name: Trivy Image Scan + runs-on: ubuntu-latest + permissions: + security-events: write + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build image + uses: docker/build-push-action@v6 + with: + context: ./docker/ + load: true + tags: ${{ env.TEST_TAG }} + cache-from: type=gha + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.34.1 + with: + image-ref: ${{ env.TEST_TAG }} + format: "sarif" + output: "trivy-results.sarif" + severity: "CRITICAL,HIGH" + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v4 + if: always() + with: + sarif_file: "trivy-results.sarif" + + - name: Trivy summary (table) + uses: aquasecurity/trivy-action@0.34.1 + if: always() + with: + image-ref: ${{ env.TEST_TAG }} + format: "table" + severity: "CRITICAL,HIGH" diff --git a/.gitignore b/.gitignore index 4058889..559d3c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ data .vscode +charts/charts/ +charts/Chart.lock diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 8b01360..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,144 +0,0 @@ -include: - - project: 'papers/papers-internal/internal' - file: '/.base-gitlab-ci.yml' - -variables: - GIT_SUBMODULE_STRATEGY: recursive - DOCKERHUB_TAG: airgapdocker/beacon-node:$CI_COMMIT_SHA - DOCKERHUB_TAG_LATEST: airgapdocker/beacon-node:latest - -stages: - - build - - publish - - deploy - - provision - -build: - stage: build - script: - - docker build -t $DOCKERHUB_TAG ./docker/ - -.publish-base: - stage: publish - before_script: - - docker login -u "$DOCKERHUB_USER" -p "$DOCKERHUB_PASSWORD" - script: - - docker tag $DOCKERHUB_TAG $SECOND_TAG - - docker push $DOCKERHUB_TAG - - docker push $SECOND_TAG - -publish: - extends: .publish-base - variables: - SECOND_TAG: $DOCKERHUB_TAG_LATEST - only: - - master - - development - -publish-tag: - extends: .publish-base - variables: - SECOND_TAG: airgapdocker/beacon-node:$CI_COMMIT_TAG - only: - - tags - - -k8s-deploy-production: - stage: deploy - only: - - master - when: manual - image: google/cloud-sdk - before_script: - - echo $GCLOUD_GOOGLE_KEY > key.json - - gcloud auth activate-service-account $GCLOUD_ACCOUNT --key-file key.json - - gcloud config set account $GCLOUD_ACCOUNT - - gcloud config set project $GCLOUD_PROJECT - - gcloud config set compute/zone $GCLOUD_ZONE - - gcloud container clusters get-credentials papers-cluster-production - script: - - sed -i "s|_TO_BE_REPLACED_BY_IMAGE_TAG_|"$DOCKERHUB_TAG"|g" k8s/common/synapse/deployment.yaml - - sed -i "s|_TO_BE_REPLACED_BY_PROD_DB_HOST_|"$PROD_DB_HOST"|g" k8s/production/secret.yaml - - sed -i "s|_TO_BE_REPLACED_BY_PROD_DB_PASS_|"$PROD_DB_PASS"|g" k8s/production/secret.yaml - - sed -i "s|_TO_BE_REPLACED_BY_PROD_DB_NAME_|"$PROD_DB_NAME"|g" k8s/production/secret.yaml - - sed -i "s|_TO_BE_REPLACED_BY_PROD_DB_USER_|"$PROD_DB_USER"|g" k8s/production/secret.yaml - - sed -i "s|_TO_BE_REPLACED_BY_PROD_SIGNING_KEY_|$PROD_SIGNING_KEY|g" k8s/production/secret.yaml - - - kubectl apply -f ./k8s/common/namespace.yaml - - kubectl apply -f ./k8s/common/ --recursive - - kubectl apply -f ./k8s/production/ --recursive - - tags: - - docker - -k8s-deploy-development: - stage: deploy - only: - - master - when: manual - image: google/cloud-sdk - before_script: - - echo $GCLOUD_GOOGLE_KEY > key.json - - gcloud auth activate-service-account $GCLOUD_ACCOUNT --key-file key.json - - gcloud config set account $GCLOUD_ACCOUNT - - gcloud config set project $GCLOUD_PROJECT - - gcloud config set compute/zone $GCLOUD_ZONE_DEVELOPMENT - - gcloud container clusters get-credentials papers-cluster-development - script: - - sed -i "s|_TO_BE_REPLACED_BY_IMAGE_TAG_|"$DOCKERHUB_TAG"|g" k8s/common/synapse/deployment.yaml - - sed -i "s|_TO_BE_REPLACED_BY_DEV_DB_HOST_|"$DEV_DB_HOST"|g" k8s/development/secret.yaml - - sed -i "s|_TO_BE_REPLACED_BY_DEV_DB_PASS_|"$DEV_DB_PASS"|g" k8s/development/secret.yaml - - sed -i "s|_TO_BE_REPLACED_BY_DEV_DB_NAME_|"$DEV_DB_NAME"|g" k8s/development/secret.yaml - - sed -i "s|_TO_BE_REPLACED_BY_DEV_DB_USER_|"$DEV_DB_USER"|g" k8s/development/secret.yaml - - sed -i "s|_TO_BE_REPLACED_BY_DEV_SIGNING_KEY_|$DEV_SIGNING_KEY|g" k8s/development/secret.yaml - - - kubectl apply -f ./k8s/common/namespace.yaml - - kubectl apply -f ./k8s/common/ --recursive - - kubectl apply -f ./k8s/development/ --recursive - - tags: - - docker - -provision-db-development: - stage: provision - only: - - develop - - master - when: manual - image: google/cloud-sdk - before_script: - - echo $GCLOUD_GOOGLE_KEY > key.json - - gcloud auth activate-service-account $GCLOUD_ACCOUNT --key-file key.json - - gcloud config set account $GCLOUD_ACCOUNT - - gcloud config set project $GCLOUD_PROJECT - - gcloud config set compute/zone $GCLOUD_ZONE_DEVELOPMENT - - gcloud container clusters get-credentials papers-cluster-development - - export STOLON_PROXY=$(kubectl get pods --all-namespaces | grep stolon-proxy | awk 'NR==1{print $2}') - script: - - kubectl exec $STOLON_PROXY --namespace="development-postgresql" -- bash -c "export PGPASSWORD=$PG_DEV_MASTER_PASSWORD && psql --host=localhost --username=$PG_DEV_MASTER_USERNAME postgres -c \"CREATE DATABASE $DB_NAME\"" || true - - kubectl exec $STOLON_PROXY --namespace="development-postgresql" -- bash -c "export PGPASSWORD=$PG_DEV_MASTER_PASSWORD && psql --host=localhost --username=$PG_DEV_MASTER_USERNAME postgres -c \"CREATE USER $DB_USER WITH ENCRYPTED PASSWORD '$DB_PASS'\"" - - kubectl exec $STOLON_PROXY --namespace="development-postgresql" -- bash -c "export PGPASSWORD=$PG_DEV_MASTER_PASSWORD && psql --host=localhost --username=$PG_DEV_MASTER_USERNAME postgres -c \"GRANT ALL PRIVILEGES ON DATABASE $DB_NAME to $DB_USER\"" - tags: - - docker - -provision-db-production: - stage: provision - only: - - develop - - master - when: manual - image: google/cloud-sdk - before_script: - - echo $GCLOUD_GOOGLE_KEY > key.json - - gcloud auth activate-service-account $GCLOUD_ACCOUNT --key-file key.json - - gcloud config set account $GCLOUD_ACCOUNT - - gcloud config set project $GCLOUD_PROJECT - - gcloud config set compute/zone $GCLOUD_ZONE - - gcloud container clusters get-credentials papers-cluster-production - - export STOLON_PROXY=$(kubectl get pods --all-namespaces | grep stolon-proxy | awk 'NR==1{print $2}') - script: - - kubectl exec $STOLON_PROXY -- bash -c "export PGPASSWORD=$PG_PROD_MASTER_PASSWORD && psql --host=localhost --username=$PG_PROD_MASTER_USERNAME postgres -c \"CREATE DATABASE $PROD_DB_NAME\"" || true - - kubectl exec $STOLON_PROXY -- bash -c "export PGPASSWORD=$PG_PROD_MASTER_PASSWORD && psql --host=localhost --username=$PG_PROD_MASTER_USERNAME postgres -c \"CREATE USER $PROD_DB_USER WITH ENCRYPTED PASSWORD '$PROD_DB_PASS'\"" || true - - kubectl exec $STOLON_PROXY -- bash -c "export PGPASSWORD=$PG_PROD_MASTER_PASSWORD && psql --host=localhost --username=$PG_PROD_MASTER_USERNAME postgres -c \"GRANT ALL PRIVILEGES ON DATABASE $PROD_DB_NAME to $PROD_DB_USER\"" || true - tags: - - docker diff --git a/.gitlab/issue_templates/.gitkeep b/.gitlab/issue_templates/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md deleted file mode 100644 index 7f6f091..0000000 --- a/.gitlab/issue_templates/Bug.md +++ /dev/null @@ -1,39 +0,0 @@ - - -## Summary - - - -## Steps to reproduce - - -## What is the current _bug_ behavior? - - -## What is the expected _correct_ behavior? - - -## Relevant logs, screenshots and/or links - - -## Possible fixes / approach - - -## Additional information - - ----- - -/estimate - - -/label ~"type::bug" ~"proj::beacon" - - diff --git a/.gitlab/issue_templates/Epic.md b/.gitlab/issue_templates/Epic.md deleted file mode 100644 index 67831b9..0000000 --- a/.gitlab/issue_templates/Epic.md +++ /dev/null @@ -1,30 +0,0 @@ - - -## Summary - - - - -## Which issues need to be completed - - - - - - - -/label ~2141 ~"proj::beacon" - - - - diff --git a/.gitlab/issue_templates/Feature.md b/.gitlab/issue_templates/Feature.md deleted file mode 100644 index 70b8732..0000000 --- a/.gitlab/issue_templates/Feature.md +++ /dev/null @@ -1,43 +0,0 @@ - - -## Summary - - - - -## What is the expected behavior? - - - - -## Relevant Mockups, Screenshots and/or links - - - - -## Possible fixes / approach - - - - -## Additional information - - - - - - - -/estimate - - - - -/label ~"type::feature" ~"proj::beacon" - diff --git a/.gitlab/issue_templates/Task.md b/.gitlab/issue_templates/Task.md deleted file mode 100644 index 8952ffe..0000000 --- a/.gitlab/issue_templates/Task.md +++ /dev/null @@ -1,39 +0,0 @@ - - -## Summary - - - - -## What steps need to be done? - - - - -## Relevant Mockups, Screenshots and/or links - - - - -## Additional information - - - - - - - -/estimate - - - - -/label ~"type::task" ~"proj::beacon" - - diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 0000000..d4918af --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,3 @@ +ignored: + - DL3008 # apt pin versions - managed by base image + - DL3013 # pip pin versions - managed by base image diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..90ffe8c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Anthony Pham + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/charts/Chart.yaml b/charts/Chart.yaml new file mode 100644 index 0000000..e752449 --- /dev/null +++ b/charts/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v2 +name: beacon-node +description: A Helm chart for deploying Beacon Node (Matrix Synapse fork with crypto authentication) +type: application +version: 0.4.1 +appVersion: "1.148.0" + +keywords: + - matrix + - synapse + - beacon + - airgap + - crypto-auth + +home: https://github.com/apham0001/beacon-node +sources: + - https://github.com/apham0001/beacon-node + +dependencies: + - name: postgresql + version: "0.2.8" + repository: https://kubelauncher.github.io/charts + condition: postgresql.enabled + - name: redis + version: "0.2.15" + repository: https://kubelauncher.github.io/charts + condition: redis.enabled diff --git a/charts/README.md b/charts/README.md new file mode 100644 index 0000000..c9487d8 --- /dev/null +++ b/charts/README.md @@ -0,0 +1,103 @@ +# beacon-node + +![Version: 0.4.1](https://img.shields.io/badge/Version-0.4.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.148.0](https://img.shields.io/badge/AppVersion-1.148.0-informational?style=flat-square) + +A Helm chart for deploying Beacon Node (Matrix Synapse fork with crypto authentication) + +**Homepage:** + +## Source Code + +* + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| https://kubelauncher.github.io/charts | postgresql | 0.2.8 | +| https://kubelauncher.github.io/charts | redis | 0.2.15 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | Affinity rules for pod scheduling | +| existingSigningKeySecret | object | `{"key":"SIGNING_KEY","name":""}` | Use an existing secret for the signing key instead of creating one If set, signingKey value is ignored | +| existingSigningKeySecret.key | string | `"SIGNING_KEY"` | Key in the secret that contains the signing key value | +| existingSigningKeySecret.name | string | `""` | Name of the existing secret containing the signing key | +| externalDatabase.database | string | `"synapse"` | External PostgreSQL database name | +| externalDatabase.host | string | `""` | External PostgreSQL host | +| externalDatabase.password | string | `""` | External PostgreSQL password | +| externalDatabase.port | int | `5432` | External PostgreSQL port | +| externalDatabase.username | string | `"synapse"` | External PostgreSQL username | +| fullnameOverride | string | `""` | Override the full release name | +| image.digest | string | `""` | Image digest (optional, for pinning exact image version) Example: "sha256:abc123..." | +| image.pullPolicy | string | `"Always"` | Image pull policy | +| image.repository | string | `"ghcr.io/apham0001/beacon-node"` | Container image repository | +| image.tag | string | `"latest"` | Image tag | +| imagePullSecrets | list | `[]` | Image pull secrets for private registries | +| ingress.annotations | object | `{"cert-manager.io/cluster-issuer":"letsencrypt-prod","nginx.ingress.kubernetes.io/proxy-connect-timeout":"60","nginx.ingress.kubernetes.io/proxy-read-timeout":"300","nginx.ingress.kubernetes.io/proxy-send-timeout":"300"}` | Ingress annotations | +| ingress.className | string | `"nginx"` | Ingress class name | +| ingress.enabled | bool | `false` | Enable ingress | +| ingress.hosts | list | `[{"host":"matrix.example.com","paths":[{"path":"/","pathType":"Prefix"}]}]` | Ingress hosts configuration | +| ingress.tls | list | `[{"hosts":["matrix.example.com"],"secretName":"beacon-node-tls"}]` | Ingress TLS configuration | +| livenessProbe.failureThreshold | int | `3` | | +| livenessProbe.httpGet.path | string | `"/health"` | | +| livenessProbe.httpGet.port | string | `"http"` | | +| livenessProbe.initialDelaySeconds | int | `30` | | +| livenessProbe.periodSeconds | int | `10` | | +| livenessProbe.timeoutSeconds | int | `5` | | +| monitoring.enabled | bool | `false` | Enable ServiceMonitor for Prometheus scraping | +| monitoring.interval | string | `"30s"` | Scrape interval | +| monitoring.path | string | `"/_synapse/metrics"` | Metrics endpoint path | +| monitoring.prometheusRelease | string | `"prometheus-stack"` | Prometheus release label for service discovery | +| monitoring.scrapeTimeout | string | `"10s"` | Scrape timeout | +| nameOverride | string | `""` | Override the chart name | +| nodeSelector | object | `{}` | Node selector for pod scheduling | +| persistence.accessModes | list | `["ReadWriteOnce"]` | Access modes | +| persistence.enabled | bool | `true` | Enable persistence for Synapse data | +| persistence.size | string | `"5Gi"` | Storage size | +| persistence.storageClass | string | `""` | Storage class (leave empty for default) | +| podAnnotations | object | `{}` | Pod annotations | +| podSecurityContext | object | `{}` | Pod security context | +| postgresql.auth.database | string | `"synapse"` | PostgreSQL database name | +| postgresql.auth.password | string | `""` | PostgreSQL password (REQUIRED if postgresql.enabled=true) | +| postgresql.auth.username | string | `"synapse"` | PostgreSQL username | +| postgresql.enabled | bool | `true` | Deploy PostgreSQL as a subchart | +| postgresql.primary.configuration | string | `"max_connections = 300\n"` | PostgreSQL configuration Synapse workers use cp_min=20, cp_max=80 connections per process With 4 workers + 1 main: 5 * 80 + buffer = 300 | +| postgresql.primary.initdb | object | `{"args":"--encoding=UTF8 --lc-collate=C --lc-ctype=C"}` | initdb configuration (IMPORTANT: Synapse requires C locale) | +| postgresql.primary.persistence.enabled | bool | `true` | Enable persistence for PostgreSQL | +| postgresql.primary.persistence.size | string | `"10Gi"` | Storage size for PostgreSQL | +| postgresql.primary.resources | object | `{"limits":{"cpu":"500m","memory":"1Gi"},"requests":{"cpu":"100m","memory":"256Mi"}}` | PostgreSQL resource limits | +| readinessProbe.failureThreshold | int | `3` | | +| readinessProbe.httpGet.path | string | `"/health"` | | +| readinessProbe.httpGet.port | string | `"http"` | | +| readinessProbe.initialDelaySeconds | int | `10` | | +| readinessProbe.periodSeconds | int | `5` | | +| readinessProbe.timeoutSeconds | int | `3` | | +| redis.architecture | string | `"standalone"` | Redis architecture (standalone or replication) | +| redis.auth.enabled | bool | `false` | Disable Redis authentication (Synapse default) | +| redis.enabled | bool | `true` | Deploy Redis as a subchart (required for workers) | +| redis.master.persistence.enabled | bool | `true` | Enable persistence for Redis | +| redis.master.persistence.size | string | `"1Gi"` | Storage size for Redis | +| redis.master.resources | object | `{"limits":{"cpu":"200m","memory":"256Mi"},"requests":{"cpu":"50m","memory":"64Mi"}}` | Redis resource limits | +| registrationSharedSecret | string | `""` | Shared secret for user registration (optional) | +| replicaCount | int | `1` | Number of replicas (only 1 supported due to Synapse architecture) | +| resources.limits.cpu | string | `"1000m"` | | +| resources.limits.memory | string | `"2Gi"` | | +| resources.requests.cpu | string | `"250m"` | | +| resources.requests.memory | string | `"512Mi"` | | +| securityContext | object | `{}` | Container security context | +| serverName | string | `"matrix.example.com"` | Matrix server name (FQDN). This MUST match your domain. Example: matrix.example.com | +| serverRegion | string | `"EU"` | Geographic region for beacon info endpoint | +| service.port | int | `8008` | Main client/federation port | +| service.type | string | `"ClusterIP"` | Service type (ClusterIP, NodePort, LoadBalancer) | +| serviceAccount.annotations | object | `{}` | Service account annotations | +| serviceAccount.create | bool | `true` | Create a service account | +| serviceAccount.name | string | `""` | Service account name (generated if not set) | +| signingKey | string | `""` | Ed25519 signing key for Matrix federation Format: "ed25519 a_XXXX " IMPORTANT: The base64-encoded seed MUST decode to exactly 32 bytes! Generate with: python3 -c "import base64,os; print(f'ed25519 a_{os.urandom(2).hex()} {base64.b64encode(os.urandom(32)).decode()}')" If not provided, a deterministic key is auto-generated based on release name. WARNING: Auto-generated keys are NOT secure for production - always provide your own key! | +| tolerations | list | `[]` | Tolerations for pod scheduling | +| workers.enabled | bool | `true` | Enable Synapse workers (4 generic workers on ports 8083-8086) Worker count is fixed at 4 in the Docker image (hardcoded worker configs) | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/charts/templates/NOTES.txt b/charts/templates/NOTES.txt new file mode 100644 index 0000000..eab6913 --- /dev/null +++ b/charts/templates/NOTES.txt @@ -0,0 +1,74 @@ +=============================================================================== + Beacon Node has been deployed! +=============================================================================== + +Server Name: {{ .Values.serverName }} +Server Region: {{ .Values.serverRegion }} + +{{- if .Values.ingress.enabled }} + +Your beacon node is accessible at: +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + https://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} + +{{- else }} + +To access your beacon node, run: + + kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "beacon-node.fullname" . }} 8008:{{ .Values.service.port }} + +Then open: http://localhost:8008 + +{{- end }} + +To verify the deployment, check the beacon info endpoint: + +{{- if .Values.ingress.enabled }} + curl https://{{ (index .Values.ingress.hosts 0).host }}/_synapse/client/beacon/info +{{- else }} + # After port-forwarding: + curl http://localhost:8008/_synapse/client/beacon/info +{{- end }} + +Expected response: + {"region": "{{ .Values.serverRegion }}", "known_servers": [...], "timestamp": ...} + +=============================================================================== + Important Notes +=============================================================================== + +{{- if not .Values.signingKey }} + +WARNING: No signing key was provided! +The deployment may fail without a valid Ed25519 signing key. + +Generate one with: + docker run --rm matrixdotorg/synapse generate + +Then upgrade the release: + helm upgrade {{ .Release.Name }} beacon-node --set signingKey="ed25519 a_XXXX " + +{{- end }} + +{{- if .Values.postgresql.enabled }} +{{- if not .Values.postgresql.auth.password }} + +WARNING: No PostgreSQL password was provided! +Please set postgresql.auth.password for production deployments. + +{{- end }} +{{- end }} + +Database: {{ include "beacon-node.postgresql.host" . }}:5432/{{ include "beacon-node.postgresql.database" . }} +Redis: {{ include "beacon-node.redis.host" . }}:6379 + +{{- if .Values.workers.enabled }} + +Workers: 4 generic workers enabled (ports 8083-8086) + +{{- end }} + +For more information, visit: https://github.com/apham0001/beacon-node diff --git a/charts/templates/_helpers.tpl b/charts/templates/_helpers.tpl new file mode 100644 index 0000000..01628b8 --- /dev/null +++ b/charts/templates/_helpers.tpl @@ -0,0 +1,135 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "beacon-node.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "beacon-node.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "beacon-node.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "beacon-node.labels" -}} +helm.sh/chart: {{ include "beacon-node.chart" . }} +{{ include "beacon-node.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "beacon-node.selectorLabels" -}} +app.kubernetes.io/name: {{ include "beacon-node.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "beacon-node.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "beacon-node.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Get the PostgreSQL host +*/}} +{{- define "beacon-node.postgresql.host" -}} +{{- if .Values.postgresql.enabled }} +{{- printf "%s-postgresql" .Release.Name }} +{{- else }} +{{- .Values.externalDatabase.host }} +{{- end }} +{{- end }} + +{{/* +Get the PostgreSQL port +*/}} +{{- define "beacon-node.postgresql.port" -}} +{{- if .Values.postgresql.enabled }} +{{- printf "5432" }} +{{- else }} +{{- .Values.externalDatabase.port | default 5432 }} +{{- end }} +{{- end }} + +{{/* +Get the PostgreSQL username +*/}} +{{- define "beacon-node.postgresql.username" -}} +{{- if .Values.postgresql.enabled }} +{{- .Values.postgresql.auth.username }} +{{- else }} +{{- .Values.externalDatabase.username }} +{{- end }} +{{- end }} + +{{/* +Get the PostgreSQL password secret name +*/}} +{{- define "beacon-node.postgresql.secretName" -}} +{{- if .Values.postgresql.enabled }} +{{- printf "%s-postgresql" .Release.Name }} +{{- else }} +{{- include "beacon-node.fullname" . }} +{{- end }} +{{- end }} + +{{/* +Get the PostgreSQL password secret key +*/}} +{{- define "beacon-node.postgresql.secretKey" -}} +{{- if .Values.postgresql.enabled }} +{{- printf "password" }} +{{- else }} +{{- printf "db-password" }} +{{- end }} +{{- end }} + +{{/* +Get the PostgreSQL database name +*/}} +{{- define "beacon-node.postgresql.database" -}} +{{- if .Values.postgresql.enabled }} +{{- .Values.postgresql.auth.database }} +{{- else }} +{{- .Values.externalDatabase.database }} +{{- end }} +{{- end }} + +{{/* +Get the Redis host +*/}} +{{- define "beacon-node.redis.host" -}} +{{- printf "%s-redis" .Release.Name }} +{{- end }} diff --git a/charts/templates/configmap.yaml b/charts/templates/configmap.yaml new file mode 100644 index 0000000..e545e7f --- /dev/null +++ b/charts/templates/configmap.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "beacon-node.fullname" . }} + labels: + {{- include "beacon-node.labels" . | nindent 4 }} +data: + SERVER_NAME: {{ .Values.serverName | quote }} + SERVER_REGION: {{ .Values.serverRegion | quote }} + DB_HOST: {{ include "beacon-node.postgresql.host" . | quote }} + DB_NAME: {{ include "beacon-node.postgresql.database" . | quote }} + DB_USER: {{ include "beacon-node.postgresql.username" . | quote }} + REDIS_HOST: {{ include "beacon-node.redis.host" . | quote }} diff --git a/charts/templates/deployment.yaml b/charts/templates/deployment.yaml new file mode 100644 index 0000000..07a1f44 --- /dev/null +++ b/charts/templates/deployment.yaml @@ -0,0 +1,118 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "beacon-node.fullname" . }} + labels: + {{- include "beacon-node.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: Recreate + selector: + matchLabels: + {{- include "beacon-node.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "beacon-node.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "beacon-node.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.image.digest }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}@{{ .Values.image.digest }}" + {{- else }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + {{- end }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + envFrom: + - configMapRef: + name: {{ include "beacon-node.fullname" . }} + - secretRef: + name: {{ include "beacon-node.fullname" . }} + env: + {{- if and .Values.existingSigningKeySecret .Values.existingSigningKeySecret.name }} + # Signing key from existing secret (e.g., ExternalSecret) + - name: SIGNING_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.existingSigningKeySecret.name }} + key: {{ .Values.existingSigningKeySecret.key | default "SIGNING_KEY" }} + {{- end }} + # Database password from appropriate secret + - name: DB_PASS + valueFrom: + secretKeyRef: + name: {{ include "beacon-node.postgresql.secretName" . }} + key: {{ include "beacon-node.postgresql.secretKey" . }} + ports: + - name: http + containerPort: 8008 + protocol: TCP + {{- if .Values.workers.enabled }} + - name: worker-1 + containerPort: 8083 + protocol: TCP + - name: worker-2 + containerPort: 8084 + protocol: TCP + - name: worker-3 + containerPort: 8085 + protocol: TCP + - name: worker-4 + containerPort: 8086 + protocol: TCP + {{- end }} + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: data + mountPath: /data + volumes: + - name: data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "beacon-node.fullname" . }} + {{- else }} + emptyDir: {} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/templates/ingress.yaml b/charts/templates/ingress.yaml new file mode 100644 index 0000000..818769e --- /dev/null +++ b/charts/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "beacon-node.fullname" . }} + labels: + {{- include "beacon-node.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "beacon-node.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/templates/pvc.yaml b/charts/templates/pvc.yaml new file mode 100644 index 0000000..2acccf6 --- /dev/null +++ b/charts/templates/pvc.yaml @@ -0,0 +1,19 @@ +{{- if .Values.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "beacon-node.fullname" . }} + labels: + {{- include "beacon-node.labels" . | nindent 4 }} +spec: + accessModes: + {{- range .Values.persistence.accessModes }} + - {{ . | quote }} + {{- end }} + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} +{{- end }} diff --git a/charts/templates/secret.yaml b/charts/templates/secret.yaml new file mode 100644 index 0000000..01da1ec --- /dev/null +++ b/charts/templates/secret.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "beacon-node.fullname" . }} + labels: + {{- include "beacon-node.labels" . | nindent 4 }} +type: Opaque +stringData: + {{- if or .Values.signingKey (not (and .Values.existingSigningKeySecret .Values.existingSigningKeySecret.name)) }} + # Signing key is required for Matrix federation + # If not provided, a deterministic key is generated based on release name (NOT SECURE FOR PRODUCTION) + SIGNING_KEY: {{ .Values.signingKey | default (printf "ed25519 a_%s %s" (substr 0 4 (sha256sum .Release.Name)) (b64enc (substr 0 32 (sha256sum (printf "%s-signing-key" .Release.Name))))) | quote }} + {{- end }} + {{- if .Values.registrationSharedSecret }} + REGISTRATION_SHARED_SECRET: {{ .Values.registrationSharedSecret | quote }} + {{- end }} + {{- if not .Values.postgresql.enabled }} + db-password: {{ .Values.externalDatabase.password | quote }} + {{- end }} diff --git a/charts/templates/service.yaml b/charts/templates/service.yaml new file mode 100644 index 0000000..7942456 --- /dev/null +++ b/charts/templates/service.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "beacon-node.fullname" . }} + labels: + {{- include "beacon-node.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + {{- if .Values.workers.enabled }} + - port: 8083 + targetPort: 8083 + protocol: TCP + name: worker-1 + - port: 8084 + targetPort: 8084 + protocol: TCP + name: worker-2 + - port: 8085 + targetPort: 8085 + protocol: TCP + name: worker-3 + - port: 8086 + targetPort: 8086 + protocol: TCP + name: worker-4 + {{- end }} + selector: + {{- include "beacon-node.selectorLabels" . | nindent 4 }} diff --git a/charts/templates/serviceaccount.yaml b/charts/templates/serviceaccount.yaml new file mode 100644 index 0000000..b54d2f3 --- /dev/null +++ b/charts/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "beacon-node.serviceAccountName" . }} + labels: + {{- include "beacon-node.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/templates/servicemonitor.yaml b/charts/templates/servicemonitor.yaml new file mode 100644 index 0000000..a4bbcb6 --- /dev/null +++ b/charts/templates/servicemonitor.yaml @@ -0,0 +1,21 @@ +{{- if .Values.monitoring.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "beacon-node.fullname" . }} + labels: + {{- include "beacon-node.labels" . | nindent 4 }} + {{- if .Values.monitoring.prometheusRelease }} + release: {{ .Values.monitoring.prometheusRelease }} + {{- end }} +spec: + jobLabel: app.kubernetes.io/name + selector: + matchLabels: + {{- include "beacon-node.selectorLabels" . | nindent 6 }} + endpoints: + - port: http + path: {{ .Values.monitoring.path }} + interval: {{ .Values.monitoring.interval }} + scrapeTimeout: {{ .Values.monitoring.scrapeTimeout }} +{{- end }} diff --git a/charts/values.yaml b/charts/values.yaml new file mode 100644 index 0000000..3b9eedd --- /dev/null +++ b/charts/values.yaml @@ -0,0 +1,281 @@ +# Beacon Node Helm Chart Values +# ============================== + +# -- Number of replicas (only 1 supported due to Synapse architecture) +replicaCount: 1 + +image: + # -- Container image repository + repository: ghcr.io/apham0001/beacon-node + # -- Image pull policy + pullPolicy: Always + # -- Image tag + tag: "latest" + # -- Image digest (optional, for pinning exact image version) + # Example: "sha256:abc123..." + digest: "" + +# -- Image pull secrets for private registries +imagePullSecrets: [] + +# -- Override the chart name +nameOverride: "" +# -- Override the full release name +fullnameOverride: "" + +# ============================================================================= +# Beacon Node Configuration +# ============================================================================= + +# -- Matrix server name (FQDN). This MUST match your domain. +# Example: matrix.example.com +serverName: "matrix.example.com" + +# -- Geographic region for beacon info endpoint +serverRegion: "EU" + +# -- Ed25519 signing key for Matrix federation +# Format: "ed25519 a_XXXX " +# IMPORTANT: The base64-encoded seed MUST decode to exactly 32 bytes! +# Generate with: +# python3 -c "import base64,os; print(f'ed25519 a_{os.urandom(2).hex()} {base64.b64encode(os.urandom(32)).decode()}')" +# If not provided, a deterministic key is auto-generated based on release name. +# WARNING: Auto-generated keys are NOT secure for production - always provide your own key! +signingKey: "" + +# -- Use an existing secret for the signing key instead of creating one +# If set, signingKey value is ignored +existingSigningKeySecret: + # -- Name of the existing secret containing the signing key + name: "" + # -- Key in the secret that contains the signing key value + key: "SIGNING_KEY" + +# -- Shared secret for user registration (optional) +registrationSharedSecret: "" + +# ============================================================================= +# Workers Configuration +# ============================================================================= + +workers: + # -- Enable Synapse workers (4 generic workers on ports 8083-8086) + # Worker count is fixed at 4 in the Docker image (hardcoded worker configs) + enabled: true + +# ============================================================================= +# PostgreSQL Configuration (kubelauncher subchart) +# ============================================================================= + +postgresql: + # -- Deploy PostgreSQL as a subchart + enabled: true + auth: + # -- PostgreSQL username + username: synapse + # -- PostgreSQL password (REQUIRED if postgresql.enabled=true) + password: "" + # -- PostgreSQL database name + database: synapse + primary: + # -- initdb configuration (IMPORTANT: Synapse requires C locale) + initdb: + args: "--encoding=UTF8 --lc-collate=C --lc-ctype=C" + # -- PostgreSQL configuration + # Synapse workers use cp_min=20, cp_max=80 connections per process + # With 4 workers + 1 main: 5 * 80 + buffer = 300 + configuration: | + max_connections = 300 + persistence: + # -- Enable persistence for PostgreSQL + enabled: true + # -- Storage size for PostgreSQL + size: 10Gi + # -- PostgreSQL resource limits + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 1Gi + cpu: 500m + +# ============================================================================= +# External Database Configuration (when postgresql.enabled=false) +# ============================================================================= + +externalDatabase: + # -- External PostgreSQL host + host: "" + # -- External PostgreSQL port + port: 5432 + # -- External PostgreSQL username + username: synapse + # -- External PostgreSQL password + password: "" + # -- External PostgreSQL database name + database: synapse + +# ============================================================================= +# Redis Configuration (kubelauncher subchart) +# ============================================================================= + +redis: + # -- Deploy Redis as a subchart (required for workers) + enabled: true + # -- Redis architecture (standalone or replication) + architecture: standalone + auth: + # -- Disable Redis authentication (Synapse default) + enabled: false + master: + persistence: + # -- Enable persistence for Redis + enabled: true + # -- Storage size for Redis + size: 1Gi + # -- Redis resource limits + resources: + requests: + memory: 64Mi + cpu: 50m + limits: + memory: 256Mi + cpu: 200m + +# ============================================================================= +# Monitoring Configuration +# ============================================================================= + +monitoring: + # -- Enable ServiceMonitor for Prometheus scraping + enabled: false + # -- Prometheus release label for service discovery + prometheusRelease: prometheus-stack + # -- Metrics endpoint path + path: /_synapse/metrics + # -- Scrape interval + interval: 30s + # -- Scrape timeout + scrapeTimeout: 10s + +# ============================================================================= +# Service Configuration +# ============================================================================= + +service: + # -- Service type (ClusterIP, NodePort, LoadBalancer) + type: ClusterIP + # -- Main client/federation port + port: 8008 + +# ============================================================================= +# Ingress Configuration +# ============================================================================= + +ingress: + # -- Enable ingress + enabled: false + # -- Ingress class name + className: "nginx" + # -- Ingress annotations + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + # Federation requests can take longer, increase timeouts + nginx.ingress.kubernetes.io/proxy-read-timeout: "300" + nginx.ingress.kubernetes.io/proxy-send-timeout: "300" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "60" + # -- Ingress hosts configuration + hosts: + - host: matrix.example.com + paths: + - path: / + pathType: Prefix + # -- Ingress TLS configuration + tls: + - secretName: beacon-node-tls + hosts: + - matrix.example.com + +# ============================================================================= +# Resources Configuration +# ============================================================================= + +resources: + requests: + memory: 512Mi + cpu: 250m + limits: + memory: 2Gi + cpu: 1000m + +# ============================================================================= +# Pod Configuration +# ============================================================================= + +# -- Node selector for pod scheduling +nodeSelector: {} + +# -- Tolerations for pod scheduling +tolerations: [] + +# -- Affinity rules for pod scheduling +affinity: {} + +# -- Pod annotations +podAnnotations: {} + +# -- Pod security context +podSecurityContext: {} + +# -- Container security context +securityContext: {} + +# ============================================================================= +# Probes Configuration +# ============================================================================= + +livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + +readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + +# ============================================================================= +# Persistence Configuration +# ============================================================================= + +persistence: + # -- Enable persistence for Synapse data + enabled: true + # -- Storage class (leave empty for default) + storageClass: "" + # -- Access modes + accessModes: + - ReadWriteOnce + # -- Storage size + size: 5Gi + +# ============================================================================= +# Service Account Configuration +# ============================================================================= + +serviceAccount: + # -- Create a service account + create: true + # -- Service account annotations + annotations: {} + # -- Service account name (generated if not set) + name: "" diff --git a/samples/docker-compose.yml b/docker-compose/docker-compose.yml similarity index 50% rename from samples/docker-compose.yml rename to docker-compose/docker-compose.yml index 60628b8..efae9df 100644 --- a/samples/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -1,8 +1,6 @@ -version: "3.5" - services: postgres: - image: postgres:12.0 + image: postgres:18-bookworm environment: POSTGRES_USER: synapse POSTGRES_PASSWORD: synapsepassword @@ -15,15 +13,16 @@ services: ENCODING: "UTF8" POSTGRES_INITDB_ARGS: "-E UTF8" volumes: - - ./postgres_data:/var/lib/postgresql/data + - ./postgres_data:/var/lib/postgresql redis: - image: redis:6.2.4-buster - expose: + image: redis:8-bookworm + expose: - "6379" tezos-synapse: - image: beacon-node + image: ghcr.io/apham0001/beacon-node:latest depends_on: - postgres + - redis ports: - "8008:8008" # 8008 is exposed. Route all traffic from port 443 and 8448 to this port - "8083:8083" # exposing the workers for loadbalancing @@ -35,9 +34,13 @@ services: DB_USER: synapse DB_PASS: synapsepassword DB_NAME: synapse - SERVER_NAME: matrix.papers.tech - SIGNING_KEY: "dummy signing key" + SERVER_NAME: localhost SERVER_REGION: EU - + REDIS_HOST: redis + # Generate a real signing key with: + # docker run --rm --entrypoint="" ghcr.io/apham0001/beacon-node:latest \ + # python -m synapse.app.homeserver --generate-keys --config-path /dev/null 2>/dev/null && cat /config/signing.key + # Or use: python -c "import signedjson.key; import sys; key=signedjson.key.generate_signing_key('0'); signedjson.key.write_signing_keys(sys.stdout, [key])" + SIGNING_KEY: "ed25519 a_fake AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" volumes: - - ./data/synapse-config:/data \ No newline at end of file + - ./data/synapse-config:/data diff --git a/docker/Dockerfile b/docker/Dockerfile index eae853d..8399d77 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,14 +1,21 @@ -FROM matrixdotorg/synapse:v1.98.0 -LABEL maintainer="AirGap Team " +FROM ghcr.io/element-hq/synapse:v1.148.0 AS builder -# RUN apk add libsodium-dev gcc -RUN apt-get update && apt-get install -y libsodium-dev gcc +RUN apt-get update && \ + apt-get install -y --no-install-recommends libsodium-dev gcc && \ + pip install --no-cache-dir psycopg2 pysodium && \ + rm -rf /var/lib/apt/lists/* -RUN pip install psycopg2 pysodium +FROM ghcr.io/element-hq/synapse:v1.148.0 -RUN mkdir -p /keys +RUN apt-get update && \ + apt-get install -y --no-install-recommends libsodium23 libpq5 && \ + rm -rf /var/lib/apt/lists/* && \ + apt-get purge -y --auto-remove && \ + mkdir -p /keys -COPY crypto_auth_provider.py /usr/local/lib/python3.11/site-packages/ +COPY --from=builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/ + +COPY crypto_auth_provider.py /usr/local/lib/python3.13/site-packages/ COPY homeserver.yaml /config/ COPY synapse.log.config /config/ COPY synapse_master.service /etc/systemd/system/ @@ -16,10 +23,13 @@ COPY synapse_worker@.service /etc/systemd/system/ COPY matrix_synapse.target /etc/systemd/system/ COPY workers /config/workers COPY shared_config.yaml /config/ -COPY beacon_info_module.py /usr/local/lib/python3.11/site-packages/ +COPY beacon_info_module.py /usr/local/lib/python3.13/site-packages/ -# run change on max size -RUN sed -i 's/65536/1048576/' /usr/local/lib/python3.11/site-packages/synapse/api/constants.py +# Increase max event size (1MB instead of default 64KB). +# Beacon messages can exceed the default Matrix PDU size limit. +RUN sed -i 's/^MAX_PDU_SIZE = 65536$/MAX_PDU_SIZE = 1048576/' /usr/local/lib/python3.13/site-packages/synapse/api/constants.py && \ + grep -q '^MAX_PDU_SIZE = 1048576$' /usr/local/lib/python3.13/site-packages/synapse/api/constants.py || \ + (echo "FATAL: PDU size patch failed - 'MAX_PDU_SIZE = 65536' not found in constants.py. Upstream may have changed." >&2 && exit 1) COPY wait-for.sh /usr/local/bin/ COPY synctl_entrypoint.sh /usr/local/bin/ diff --git a/docker/beacon_info_module.py b/docker/beacon_info_module.py index 6ac16a0..46fee77 100644 --- a/docker/beacon_info_module.py +++ b/docker/beacon_info_module.py @@ -6,6 +6,35 @@ logger = logging.getLogger(__name__) + +class WellKnownServerResource(Resource): + isLeaf = True + + def render_GET(self, request): + server_name = os.environ.get("SERVER_NAME", "localhost") + request.setHeader(b"content-type", b"application/json") + return json.dumps({"m.server": f"{server_name}:443"}).encode("utf-8") + + +class WellKnownClientResource(Resource): + isLeaf = True + + def render_GET(self, request): + server_name = os.environ.get("SERVER_NAME", "localhost") + request.setHeader(b"content-type", b"application/json") + request.setHeader(b"Access-Control-Allow-Origin", b"*") + return json.dumps({"m.homeserver": {"base_url": f"https://{server_name}"}}).encode("utf-8") + + +class WellKnownMatrixResource(Resource): + """Parent resource for /.well-known/matrix/* endpoints.""" + + def __init__(self): + super().__init__() + self.putChild(b"server", WellKnownServerResource()) + self.putChild(b"client", WellKnownClientResource()) + + class BeaconInfoModule(Resource): def __init__(self, config, api): super().__init__() @@ -13,12 +42,13 @@ def __init__(self, config, api): self.config = config self.api = api self.api.register_web_resource(path="/_synapse/client/beacon/info", resource=self) + self.api.register_web_resource(path="/.well-known/matrix", resource=WellKnownMatrixResource()) def render(self, request): request.setHeader(b"content-type", b"application/json; charset=utf-8") - request.setHeader(b"Access-Control-Allow-Origin",b"*") + request.setHeader(b"Access-Control-Allow-Origin", b"*") return json.dumps({ - "region":os.environ.get("SERVER_REGION", "region not set"), - "known_servers":self.config.get("known_servers",[]), + "region": os.environ.get("SERVER_REGION", "region not set"), + "known_servers": self.config.get("known_servers", []), "timestamp": time.time() }).encode("utf-8") diff --git a/docker/homeserver.yaml b/docker/homeserver.yaml index 41ec4cb..afd8e84 100644 --- a/docker/homeserver.yaml +++ b/docker/homeserver.yaml @@ -134,7 +134,7 @@ url_preview_enabled: false redis: enabled: true - host: redis + host: {{REDIS_HOST}} ## Caching - Performance optimizations caches: @@ -155,18 +155,14 @@ modules: - module: beacon_info_module.BeaconInfoModule config: known_servers: - - "beacon-node-1.diamond.papers.tech" - - "beacon-node-1.sky.papers.tech" - - "beacon-node-2.sky.papers.tech" - - "beacon-node-1.hope.papers.tech" - - "beacon-node-1.hope-2.papers.tech" - - "beacon-node-1.hope-3.papers.tech" - - "beacon-node-1.hope-4.papers.tech" - - "beacon-node-1.hope-5.papers.tech" - - "beacon-node-1.beacon-server-1.papers.tech" - - "beacon-node-1.beacon-server-2.papers.tech" - - "beacon-node-1.beacon-server-3.papers.tech" - - "beacon-node-1.beacon-server-4.papers.tech" + - "beacon-node-1.octez.io" + - "beacon-node-2.octez.io" + - "beacon-node-3.octez.io" + - "beacon-node-4.octez.io" + - "beacon-node-5.octez.io" + - "beacon-node-6.octez.io" + - "beacon-node-7.octez.io" + - "beacon-node-8.octez.io" instance_map: main: diff --git a/docker/shared_config.yaml b/docker/shared_config.yaml index e3a8dc4..353b5e1 100644 --- a/docker/shared_config.yaml +++ b/docker/shared_config.yaml @@ -17,7 +17,7 @@ listeners: redis: enabled: true - host: redis + host: {{REDIS_HOST}} report_stats: false diff --git a/docker/synctl_entrypoint.sh b/docker/synctl_entrypoint.sh index 38f5dcb..cd32275 100755 --- a/docker/synctl_entrypoint.sh +++ b/docker/synctl_entrypoint.sh @@ -5,6 +5,8 @@ sed -i "s/{{DB_HOST}}/$DB_HOST/g" /config/homeserver.yaml sed -i "s/{{DB_USER}}/$DB_USER/g" /config/homeserver.yaml sed -i "s/{{DB_PASS}}/$DB_PASS/g" /config/homeserver.yaml sed -i "s/{{DB_NAME}}/$DB_NAME/g" /config/homeserver.yaml +sed -i "s/{{REDIS_HOST}}/${REDIS_HOST:-redis}/g" /config/shared_config.yaml +sed -i "s/{{REDIS_HOST}}/${REDIS_HOST:-redis}/g" /config/homeserver.yaml echo "${SIGNING_KEY}" > /config/signing.key /usr/local/bin/wait-for.sh $DB_HOST:5432 diff --git a/k8s/common/limitrange.yaml b/k8s/common/limitrange.yaml deleted file mode 100644 index 83dd6d4..0000000 --- a/k8s/common/limitrange.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: LimitRange -metadata: - name: beacon-node - namespace: beacon-node -spec: - limits: - - default: - cpu: "0.5" - memory: "1000Mi" - defaultRequest: - cpu: "0.05" - memory: 150Mi - type: Container diff --git a/k8s/common/namespace.yaml b/k8s/common/namespace.yaml deleted file mode 100644 index b8a1148..0000000 --- a/k8s/common/namespace.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: beacon-node diff --git a/k8s/common/synapse/deployment.yaml b/k8s/common/synapse/deployment.yaml deleted file mode 100644 index d57fa8a..0000000 --- a/k8s/common/synapse/deployment.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: synapse - namespace: beacon-node - labels: - app: synapse -spec: - selector: - matchLabels: - app: synapse - strategy: - type: Recreate - template: - metadata: - labels: - app: synapse - spec: - containers: - - image: _TO_BE_REPLACED_BY_IMAGE_TAG_ - imagePullPolicy: IfNotPresent - name: synapse - envFrom: - - configMapRef: - name: synapse-config-map - - secretRef: - name: synapse-secret - ports: - - containerPort: 8008 - restartPolicy: Always -status: {} diff --git a/k8s/common/synapse/service.yaml b/k8s/common/synapse/service.yaml deleted file mode 100644 index b58e8c6..0000000 --- a/k8s/common/synapse/service.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: synapse-service - namespace: beacon-node -spec: - ports: - - name: client - port: 8008 - targetPort: 8008 - protocol: TCP - type: NodePort - selector: - app: synapse diff --git a/k8s/development/config-map.yaml b/k8s/development/config-map.yaml deleted file mode 100644 index 2b52e0b..0000000 --- a/k8s/development/config-map.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: synapse-config-map - namespace: beacon-node -data: - SERVER_NAME: "matrix-dev.papers.tech" diff --git a/k8s/development/ingress.yaml b/k8s/development/ingress.yaml deleted file mode 100644 index f0c71fd..0000000 --- a/k8s/development/ingress.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: networking.k8s.io/v1beta1 -kind: Ingress -metadata: - name: synapse - namespace: beacon-node - annotations: - kubernetes.io/ingress.class: "nginx" - cert-manager.io/cluster-issuer: "letsencrypt-prod" -spec: - tls: - - hosts: - - matrix-dev.papers.tech - - matrix.dev.gke.papers.tech - secretName: matrix-synapse-prod-tls - rules: - - host: matrix-dev.papers.tech - http: - paths: - - backend: - serviceName: synapse-service - servicePort: 8008 - - host: matrix.dev.gke.papers.tech - http: - paths: - - backend: - serviceName: synapse-service - servicePort: 8008 diff --git a/k8s/development/secret.yaml b/k8s/development/secret.yaml deleted file mode 100644 index 7c16639..0000000 --- a/k8s/development/secret.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# this is just a dummy sample, make sure to create the secret manually and never check it in. -apiVersion: v1 -kind: Secret -metadata: - name: synapse-secret - namespace: beacon-node -type: Opaque -stringData: - DB_HOST: _TO_BE_REPLACED_BY_DEV_DB_HOST_ - DB_NAME: _TO_BE_REPLACED_BY_DEV_DB_NAME_ - DB_PASS: _TO_BE_REPLACED_BY_DEV_DB_PASS_ - DB_USER: _TO_BE_REPLACED_BY_DEV_DB_USER_ - SIGNING_KEY: _TO_BE_REPLACED_BY_DEV_SIGNING_KEY_ diff --git a/k8s/production/config-map.yaml b/k8s/production/config-map.yaml deleted file mode 100644 index a27ec2a..0000000 --- a/k8s/production/config-map.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: synapse-config-map - namespace: beacon-node -data: - SERVER_NAME: "matrix.papers.tech" diff --git a/k8s/production/ingress.yaml b/k8s/production/ingress.yaml deleted file mode 100644 index a1c854c..0000000 --- a/k8s/production/ingress.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: networking.k8s.io/v1beta1 -kind: Ingress -metadata: - name: synapse - namespace: beacon-node - annotations: - kubernetes.io/ingress.class: "nginx" - cert-manager.io/cluster-issuer: "letsencrypt-prod" -spec: - tls: - - hosts: - - matrix.prod.gke.papers.tech - secretName: matrix-synapse-prod-tls - rules: - - host: matrix.prod.gke.papers.tech - http: - paths: - - backend: - serviceName: synapse-service - servicePort: 8008 diff --git a/k8s/production/secret.yaml b/k8s/production/secret.yaml deleted file mode 100644 index 95ee4d5..0000000 --- a/k8s/production/secret.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# this is just a dummy sample, make sure to create the secret manually and never check it in. -apiVersion: v1 -kind: Secret -metadata: - name: synapse-secret - namespace: beacon-node -type: Opaque -stringData: - DB_HOST: _TO_BE_REPLACED_BY_PROD_DB_HOST_ - DB_NAME: _TO_BE_REPLACED_BY_PROD_DB_NAME_ - DB_PASS: _TO_BE_REPLACED_BY_PROD_DB_PASS_ - DB_USER: _TO_BE_REPLACED_BY_PROD_DB_USER_ - SIGNING_KEY: _TO_BE_REPLACED_BY_PROD_SIGNING_KEY_ diff --git a/readme.md b/readme.md index 357d040..1b9cd9b 100644 --- a/readme.md +++ b/readme.md @@ -1,128 +1,110 @@ -[![Docker Pulls](https://img.shields.io/docker/pulls/airgapdocker/beacon-node)](https://hub.docker.com/r/airgapdocker/beacon-node) +# Beacon Node -# Beacon Node Docker +A custom [Synapse](https://github.com/element-hq/synapse) Docker image with cryptographic authentication, worker support, and the beacon info module. -This Docker image will run Synapse as a single process and is a fork of the matrixdotorg/synapse docker image. +## What's different from upstream Synapse -The big difference this image has is that it include pysodium and an auth provider that is compatible with cryptographic signatures: `crypto_auth_provider.py` +- **Crypto auth provider** (`crypto_auth_provider.py`) — authentication via cryptographic signatures +- **Beacon info module** (`beacon_info_module.py`) — exposes `/_synapse/client/beacon/info` and `/.well-known/matrix/*` endpoints +- **pysodium** and **psycopg2** pre-installed +- **Worker mode** enabled by default (1 main process + 4 workers) +- **Max event size** increased from 64KB to 1MB +- Media repo disabled (pure communication transport layer) -By default it uses a postgres database; and is hence suited for production use. +## Quick start with Docker Compose -The image also does _not_ provide a TURN server. +```bash +git clone https://github.com/apham0001/beacon-node.git +cd beacon-node/docker-compose +# Edit docker-compose.yml: set SERVER_NAME and SIGNING_KEY +docker compose up -d +``` + +## Environment variables -## Volumes +| Variable | Required | Description | +|----------|----------|-------------| +| `SERVER_NAME` | Yes | Matrix server FQDN (e.g. `beacon-node-1.octez.io`) | +| `DB_HOST` | Yes | PostgreSQL hostname | +| `DB_USER` | Yes | PostgreSQL username | +| `DB_PASS` | Yes | PostgreSQL password | +| `DB_NAME` | Yes | PostgreSQL database name | +| `SIGNING_KEY` | Yes | Ed25519 signing key (see below) | +| `SERVER_REGION` | Yes | Geographic region for beacon info | +| `REDIS_HOST` | No | Redis hostname (default: `redis`) | -By default, the image expects a single volume, located at `/data`, that will hold: +### Generating a signing key -- keys; -- temporary data; +```bash +docker run --rm --entrypoint="" ghcr.io/apham0001/beacon-node:latest \ + python -c "import signedjson.key, sys; signedjson.key.write_signing_keys(sys.stdout, [signedjson.key.generate_signing_key('0')])" +``` ## Dependencies -We require a postgres database connection. The environment variable `SERVER_NAME` controls how your matrix node will be called and reached, this _needs_ to be a fqdn which will be forwarding requests on port 8080 and 8448 to this container. +### PostgreSQL -## Service Ports +Required for all Synapse data (rooms, messages, events, users, keys). Synapse requires C locale and UTF8 encoding. -Port 8008 is required for the actual matrix service and will be your endpoint. +The image expects the database to be reachable at `DB_HOST` on port 5432. The entrypoint waits for PG to be ready before starting Synapse. -Note: in order for federation to work you will need: +### Redis -1. SSL/TLS enabled for your domain -2. have that SSL/TLS setup for port 8448 and 443 -3. forward SSL/TLS traffic from 8448 to 8008 of this container -4. forward SSL/TLS traffic from 443 to 8008 of this container +Required for inter-process communication between the main process and the 4 workers. Configurable via the `REDIS_HOST` env var (defaults to `redis` if not set). -## Running beacon-node using docker +Redis does not require authentication (Synapse default). -You can start beacon-node as follows (currently we support only postgres setups and expect that the domain given in "SERVER_NAME" is also where this container will be reachable on port 8080 for the letsencrypt request): +## Known servers -``` +The list of known beacon servers is hardcoded in `docker/homeserver.yaml` and served via the `/_synapse/client/beacon/info` endpoint. Clients use this list for server discovery and failover. + +Current servers: +- `beacon-node-1.octez.io` through `beacon-node-8.octez.io` + +To update the list, edit `docker/homeserver.yaml` under `modules > beacon_info_module > config > known_servers` and rebuild the image. + +## Ports + +| Port | Service | +|------|---------| +| 8008 | Main Synapse (client + federation) | +| 8083 | Worker 1 | +| 8084 | Worker 2 | +| 8085 | Worker 3 | +| 8086 | Worker 4 | + +Route all external traffic (443, 8448) to port 8008 with TLS termination. + +## Federation endpoints + +The beacon info module serves Matrix well-known endpoints directly from Synapse (no nginx snippet needed): + +- `/.well-known/matrix/server` — returns `{"m.server": ":443"}` +- `/.well-known/matrix/client` — returns `{"m.homeserver": {"base_url": "https://"}}` +- `/_synapse/client/beacon/info` — returns region, known servers list, and timestamp + +## Running with Docker + +```bash docker run -d --name beacon-node \ - --mount type=volume,src=synapse-data,dst=/data \ - -p 8080:8080 \ -p 8008:8008 \ -e SERVER_NAME=matrix.example.com \ -e DB_HOST=postgres \ -e DB_USER=synapse \ -e DB_NAME=synapse \ -e DB_PASS=password \ - airgapdocker/beacon-node:latest + -e SIGNING_KEY="ed25519 a_0 " \ + -e SERVER_REGION=EU \ + ghcr.io/apham0001/beacon-node:latest ``` -## Running beacon-node using docker-compose - -You can start beacon-node as follows (currently we support only postgres setups and expect that the domain given in "SERVER_NAME" is also where this container will be reachable): +## Building the image +```bash +cd docker +docker build -t beacon-node . ``` -git clone -cd beacon-node/samples -vim docker-compose.yml # edit according to your likings: SERVER_NAME must be changed! -docker-compose up -d -``` - -## Sample Nginx configuration - -This is a sample configuration of `nginx` that will route all the traffic to the correct port. The certificates were added by Certbot and provided by letsencrypt. - -```nginx - -upstream matrix_workers { - server localhost:8083; - server localhost:8084; - server localhost:8085; - server localhost:8086; - } - -server { - listen 8448 ssl; - listen [::]:8448 ssl; - - server_name MY_SERVER_DOMAIN; - - location ~* ^(\/_matrix\/client\/(v2_alpha|r0)\/sync) { - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_pass http://matrix_workers; - client_max_body_size 50M; - } - - location ~* ^(\/_matrix\/client\/(api/v1|r0|unstable)\/rooms\/.*\/(join|invite|leave|ban|unban|kick)) { - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_pass http://matrix_workers; - client_max_body_size 50M; - } - - - location ~* ^(\/_matrix\/client\/(api/v1|r0|unstable)\/login) { - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_pass http://matrix_workers; - client_max_body_size 50M; - } - - location / { - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header Host $http_host; - proxy_pass http://localhost:8008; - } - - listen [::]:443 ssl ipv6only=on; # managed by Certbot - listen 443 ssl; # managed by Certbot - ssl_certificate /etc/letsencrypt/live/MY_SERVER_DOMAIN/fullchain.pem; # managed by Certbot - ssl_certificate_key /etc/letsencrypt/live/MY_SERVER_DOMAIN/privkey.pem; # managed by Certbot - include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot -} -``` - -## Running beacon-node using kubernetes - -See the k8s folder in this project for a production ready k8s setup. -## Running beacon-node directly +## License -Our requirements to any beacon-node installation are minimal. Check `docker/homeserver.yaml` for the configuration and make sure to place `docker/crypto_auth_provider.py` to a place where it can be picked up by beacon-node (the Dockerfile is quite straight forward and the best documentation). +[MIT](LICENSE) diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..08a926e --- /dev/null +++ b/renovate.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "forkProcessing": "enabled", + "automerge": true, + "automergeType": "pr", + "packageRules": [ + { + "description": "Auto-merge all updates (CI must pass via branch protection)", + "matchManagers": ["github-actions", "dockerfile"], + "automerge": true, + "automergeType": "pr" + }, + { + "description": "Auto-merge patch updates for Synapse base image", + "matchDatasources": ["docker"], + "matchPackageNames": ["ghcr.io/element-hq/synapse"], + "automerge": true, + "automergeType": "pr", + "matchUpdateTypes": ["patch"] + } + ] +} diff --git a/samples/docker-compose-optimized.yml b/samples/docker-compose-optimized.yml deleted file mode 100644 index 9974b4f..0000000 --- a/samples/docker-compose-optimized.yml +++ /dev/null @@ -1,96 +0,0 @@ -version: "3.1" -services: - postgres: - image: postgres:12.0 - restart: always - # IMPORTANT: shm_size must be larger than shared_buffers to prevent Docker shared memory errors - shm_size: '2gb' - environment: - POSTGRES_USER: synapse - POSTGRES_PASSWORD: "CHANGE_ME_STRONG_PASSWORD" - POSTGRES_DB: synapse - LANG: 'C' - LANGUAGE: 'C' - LC_ALL: 'C' - LC_COLLATE: 'C' - LC_CTYPE: 'C' - ENCODING: "UTF8" - POSTGRES_INITDB_ARGS: "-E UTF8" - # Performance-optimized PostgreSQL settings for 16GB system with 8GB available - command: > - postgres - -c shared_buffers=1GB # Main cache for frequently accessed data (default: 128MB) - -c effective_cache_size=4GB # Hint to query planner about OS cache, not actual allocation (default: 4GB) - -c work_mem=64MB # Memory per sort/hash operation - critical for 73KB+ messages (default: 4MB) - -c maintenance_work_mem=512MB # Memory for VACUUM, CREATE INDEX - prevents vacuum issues (default: 64MB) - -c max_connections=300 # Supports 80 Synapse connections + overhead (default: 100) - -c random_page_cost=1.1 # Optimized for SSD, lower = favor index scans (default: 4.0 for HDD) - -c checkpoint_completion_target=0.9 # Spread checkpoint writes over 90% of interval, reduces I/O spikes (default: 0.5) - -c autovacuum_vacuum_scale_factor=0.05 # Vacuum when 5% of table changes, prevents bloat (default: 0.2 = 20%) - -c min_wal_size=2GB # Minimum WAL size - reduces frequent WAL recycling (default: 80MB) - -c max_wal_size=4GB # Maximum WAL size before checkpoint forced (default: 1GB) - -c wal_buffers=32MB # WAL write buffer - improves write performance (default: -1 = 1/32 of shared_buffers) - -c autovacuum_max_workers=4 # Parallel vacuum workers - faster cleanup (default: 3) - -c autovacuum_analyze_scale_factor=0.02 # Analyze when 2% changes, keeps statistics fresh (default: 0.1 = 10%) - volumes: - - ./postgres_data:/var/lib/postgresql/data - logging: - options: - max-size: "10m" - max-file: "3" - - redis: - image: redis:6.2.4-buster - restart: always - # Conservative Redis configuration - only setting memory limit - command: > - redis-server - --maxmemory 1gb # Limit Redis memory usage to prevent OOM - --maxmemory-policy allkeys-lru # When limit hit, evict least recently used keys - expose: - - "6379" - volumes: - - ./redis_data:/data - logging: - options: - max-size: "10m" - max-file: "3" - - tezos-synapse: - # Update this to your new image version after building with updated homeserver.yaml - image: airgapdocker/beacon-node:optimized - restart: always - depends_on: - - postgres - - redis - ports: - - "8008:8008" # Main client/federation port - - "8083:8083" # Worker 1 - - "8084:8084" # Worker 2 - - "8085:8085" # Worker 3 - - "8086:8086" # Worker 4 - environment: - DB_HOST: postgres - DB_USER: synapse - DB_PASS: "CHANGE_ME_STRONG_PASSWORD" - DB_NAME: synapse - SERVER_NAME: "YOUR_SERVER_NAME.example.com" - SIGNING_KEY: "YOUR_SIGNING_KEY" - REGISTRATION_SHARED_SECRET: "CHANGE_ME_RANDOM_SECRET" - SERVER_REGION: "your-region" - # Optional: Sentry error tracking (NOTE: Requires additional setup - see docs/PERFORMANCE_IMPROVEMENTS.md) - # SENTRY_DSN: "https://fc05bbc75d345906ed15de7d8ef76fc7@reporting.papers.tech/17" - volumes: - - ./synapse_data:/data - logging: - options: - max-size: "10m" - max-file: "3" - -# Optional: Add networks for better isolation -networks: - default: - driver: bridge - ipam: - config: - - subnet: 172.20.0.0/16 \ No newline at end of file