diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 00000000..525978e1 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,106 @@ +# GitHub Actions Workflows + +## Übersicht + +### Release & Deployment Workflows + +#### `main-release.yml` (Haupt-Release-Pipeline) + +Läuft automatisch bei jedem Push auf `main`: + +1. **Semantic Release** - Erstellt neue Releases basierend auf Conventional Commits +2. **Docker Build Backend** - Baut und pusht Backend-Image nach ghcr.io +3. **Docker Build Frontend** - Baut und pusht Frontend-Image nach ghcr.io +4. **Summary** - Zeigt Übersicht aller Artefakte + +**Outputs:** + +- GitHub Release mit Binaries +- Docker Images: `ghcr.io/cs-foundry/csf-core-backend:latest` & `:version` +- Docker Images: `ghcr.io/cs-foundry/csf-core-frontend:latest` & `:version` + +#### `release.yml` (Wiederverwendbarer Release-Workflow) + +Wird von `main-release.yml` aufgerufen: + +- Führt Semantic Release aus +- Baut Backend-Binaries (Linux/macOS) +- Baut Frontend-Package +- Lädt alle Artefakte zum Release hoch + +#### `docker-build-manual.yml` (Manuelles Docker-Build) + +Manueller Workflow für Docker-Builds: + +- Auswahl: Backend, Frontend oder beides +- Eigene Versionsnummer angeben +- Erstellt Tags: `` und `manual-latest` + +### Weitere Workflows + +#### `beta-release.yml` + +Release-Pipeline für Beta-Versionen auf dem `beta` Branch + +#### `docker-build-push.yml` + +Legacy-Workflow für das vereinigte Backend+Frontend Image + +#### `build-artifacts.yml` + +Standalone-Workflow für Binary-Builds + +#### `lint.yml` + +Code-Quality-Checks (Rust, TypeScript, etc.) + +## Verwendung + +### Automatischer Release (main) + +```bash +git commit -m "feat: neue Feature" +git push origin main +# → Automatischer Release + Docker Images +``` + +### Manueller Docker-Build + +1. GitHub Actions → **Manual Docker Build** +2. **Run workflow** klicken +3. Version eingeben (z.B. `1.2.3`) +4. Target auswählen (backend/frontend/both) +5. **Run workflow** ausführen + +## Image-URLs + +Nach erfolgreichem Build sind die Images verfügbar unter: + +```bash +# Backend +ghcr.io/cs-foundry/csf-core-backend:latest +ghcr.io/cs-foundry/csf-core-backend: + +# Frontend +ghcr.io/cs-foundry/csf-core-frontend:latest +ghcr.io/cs-foundry/csf-core-frontend: +``` + +## Permissions + +Die Workflows benötigen folgende Permissions: + +- `contents: write` - Für Releases +- `packages: write` - Für Docker Registry +- `issues: write` - Für Issue-Updates +- `pull-requests: write` - Für PR-Updates + +## Secrets + +Keine zusätzlichen Secrets erforderlich - verwendet `GITHUB_TOKEN` automatisch. + +## Weitere Dokumentation + +- [Docker Registry Integration](../docs/deployment/DOCKER_REGISTRY.md) +- [NixOS Deployment](../docs/deployment/DEPLOYMENT.md) +- [Installation Guide](../docs/deployment/INSTALLATION.md) diff --git a/.github/workflows/docker-build-manual.yml b/.github/workflows/docker-build-manual.yml new file mode 100644 index 00000000..6400622b --- /dev/null +++ b/.github/workflows/docker-build-manual.yml @@ -0,0 +1,116 @@ +name: Manual Docker Build + +on: + workflow_dispatch: + inputs: + version: + description: "Version tag (e.g., 1.2.3)" + required: true + type: string + build_target: + description: "What to build" + required: true + type: choice + options: + - both + - backend + - frontend + +permissions: + contents: read + packages: write + +jobs: + build-backend: + name: Build Backend Docker Image + runs-on: ubuntu-latest + if: inputs.build_target == 'both' || inputs.build_target == 'backend' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Convert repository name to lowercase + id: repo + run: | + echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Backend Docker image + uses: docker/build-push-action@v5 + with: + context: ./backend + file: ./backend/Dockerfile + push: true + tags: | + ghcr.io/${{ steps.repo.outputs.image_name }}-backend:${{ inputs.version }} + ghcr.io/${{ steps.repo.outputs.image_name }}-backend:manual-latest + cache-from: type=gha,scope=backend + cache-to: type=gha,mode=max,scope=backend + platforms: linux/amd64 + + - name: Summary + run: | + REPO_LOWER=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + echo "## 🐳 Backend Docker Image Built" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Tags:**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "ghcr.io/${REPO_LOWER}-backend:${{ inputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "ghcr.io/${REPO_LOWER}-backend:manual-latest" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + build-frontend: + name: Build Frontend Docker Image + runs-on: ubuntu-latest + if: inputs.build_target == 'both' || inputs.build_target == 'frontend' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Convert repository name to lowercase + id: repo + run: | + echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Frontend Docker image + uses: docker/build-push-action@v5 + with: + context: ./frontend + file: ./frontend/Dockerfile.prod + push: true + tags: | + ghcr.io/${{ steps.repo.outputs.image_name }}-frontend:${{ inputs.version }} + ghcr.io/${{ steps.repo.outputs.image_name }}-frontend:manual-latest + cache-from: type=gha,scope=frontend + cache-to: type=gha,mode=max,scope=frontend + platforms: linux/amd64 + + - name: Summary + run: | + REPO_LOWER=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + echo "## 🐳 Frontend Docker Image Built" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Tags:**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "ghcr.io/${REPO_LOWER}-frontend:${{ inputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "ghcr.io/${REPO_LOWER}-frontend:manual-latest" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/main-release.yml b/.github/workflows/main-release.yml index d94b155d..505fa6e7 100644 --- a/.github/workflows/main-release.yml +++ b/.github/workflows/main-release.yml @@ -10,6 +10,7 @@ permissions: contents: write issues: write pull-requests: write + packages: write jobs: # Step 1: Run Release Workflow @@ -18,11 +19,111 @@ jobs: uses: ./.github/workflows/release.yml secrets: inherit - # Step 2: Summary + # Step 2: Build and Push Backend Docker Image + build-backend-docker: + name: Build Backend Docker Image + runs-on: ubuntu-latest + needs: [release] + if: needs.release.outputs.new_release_published == 'true' + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Convert repository name to lowercase + id: repo + run: | + echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version + id: version + run: | + VERSION="${{ needs.release.outputs.new_release_version }}" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Building backend version: $VERSION" + + - name: Build and push Backend Docker image + uses: docker/build-push-action@v5 + with: + context: ./backend + file: ./backend/Dockerfile + push: true + tags: | + ghcr.io/${{ steps.repo.outputs.image_name }}-backend:latest + ghcr.io/${{ steps.repo.outputs.image_name }}-backend:${{ steps.version.outputs.version }} + cache-from: type=gha,scope=backend + cache-to: type=gha,mode=max,scope=backend + platforms: linux/amd64 + + # Step 3: Build and Push Frontend Docker Image + build-frontend-docker: + name: Build Frontend Docker Image + runs-on: ubuntu-latest + needs: [release] + if: needs.release.outputs.new_release_published == 'true' + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: main + + - name: Convert repository name to lowercase + id: repo + run: | + echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version + id: version + run: | + VERSION="${{ needs.release.outputs.new_release_version }}" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Building frontend version: $VERSION" + + - name: Build and push Frontend Docker image + uses: docker/build-push-action@v5 + with: + context: ./frontend + file: ./frontend/Dockerfile.prod + push: true + tags: | + ghcr.io/${{ steps.repo.outputs.image_name }}-frontend:latest + ghcr.io/${{ steps.repo.outputs.image_name }}-frontend:${{ steps.version.outputs.version }} + cache-from: type=gha,scope=frontend + cache-to: type=gha,mode=max,scope=frontend + platforms: linux/amd64 + + # Step 4: Summary summary: name: Pipeline Summary runs-on: ubuntu-latest - needs: [release] + needs: [release, build-backend-docker, build-frontend-docker] if: always() steps: - name: Generate Summary @@ -34,8 +135,27 @@ jobs: if [ "${{ needs.release.outputs.new_release_published }}" == "true" ]; then VERSION="v${{ needs.release.outputs.new_release_version }}" + REPO_LOWER=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + echo "✅ **Release Created:** $VERSION" >> $GITHUB_STEP_SUMMARY echo "✅ **Binaries Built:** Available for download" >> $GITHUB_STEP_SUMMARY + echo "✅ **Docker Images:** Published to GitHub Container Registry" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Docker Images Info + echo "### 🐳 Docker Images" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Backend Image:**" >> $GITHUB_STEP_SUMMARY + echo '```bash' >> $GITHUB_STEP_SUMMARY + echo "docker pull ghcr.io/${REPO_LOWER}-backend:${{ needs.release.outputs.new_release_version }}" >> $GITHUB_STEP_SUMMARY + echo "docker pull ghcr.io/${REPO_LOWER}-backend:latest" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Frontend Image:**" >> $GITHUB_STEP_SUMMARY + echo '```bash' >> $GITHUB_STEP_SUMMARY + echo "docker pull ghcr.io/${REPO_LOWER}-frontend:${{ needs.release.outputs.new_release_version }}" >> $GITHUB_STEP_SUMMARY + echo "docker pull ghcr.io/${REPO_LOWER}-frontend:latest" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Add download statistics @@ -58,6 +178,8 @@ jobs: fi echo "### 🚀 Installation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Native Installation:**" >> $GITHUB_STEP_SUMMARY echo '```bash' >> $GITHUB_STEP_SUMMARY echo "# Install latest version" >> $GITHUB_STEP_SUMMARY echo "curl -fsSL https://raw.githubusercontent.com/CS-Foundry/CSF-Core/main/scripts/install.sh | sudo bash" >> $GITHUB_STEP_SUMMARY @@ -65,6 +187,21 @@ jobs: echo "# Or download specific binary:" >> $GITHUB_STEP_SUMMARY echo "# https://github.com/CS-Foundry/CSF-Core/releases/tag/$VERSION" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Docker Installation:**" >> $GITHUB_STEP_SUMMARY + echo '```bash' >> $GITHUB_STEP_SUMMARY + echo "# Run backend" >> $GITHUB_STEP_SUMMARY + echo "docker run -d -p 8000:8000 \\" >> $GITHUB_STEP_SUMMARY + echo " -v csf_data:/data \\" >> $GITHUB_STEP_SUMMARY + echo " -e JWT_SECRET=\$(openssl rand -hex 32) \\" >> $GITHUB_STEP_SUMMARY + echo " --name csf-backend \\" >> $GITHUB_STEP_SUMMARY + echo " ghcr.io/${REPO_LOWER}-backend:latest" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "# Run frontend" >> $GITHUB_STEP_SUMMARY + echo "docker run -d -p 80:80 \\" >> $GITHUB_STEP_SUMMARY + echo " --name csf-frontend \\" >> $GITHUB_STEP_SUMMARY + echo " ghcr.io/${REPO_LOWER}-frontend:latest" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY else echo "ℹ️ **No Release Created**" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 1a2f1bc8..a9ffa903 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,23 @@ devenv.local.nix finance.db /backend/db/ -/backend/target/ \ No newline at end of file +/backend/target/ + +# Agent test directories +/agent/test-agent*/ +/agent/test-p2p/ +/agent/certs-agent*/ + +# NixOS build artifacts +nixos-node/result +nixos-node/result-* +nixos/result +nixos/result-* +*.iso +*.qcow2 + +# Nix temporary files +.nix-defexpr +.nix-profile + +keys/ \ No newline at end of file diff --git a/agent/Cargo.lock b/agent/Cargo.lock index 9b1f7768..b4b1c2c4 100644 --- a/agent/Cargo.lock +++ b/agent/Cargo.lock @@ -50,6 +50,45 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -73,6 +112,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "base64" version = "0.21.7" @@ -122,6 +183,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -145,6 +208,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "config" version = "0.14.1" @@ -264,19 +336,59 @@ name = "csf-agent" version = "0.1.0" dependencies = [ "anyhow", + "bytes", "chrono", "config", "hostname", + "http-body-util", + "hyper", + "hyper-util", + "rcgen", "reqwest", + "rustls", + "rustls-pemfile", "serde", "serde_json", "sysinfo", "thiserror", + "time", "tokio", + "tokio-rustls", + "tokio-util", "toml", "tracing", "tracing-subscriber", "uuid", + "webpki", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", ] [[package]] @@ -309,6 +421,12 @@ dependencies = [ "const-random", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -382,6 +500,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.31" @@ -548,6 +672,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -562,6 +692,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -623,6 +754,7 @@ dependencies = [ "socket2", "system-configuration", "tokio", + "tower-layer", "tower-service", "tracing", "windows-registry", @@ -786,6 +918,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.83" @@ -929,6 +1071,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -938,6 +1105,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1037,6 +1213,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1113,6 +1299,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.103" @@ -1157,6 +1349,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1259,6 +1465,15 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "1.1.2" @@ -1278,13 +1493,25 @@ version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ + "aws-lc-rs", + "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.13.1" @@ -1300,6 +1527,7 @@ version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1596,6 +1824,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -1999,6 +2258,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2321,6 +2590,24 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror", + "time", +] + [[package]] name = "yaml-rust2" version = "0.8.1" @@ -2332,6 +2619,15 @@ dependencies = [ "hashlink", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 01379e32..a36f207e 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -34,10 +34,27 @@ uuid = { version = "1", features = ["v4", "serde"] } # Time chrono = { version = "0.4", features = ["serde"] } +time = { version = "0.3", features = ["formatting", "parsing"] } # Hostname hostname = "0.4" +# TLS/mTLS +rustls = { version = "0.23", features = ["ring"] } +tokio-rustls = "0.26" +rustls-pemfile = "2.0" +rcgen = { version = "0.13", features = ["x509-parser"] } +webpki = "0.22" + +# Async HTTP server +hyper = { version = "1", features = ["full"] } +hyper-util = { version = "0.1", features = ["full"] } +http-body-util = "0.1" + +# Networking +tokio-util = { version = "0.7", features = ["codec"] } +bytes = "1" + [features] default = [] vendored-openssl = ["reqwest/native-tls-vendored"] diff --git a/agent/config.example.toml b/agent/config.example.toml new file mode 100644 index 00000000..345ab4b0 --- /dev/null +++ b/agent/config.example.toml @@ -0,0 +1,54 @@ +# CSF Agent Configuration +# This file is automatically generated on first run + +# Unique agent identifier (automatically generated) +agent_id = "00000000-0000-0000-0000-000000000000" + +# Agent name (defaults to hostname) +name = "my-agent" + +# Central server URL +server_url = "http://localhost:8000" + +# API key for authentication +api_key = "" + +# Metrics collection interval in seconds +collection_interval = 30 + +# Heartbeat interval in seconds +heartbeat_interval = 60 + +# Tags for this agent +tags = [] + +# P2P (Peer-to-Peer) Configuration +[p2p] +# Enable P2P connections between agents +enabled = false + +# Port to listen for incoming P2P connections +listen_port = 8443 + +# List of peer agents to connect to (format: "host:port") +# Example: peers = ["192.168.1.100:8443", "192.168.1.101:8443"] +peers = [] + +# Path to agent certificate +# Linux default: /etc/csf-agent/certs/agent.crt +# Windows default: C:\ProgramData\csf-agent\certs\agent.crt +cert_path = "" + +# Path to agent private key +# Linux default: /etc/csf-agent/certs/agent.key +# Windows default: C:\ProgramData\csf-agent\certs\agent.key +key_path = "" + +# Path to CA certificate +# Linux default: /etc/csf-agent/certs/ca.crt +# Windows default: C:\ProgramData\csf-agent\certs\ca.crt +ca_cert_path = "" + +# Automatically generate self-signed certificates if not found +# For production, set to false and provide your own certificates +auto_generate_certs = true diff --git a/agent/config.toml b/agent/config.toml index d74cf71b..eb9d0cfa 100644 --- a/agent/config.toml +++ b/agent/config.toml @@ -1,11 +1,16 @@ -agent_id = "550e8400-e29b-41d4-a716-446655440000" -name = "Test-MacBook" +agent_id = "2e85616c-c24f-4d91-b024-cba384ff3887" +name = "Mac.localdomain" server_url = "http://localhost:8000" -api_key = "test-api-key-123" -collection_interval = 10 -heartbeat_interval = 20 -tags = [ - "test", - "development", - "macos", -] +api_key = "" +collection_interval = 30 +heartbeat_interval = 60 +tags = [] + +[p2p] +enabled = false +listen_port = 8443 +peers = [] +cert_path = "/etc/csf-agent/certs/agent.crt" +key_path = "/etc/csf-agent/certs/agent.key" +ca_cert_path = "/etc/csf-agent/certs/ca.crt" +auto_generate_certs = true diff --git a/agent/src/config.rs b/agent/src/config.rs index 52a8f530..a969039f 100644 --- a/agent/src/config.rs +++ b/agent/src/config.rs @@ -12,9 +12,12 @@ pub struct AgentConfig { /// URL of the central server pub server_url: String, - /// API key for authentication + /// API key for authentication (optional if only P2P is used) pub api_key: String, + /// Skip backend connection if only P2P mode is needed + pub p2p_only_mode: bool, + /// How often to collect metrics (seconds) pub collection_interval: u64, @@ -23,6 +26,73 @@ pub struct AgentConfig { /// Tags for this agent pub tags: Vec, + + /// P2P connection settings + pub p2p: P2PConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct P2PConfig { + /// Enable P2P connections between agents + pub enabled: bool, + + /// Port to listen for P2P connections + pub listen_port: u16, + + /// List of peer agents to connect to (host:port) + pub peers: Vec, + + /// mTLS certificate path + pub cert_path: String, + + /// mTLS private key path + pub key_path: String, + + /// CA certificate path for verifying peers + pub ca_cert_path: String, + + /// Auto-generate self-signed certificates if not found + pub auto_generate_certs: bool, +} + +impl Default for P2PConfig { + fn default() -> Self { + Self { + enabled: false, + listen_port: 8443, + peers: vec![], + cert_path: Self::default_cert_path().to_string_lossy().to_string(), + key_path: Self::default_key_path().to_string_lossy().to_string(), + ca_cert_path: Self::default_ca_cert_path().to_string_lossy().to_string(), + auto_generate_certs: true, + } + } +} + +impl P2PConfig { + fn default_cert_path() -> std::path::PathBuf { + if cfg!(target_os = "windows") { + std::path::PathBuf::from("C:\\ProgramData\\csf-agent\\certs\\agent.crt") + } else { + std::path::PathBuf::from("/etc/csf-agent/certs/agent.crt") + } + } + + fn default_key_path() -> std::path::PathBuf { + if cfg!(target_os = "windows") { + std::path::PathBuf::from("C:\\ProgramData\\csf-agent\\certs\\agent.key") + } else { + std::path::PathBuf::from("/etc/csf-agent/certs/agent.key") + } + } + + fn default_ca_cert_path() -> std::path::PathBuf { + if cfg!(target_os = "windows") { + std::path::PathBuf::from("C:\\ProgramData\\csf-agent\\certs\\ca.crt") + } else { + std::path::PathBuf::from("/etc/csf-agent/certs/ca.crt") + } + } } impl Default for AgentConfig { @@ -35,9 +105,11 @@ impl Default for AgentConfig { .unwrap_or_else(|| "unknown".to_string()), server_url: "http://localhost:8000".to_string(), api_key: String::new(), + p2p_only_mode: false, collection_interval: 30, heartbeat_interval: 60, tags: vec![], + p2p: P2PConfig::default(), } } } diff --git a/agent/src/connect/certs.rs b/agent/src/connect/certs.rs new file mode 100644 index 00000000..aa4a78be --- /dev/null +++ b/agent/src/connect/certs.rs @@ -0,0 +1,217 @@ +use anyhow::{Context, Result}; +use rcgen::{ + BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair, KeyUsagePurpose, +}; +use std::fs; +use std::path::Path; +use time::{Duration, OffsetDateTime}; + +/// Generates a self-signed CA certificate +pub fn generate_ca_cert(common_name: &str, output_dir: &Path) -> Result<()> { + let mut params = CertificateParams::default(); + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + + let mut dn = DistinguishedName::new(); + dn.push(DnType::CommonName, common_name); + dn.push(DnType::OrganizationName, "CSF Agent Network"); + dn.push(DnType::CountryName, "US"); + params.distinguished_name = dn; + + // Set validity period (10 years) + params.not_before = OffsetDateTime::now_utc(); + params.not_after = OffsetDateTime::now_utc() + Duration::days(3650); + + // Key usage for CA + params.key_usages = vec![ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::KeyCertSign, + KeyUsagePurpose::CrlSign, + ]; + + let key_pair = KeyPair::generate()?; + let cert = params.self_signed(&key_pair)?; + + // Create output directory + fs::create_dir_all(output_dir).context("Failed to create certificate directory")?; + + // Save CA certificate + let ca_cert_path = output_dir.join("ca.crt"); + fs::write(&ca_cert_path, cert.pem()).context("Failed to write CA certificate")?; + + // Save CA private key + let ca_key_path = output_dir.join("ca.key"); + fs::write(&ca_key_path, key_pair.serialize_pem()).context("Failed to write CA private key")?; + + // Set restrictive permissions on private key (Unix only) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&ca_key_path)?.permissions(); + perms.set_mode(0o600); + fs::set_permissions(&ca_key_path, perms)?; + } + + tracing::info!("Generated CA certificate at {:?}", ca_cert_path); + Ok(()) +} + +/// Generates an agent certificate signed by the CA +pub fn generate_agent_cert( + agent_name: &str, + ca_cert_path: &Path, + ca_key_path: &Path, + output_dir: &Path, +) -> Result<()> { + // Load CA certificate and key + let ca_cert_pem = fs::read_to_string(ca_cert_path).context("Failed to read CA certificate")?; + let ca_key_pem = fs::read_to_string(ca_key_path).context("Failed to read CA private key")?; + + let ca_key_pair = KeyPair::from_pem(&ca_key_pem).context("Failed to parse CA private key")?; + let ca_cert_params = CertificateParams::from_ca_cert_pem(&ca_cert_pem) + .context("Failed to parse CA certificate")?; + let ca_cert = ca_cert_params.self_signed(&ca_key_pair)?; + + // Create agent certificate parameters + let mut params = CertificateParams::default(); + + let mut dn = DistinguishedName::new(); + dn.push(DnType::CommonName, agent_name); + dn.push(DnType::OrganizationName, "CSF Agent Network"); + params.distinguished_name = dn; + + // Set validity period (1 year) + params.not_before = OffsetDateTime::now_utc(); + params.not_after = OffsetDateTime::now_utc() + Duration::days(365); + + // Key usage for agent certificates + params.key_usages = vec![ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::KeyEncipherment, + ]; + + // Extended key usage for server and client auth + params.extended_key_usages = vec![ + rcgen::ExtendedKeyUsagePurpose::ServerAuth, + rcgen::ExtendedKeyUsagePurpose::ClientAuth, + ]; + + // Add SubjectAlternativeNames for localhost and common IPs + params.subject_alt_names = vec![ + rcgen::SanType::DnsName("localhost".try_into().unwrap()), + rcgen::SanType::IpAddress(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1))), + rcgen::SanType::IpAddress(std::net::IpAddr::V6(std::net::Ipv6Addr::new( + 0, 0, 0, 0, 0, 0, 0, 1, + ))), + ]; + + // Generate agent key pair + let agent_key_pair = KeyPair::generate()?; + + // Sign agent certificate with CA + let agent_cert = params.signed_by(&agent_key_pair, &ca_cert, &ca_key_pair)?; + + // Create output directory + fs::create_dir_all(output_dir).context("Failed to create certificate directory")?; + + // Save agent certificate + let agent_cert_path = output_dir.join("agent.crt"); + fs::write(&agent_cert_path, agent_cert.pem()).context("Failed to write agent certificate")?; + + // Save agent private key + let agent_key_path = output_dir.join("agent.key"); + fs::write(&agent_key_path, agent_key_pair.serialize_pem()) + .context("Failed to write agent private key")?; + + // Set restrictive permissions on private key (Unix only) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&agent_key_path)?.permissions(); + perms.set_mode(0o600); + fs::set_permissions(&agent_key_path, perms)?; + } + + tracing::info!("Generated agent certificate at {:?}", agent_cert_path); + Ok(()) +} + +/// Ensures certificates exist, generates them if needed +pub fn ensure_certificates(agent_name: &str, cert_dir: &Path, auto_generate: bool) -> Result<()> { + let ca_cert_path = cert_dir.join("ca.crt"); + let ca_key_path = cert_dir.join("ca.key"); + let agent_cert_path = cert_dir.join("agent.crt"); + let agent_key_path = cert_dir.join("agent.key"); + + // Check if certificates already exist + let ca_exists = ca_cert_path.exists() && ca_key_path.exists(); + let agent_exists = agent_cert_path.exists() && agent_key_path.exists(); + + if ca_exists && agent_exists { + tracing::info!("Certificates already exist, skipping generation"); + return Ok(()); + } + + if !auto_generate { + anyhow::bail!( + "Certificates not found and auto-generation is disabled. \ + Please provide certificates at {:?}", + cert_dir + ); + } + + tracing::info!("Generating certificates..."); + + // Generate CA if it doesn't exist + if !ca_exists { + let ca_common_name = format!("CSF-Agent-CA-{}", uuid::Uuid::new_v4()); + generate_ca_cert(&ca_common_name, cert_dir)?; + } + + // Generate agent certificate if it doesn't exist + if !agent_exists { + generate_agent_cert(agent_name, &ca_cert_path, &ca_key_path, cert_dir)?; + } + + Ok(()) +} + +/// Loads certificates for mTLS +pub fn load_certs(path: &Path) -> Result>> { + let cert_file = + fs::File::open(path).context(format!("Failed to open certificate file: {:?}", path))?; + let mut reader = std::io::BufReader::new(cert_file); + + rustls_pemfile::certs(&mut reader) + .collect::, _>>() + .context("Failed to parse certificates") +} + +/// Loads private key +pub fn load_private_key(path: &Path) -> Result> { + let key_file = + fs::File::open(path).context(format!("Failed to open private key file: {:?}", path))?; + let mut reader = std::io::BufReader::new(key_file); + + // Try to read as PKCS8 first + if let Some(key) = rustls_pemfile::pkcs8_private_keys(&mut reader) + .next() + .transpose() + .context("Failed to parse PKCS8 private key")? + { + return Ok(rustls::pki_types::PrivateKeyDer::Pkcs8(key)); + } + + // Reset reader and try RSA + let key_file = fs::File::open(path)?; + let mut reader = std::io::BufReader::new(key_file); + + if let Some(key) = rustls_pemfile::rsa_private_keys(&mut reader) + .next() + .transpose() + .context("Failed to parse RSA private key")? + { + return Ok(rustls::pki_types::PrivateKeyDer::Pkcs1(key)); + } + + anyhow::bail!("No valid private key found in {:?}", path) +} diff --git a/agent/src/connect/connector.rs b/agent/src/connect/connector.rs new file mode 100644 index 00000000..bb785b89 --- /dev/null +++ b/agent/src/connect/connector.rs @@ -0,0 +1,432 @@ +use anyhow::{Context, Result}; +use chrono::{DateTime, Utc}; +use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName}; +use rustls::RootCertStore; +use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; +use std::path::Path; +use std::sync::Arc; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio_rustls::{TlsAcceptor, TlsConnector}; +use uuid::Uuid; + +use super::certs; + +/// Message types for P2P communication +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum P2PMessage { + /// Handshake message to establish connection + Handshake { + agent_id: Uuid, + agent_name: String, + timestamp: DateTime, + }, + /// Heartbeat to maintain connection + Heartbeat { + agent_id: Uuid, + timestamp: DateTime, + }, + /// Share metrics with peer + MetricsShare { + agent_id: Uuid, + timestamp: DateTime, + metrics: serde_json::Value, + }, + /// Request metrics from peer + MetricsRequest { + agent_id: Uuid, + timestamp: DateTime, + }, + /// Response to a request + Response { + success: bool, + message: String, + data: Option, + }, +} + +/// mTLS P2P Connector for agent-to-agent communication +#[derive(Clone)] +pub struct P2PConnector { + agent_id: Uuid, + agent_name: String, + listen_addr: SocketAddr, + tls_acceptor: TlsAcceptor, + tls_connector: TlsConnector, +} + +impl P2PConnector { + /// Creates a new P2P connector with mTLS configuration + pub fn new( + agent_id: Uuid, + agent_name: String, + listen_port: u16, + cert_path: &Path, + key_path: &Path, + ca_cert_path: &Path, + ) -> Result { + // Load certificates + let certs = certs::load_certs(cert_path).context("Failed to load agent certificate")?; + let key = certs::load_private_key(key_path).context("Failed to load agent private key")?; + let ca_certs = certs::load_certs(ca_cert_path).context("Failed to load CA certificate")?; + + // Build server config (for accepting connections) + let server_config = Self::build_server_config(certs.clone(), key.clone_key(), &ca_certs)?; + let tls_acceptor = TlsAcceptor::from(Arc::new(server_config)); + + // Build client config (for initiating connections) + let client_config = Self::build_client_config(certs, key, &ca_certs)?; + let tls_connector = TlsConnector::from(Arc::new(client_config)); + + let listen_addr = SocketAddr::from(([0, 0, 0, 0], listen_port)); + + Ok(Self { + agent_id, + agent_name, + listen_addr, + tls_acceptor, + tls_connector, + }) + } + + /// Build server TLS config for accepting connections + fn build_server_config( + certs: Vec>, + key: PrivateKeyDer<'static>, + ca_certs: &[CertificateDer<'static>], + ) -> Result { + // Create root cert store for client verification + let mut root_store = RootCertStore::empty(); + for cert in ca_certs { + root_store + .add(cert.clone()) + .context("Failed to add CA certificate to root store")?; + } + + // Create verifier that requires client certificates + let client_verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store)) + .build() + .context("Failed to build client verifier")?; + + let config = rustls::ServerConfig::builder() + .with_client_cert_verifier(client_verifier) + .with_single_cert(certs, key) + .context("Failed to build server config")?; + + Ok(config) + } + + /// Build client TLS config for initiating connections + fn build_client_config( + certs: Vec>, + key: PrivateKeyDer<'static>, + ca_certs: &[CertificateDer<'static>], + ) -> Result { + // Create root cert store for server verification + let mut root_store = RootCertStore::empty(); + for cert in ca_certs { + root_store + .add(cert.clone()) + .context("Failed to add CA certificate to root store")?; + } + + let config = rustls::ClientConfig::builder() + .with_root_certificates(root_store) + .with_client_auth_cert(certs, key) + .context("Failed to build client config")?; + + Ok(config) + } + + /// Start listening for incoming P2P connections + pub async fn start_server(&self) -> Result<()> { + let listener = TcpListener::bind(self.listen_addr) + .await + .context(format!("Failed to bind to {}", self.listen_addr))?; + + tracing::info!("P2P server listening on {}", self.listen_addr); + + loop { + match listener.accept().await { + Ok((stream, peer_addr)) => { + tracing::info!("Accepted connection from {}", peer_addr); + let acceptor = self.tls_acceptor.clone(); + let agent_id = self.agent_id; + let agent_name = self.agent_name.clone(); + + tokio::spawn(async move { + if let Err(e) = Self::handle_connection( + stream, acceptor, agent_id, agent_name, peer_addr, + ) + .await + { + tracing::error!("Error handling connection from {}: {}", peer_addr, e); + } + }); + } + Err(e) => { + tracing::error!("Error accepting connection: {}", e); + } + } + } + } + + /// Handle an incoming connection + async fn handle_connection( + stream: TcpStream, + acceptor: TlsAcceptor, + agent_id: Uuid, + agent_name: String, + peer_addr: SocketAddr, + ) -> Result<()> { + // Perform TLS handshake + let mut tls_stream = match acceptor.accept(stream).await { + Ok(s) => s, + Err(e) => { + anyhow::bail!("TLS handshake failed: {:?}", e); + } + }; + + tracing::info!("TLS handshake completed with {}", peer_addr); + + // Send handshake message + let handshake = P2PMessage::Handshake { + agent_id, + agent_name: agent_name.clone(), + timestamp: Utc::now(), + }; + Self::send_message(&mut tls_stream, &handshake).await?; + + // Receive handshake response + let response = Self::receive_message(&mut tls_stream).await?; + match response { + P2PMessage::Handshake { + agent_id: peer_id, + agent_name: peer_name, + .. + } => { + tracing::info!("Connected to peer: {} ({})", peer_name, peer_id); + } + _ => { + anyhow::bail!("Expected handshake message, got: {:?}", response); + } + } + + // Keep connection alive and handle messages + loop { + match Self::receive_message(&mut tls_stream).await { + Ok(message) => { + tracing::debug!("Received message: {:?}", message); + + // Handle different message types + match message { + P2PMessage::Heartbeat { + agent_id: peer_id, .. + } => { + tracing::debug!("Heartbeat from {}", peer_id); + + // Send heartbeat response + let response = P2PMessage::Response { + success: true, + message: "Heartbeat received".to_string(), + data: None, + }; + Self::send_message(&mut tls_stream, &response).await?; + } + P2PMessage::MetricsRequest { + agent_id: peer_id, .. + } => { + tracing::debug!("Metrics request from {}", peer_id); + + // TODO: Get current metrics and send them + let response = P2PMessage::Response { + success: true, + message: "Metrics data".to_string(), + data: Some(serde_json::json!({"status": "ok"})), + }; + Self::send_message(&mut tls_stream, &response).await?; + } + P2PMessage::MetricsShare { + agent_id: peer_id, + metrics, + .. + } => { + tracing::info!("Received metrics from {}: {:?}", peer_id, metrics); + } + _ => { + tracing::warn!("Unhandled message type: {:?}", message); + } + } + } + Err(e) => { + tracing::info!("Connection closed: {}", e); + break; + } + } + } + + Ok(()) + } + + /// Connect to a peer agent + pub async fn connect_to_peer( + &self, + peer_addr: &str, + ) -> Result> { + // Parse address + let addr: SocketAddr = peer_addr + .parse() + .context(format!("Invalid peer address: {}", peer_addr))?; + + tracing::info!("Connecting to peer at {}", addr); + + // Connect to peer + let stream = TcpStream::connect(addr) + .await + .context(format!("Failed to connect to {}", addr))?; + + // Extract hostname for SNI + let hostname = addr.ip().to_string(); + let server_name = ServerName::try_from(hostname) + .context("Invalid hostname")? + .to_owned(); + + // Perform TLS handshake + let mut tls_stream = match self.tls_connector.connect(server_name, stream).await { + Ok(s) => s, + Err(e) => { + anyhow::bail!("TLS handshake failed: {:?}", e); + } + }; + + tracing::info!("TLS handshake completed with {}", addr); + + // Receive handshake from server + let handshake = Self::receive_message(&mut tls_stream).await?; + match handshake { + P2PMessage::Handshake { + agent_id: peer_id, + agent_name: peer_name, + .. + } => { + tracing::info!("Connected to peer: {} ({})", peer_name, peer_id); + } + _ => { + anyhow::bail!("Expected handshake message"); + } + } + + // Send our handshake + let handshake = P2PMessage::Handshake { + agent_id: self.agent_id, + agent_name: self.agent_name.clone(), + timestamp: Utc::now(), + }; + Self::send_message(&mut tls_stream, &handshake).await?; + + Ok(tls_stream) + } + + /// Send a message over any TLS stream + async fn send_message(stream: &mut S, message: &P2PMessage) -> Result<()> + where + S: AsyncReadExt + AsyncWriteExt + Unpin, + { + let json = serde_json::to_string(message)?; + let len = json.len() as u32; + + // Send length prefix (4 bytes) + stream.write_all(&len.to_be_bytes()).await?; + // Send JSON data + stream.write_all(json.as_bytes()).await?; + stream.flush().await?; + + Ok(()) + } + + /// Receive a message from any TLS stream + async fn receive_message(stream: &mut S) -> Result + where + S: AsyncReadExt + AsyncWriteExt + Unpin, + { + // Read length prefix (4 bytes) + let mut len_bytes = [0u8; 4]; + stream.read_exact(&mut len_bytes).await?; + let len = u32::from_be_bytes(len_bytes) as usize; + + // Read JSON data + let mut buffer = vec![0u8; len]; + stream.read_exact(&mut buffer).await?; + + let message: P2PMessage = serde_json::from_slice(&buffer)?; + Ok(message) + } + + /// Send a heartbeat to a peer + pub async fn send_heartbeat(&self, stream: &mut S) -> Result<()> + where + S: AsyncReadExt + AsyncWriteExt + Unpin, + { + let heartbeat = P2PMessage::Heartbeat { + agent_id: self.agent_id, + timestamp: Utc::now(), + }; + + Self::send_message(stream, &heartbeat).await?; + + // Wait for response + let response = Self::receive_message(stream).await?; + match response { + P2PMessage::Response { + success, message, .. + } => { + if success { + tracing::debug!("Heartbeat acknowledged: {}", message); + } else { + anyhow::bail!("Heartbeat failed: {}", message); + } + } + _ => { + anyhow::bail!("Unexpected response to heartbeat"); + } + } + + Ok(()) + } + + /// Request metrics from a peer + #[allow(dead_code)] + pub async fn request_metrics(&self, stream: &mut S) -> Result + where + S: AsyncReadExt + AsyncWriteExt + Unpin, + { + let request = P2PMessage::MetricsRequest { + agent_id: self.agent_id, + timestamp: Utc::now(), + }; + + Self::send_message(stream, &request).await?; + + // Wait for response + let response = Self::receive_message(stream).await?; + match response { + P2PMessage::Response { + success, + data, + message, + .. + } => { + if success { + Ok(data.unwrap_or(serde_json::json!({}))) + } else { + anyhow::bail!("Metrics request failed: {}", message); + } + } + _ => { + anyhow::bail!("Unexpected response to metrics request"); + } + } + } +} diff --git a/agent/src/connect/mod.rs b/agent/src/connect/mod.rs new file mode 100644 index 00000000..b21e81be --- /dev/null +++ b/agent/src/connect/mod.rs @@ -0,0 +1,5 @@ +pub mod certs; +pub mod connector; + +pub use certs::ensure_certificates; +pub use connector::P2PConnector; diff --git a/agent/src/main.rs b/agent/src/main.rs index bd836fab..5aea2193 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -1,17 +1,22 @@ mod client; mod collector; mod config; +mod connect; use anyhow::Result; use chrono::Utc; use client::{AgentRegistration, Heartbeat, ServerClient}; use collector::MetricsCollector; use config::AgentConfig; +use connect::{ensure_certificates, P2PConnector}; use std::time::Duration; use tracing::{error, info, warn}; #[tokio::main] async fn main() -> Result<()> { + // Initialize rustls crypto provider + let _ = rustls::crypto::ring::default_provider().install_default(); + // Initialize logging tracing_subscriber::fmt() .with_env_filter( @@ -40,60 +45,159 @@ async fn main() -> Result<()> { info!("📝 Configuration loaded"); info!(" Agent ID: {}", config.agent_id); info!(" Name: {}", config.name); - info!(" Server: {}", config.server_url); + if !config.p2p_only_mode { + info!(" Server: {}", config.server_url); + } else { + info!(" Mode: P2P Only (no backend connection)"); + } - // Initialize components - let client = ServerClient::new(&config); - let mut collector = MetricsCollector::new(); + // Initialize P2P if enabled + let p2p_connector = if config.p2p.enabled { + info!("🔐 P2P connections enabled"); - // Register with server - info!("📡 Registering with server..."); - let registration = AgentRegistration { - agent_id: config.agent_id, - name: config.name.clone(), - hostname: hostname::get() - .ok() - .and_then(|h| h.into_string().ok()) - .unwrap_or_else(|| "unknown".to_string()), - os_type: std::env::consts::OS.to_string(), - os_version: sysinfo::System::os_version().unwrap_or_else(|| "Unknown".to_string()), - architecture: std::env::consts::ARCH.to_string(), - agent_version: env!("CARGO_PKG_VERSION").to_string(), - tags: config.tags.clone(), - }; + // Get certificate directory + let cert_dir = if let Some(parent) = std::path::Path::new(&config.p2p.cert_path).parent() { + parent.to_path_buf() + } else { + std::path::PathBuf::from(if cfg!(target_os = "windows") { + "C:\\ProgramData\\csf-agent\\certs" + } else { + "/etc/csf-agent/certs" + }) + }; - match client.register(®istration).await { - Ok(response) => { - info!("✅ Registration successful: {}", response.message); + // Ensure certificates exist + match ensure_certificates(&config.name, &cert_dir, config.p2p.auto_generate_certs) { + Ok(_) => info!("✅ Certificates ready"), + Err(e) => { + error!("❌ Failed to setup certificates: {}", e); + return Err(e); + } } - Err(e) => { - error!("❌ Registration failed: {}", e); - warn!(" Continuing anyway, will retry on next heartbeat..."); + + // Create P2P connector + match P2PConnector::new( + config.agent_id, + config.name.clone(), + config.p2p.listen_port, + std::path::Path::new(&config.p2p.cert_path), + std::path::Path::new(&config.p2p.key_path), + std::path::Path::new(&config.p2p.ca_cert_path), + ) { + Ok(connector) => { + info!( + "✅ P2P connector initialized on port {}", + config.p2p.listen_port + ); + Some(connector) + } + Err(e) => { + error!("❌ Failed to create P2P connector: {}", e); + return Err(e); + } + } + } else { + info!("ℹ️ P2P connections disabled"); + None + }; + + // Start P2P server if enabled + if let Some(ref connector) = p2p_connector { + let connector_clone = connector.clone(); + tokio::spawn(async move { + if let Err(e) = connector_clone.start_server().await { + error!("❌ P2P server error: {}", e); + } + }); + + // Connect to configured peers + for peer in &config.p2p.peers { + let connector_clone = connector.clone(); + let peer_addr = peer.clone(); + tokio::spawn(async move { + info!("🔗 Connecting to peer: {}", peer_addr); + match connector_clone.connect_to_peer(&peer_addr).await { + Ok(mut stream) => { + info!("✅ Connected to peer: {}", peer_addr); + + // Keep connection alive with heartbeats + let mut interval = tokio::time::interval(Duration::from_secs(30)); + loop { + interval.tick().await; + + if let Err(e) = connector_clone.send_heartbeat(&mut stream).await { + error!( + "❌ Heartbeat to {} failed: {}. Reconnecting...", + peer_addr, e + ); + break; + } + } + } + Err(e) => { + error!("❌ Failed to connect to peer {}: {}", peer_addr, e); + } + } + }); } } - // Spawn heartbeat task - let heartbeat_client = client.clone(); - let heartbeat_agent_id = config.agent_id; - let heartbeat_interval = config.heartbeat_interval; - tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(heartbeat_interval)); - loop { - interval.tick().await; - - let heartbeat = Heartbeat { - agent_id: heartbeat_agent_id, - timestamp: Utc::now(), - status: "online".to_string(), - }; - - if let Err(e) = heartbeat_client.send_heartbeat(&heartbeat).await { - error!("Failed to send heartbeat: {}", e); - } else { - info!("💓 Heartbeat sent"); + // Initialize components + let client = ServerClient::new(&config); + let mut collector = MetricsCollector::new(); + + // Register with server (skip if P2P only mode) + if !config.p2p_only_mode { + info!("📡 Registering with server..."); + let registration = AgentRegistration { + agent_id: config.agent_id, + name: config.name.clone(), + hostname: hostname::get() + .ok() + .and_then(|h| h.into_string().ok()) + .unwrap_or_else(|| "unknown".to_string()), + os_type: std::env::consts::OS.to_string(), + os_version: sysinfo::System::os_version().unwrap_or_else(|| "Unknown".to_string()), + architecture: std::env::consts::ARCH.to_string(), + agent_version: env!("CARGO_PKG_VERSION").to_string(), + tags: config.tags.clone(), + }; + + match client.register(®istration).await { + Ok(response) => { + info!("✅ Registration successful: {}", response.message); + } + Err(e) => { + error!("❌ Registration failed: {}", e); + warn!(" Continuing anyway, will retry on next heartbeat..."); } } - }); + + // Spawn heartbeat task + let heartbeat_client = client.clone(); + let heartbeat_agent_id = config.agent_id; + let heartbeat_interval = config.heartbeat_interval; + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(heartbeat_interval)); + loop { + interval.tick().await; + + let heartbeat = Heartbeat { + agent_id: heartbeat_agent_id, + timestamp: Utc::now(), + status: "online".to_string(), + }; + + if let Err(e) = heartbeat_client.send_heartbeat(&heartbeat).await { + error!("Failed to send heartbeat: {}", e); + } else { + info!("💓 Heartbeat sent"); + } + } + }); + } else { + info!("ℹ️ Backend connection disabled (P2P only mode)"); + } // Main metrics collection loop info!("📊 Starting metrics collection..."); @@ -106,17 +210,19 @@ async fn main() -> Result<()> { let metrics = collector.collect(config.agent_id); info!( - "📈 Collected metrics - CPU: {:.1}% | RAM: {:.1}% | Disk: {:.1}%", + "📈 Metrics - CPU: {:.1}% | RAM: {:.1}% | Disk: {:.1}%", metrics.cpu_usage_percent, metrics.memory_usage_percent, metrics.disk_usage_percent ); - // Send to server - match client.send_metrics(&metrics).await { - Ok(_) => { - info!("✅ Metrics sent to server"); - } - Err(e) => { - error!("❌ Failed to send metrics: {}", e); + // Send to server (skip if P2P only mode) + if !config.p2p_only_mode { + match client.send_metrics(&metrics).await { + Ok(_) => { + info!("✅ Metrics sent to server"); + } + Err(e) => { + error!("❌ Failed to send metrics: {}", e); + } } } } diff --git a/agent/test-p2p.sh b/agent/test-p2p.sh new file mode 100755 index 00000000..7cf4b94f --- /dev/null +++ b/agent/test-p2p.sh @@ -0,0 +1,136 @@ +#!/bin/bash + +echo "🧪 P2P mTLS Test" +echo "=================" + +# Cleanup +echo "🧹 Cleanup old test files..." +rm -rf test-agent1 test-agent2 +mkdir -p test-agent1/certs test-agent2/certs + +# Build +echo "🔨 Building agent..." +cargo build --release +if [ $? -ne 0 ]; then + echo "❌ Build failed" + exit 1 +fi + +# Create configs +echo "📝 Creating configurations..." + +CURRENT_DIR=$(pwd) + +cat > test-agent1/config.toml << EOF +agent_id = "11111111-1111-1111-1111-111111111111" +name = "test-agent-1" +server_url = "http://localhost:8000" +api_key = "test-key" +p2p_only_mode = true +collection_interval = 30 +heartbeat_interval = 60 +tags = [] + +[p2p] +enabled = true +listen_port = 8443 +peers = ["127.0.0.1:8444"] +cert_path = "$CURRENT_DIR/test-agent1/certs/agent.crt" +key_path = "$CURRENT_DIR/test-agent1/certs/agent.key" +ca_cert_path = "$CURRENT_DIR/test-agent1/certs/ca.crt" +auto_generate_certs = true +EOF + +cat > test-agent2/config.toml << EOF +agent_id = "22222222-2222-2222-2222-222222222222" +name = "test-agent-2" +server_url = "http://localhost:8000" +api_key = "test-key" +p2p_only_mode = true +collection_interval = 30 +heartbeat_interval = 60 +tags = [] + +[p2p] +enabled = true +listen_port = 8444 +peers = ["127.0.0.1:8443"] +cert_path = "$CURRENT_DIR/test-agent2/certs/agent.crt" +key_path = "$CURRENT_DIR/test-agent2/certs/agent.key" +ca_cert_path = "$CURRENT_DIR/test-agent2/certs/ca.crt" +auto_generate_certs = true +EOF + +# Start Agent 1 +echo "🚀 Starting Agent 1 (port 8443)..." +cd test-agent1 +RUST_LOG=info ../target/release/csf-agent > agent.log 2>&1 & +AGENT1_PID=$! +cd .. +echo " PID: $AGENT1_PID" + +# Wait for Agent 1 to start and generate certs +echo "⏳ Waiting for Agent 1 to generate certificates..." +sleep 4 + +# Copy CA cert from Agent 1 to Agent 2 +if [ -f "test-agent1/certs/ca.crt" ]; then + echo "📋 Sharing CA certificate with Agent 2..." + cp test-agent1/certs/ca.crt test-agent2/certs/ + cp test-agent1/certs/ca.key test-agent2/certs/ + echo "✅ CA certificate shared" +else + echo "❌ CA cert not found at test-agent1/certs/ca.crt" + echo " Check Agent 1 log for errors" + tail -10 test-agent1/agent.log + exit 1 +fi + +# Start Agent 2 +echo "🚀 Starting Agent 2 (port 8444)..." +cd test-agent2 +RUST_LOG=info ../target/release/csf-agent > agent.log 2>&1 & +AGENT2_PID=$! +cd .. +echo " PID: $AGENT2_PID" + +# Wait for connection +echo "⏳ Waiting for connection (10 seconds)..." +sleep 10 + +# Check logs +echo "" +echo "📊 Agent 1 Log:" +echo "===============" +tail -25 test-agent1/agent.log + +echo "" +echo "📊 Agent 2 Log:" +echo "===============" +tail -25 test-agent2/agent.log + +echo "" +echo "🔍 Checking for successful P2P connection..." +if grep -q "Connected to peer" test-agent1/agent.log && grep -q "Connected to peer" test-agent2/agent.log; then + echo "✅ SUCCESS! Both agents connected via mTLS" +elif grep -q "P2P server listening" test-agent1/agent.log && grep -q "P2P server listening" test-agent2/agent.log; then + echo "⚠️ Servers running but connection pending. Check logs for details." +else + echo "❌ Connection not established" +fi + +echo "" +echo "📝 Process Info:" +echo " Agent 1 PID: $AGENT1_PID" +echo " Agent 2 PID: $AGENT2_PID" +echo "" +echo "🛑 To stop agents:" +echo " kill $AGENT1_PID $AGENT2_PID" +echo "" +echo "📖 To follow logs:" +echo " tail -f test-agent1/agent.log" +echo " tail -f test-agent2/agent.log" +echo "" +echo "🧹 To cleanup:" +echo " kill $AGENT1_PID $AGENT2_PID 2>/dev/null" +echo " rm -rf test-agent1 test-agent2" diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 7260c093..14fa5da6 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -434,7 +434,7 @@ dependencies = [ [[package]] name = "backend" -version = "0.2.0" +version = "0.2.2" dependencies = [ "anyhow", "async-trait", diff --git a/docker-compose.registry.yml b/docker-compose.registry.yml new file mode 100644 index 00000000..1d86ce39 --- /dev/null +++ b/docker-compose.registry.yml @@ -0,0 +1,42 @@ +# Docker Compose configuration using GitHub Container Registry images +# This file uses the published Docker images from ghcr.io + +version: '3.8' + +services: + backend: + image: ghcr.io/cs-foundry/csf-core-backend:latest + container_name: csf-backend + ports: + - "8000:8000" + environment: + - DATABASE_URL=sqlite:/data/finance.db + - RUST_LOG=info + - JWT_SECRET=${JWT_SECRET:-changeme} + - FRONTEND_URL=http://frontend:80 + volumes: + - csf_data:/data + restart: unless-stopped + healthcheck: + test: ["CMD", "/app/backend", "--health-check"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + frontend: + image: ghcr.io/cs-foundry/csf-core-frontend:latest + container_name: csf-frontend + ports: + - "3000:80" + restart: unless-stopped + depends_on: + - backend + +volumes: + csf_data: + driver: local + +networks: + default: + name: csf-network diff --git a/nixos-node/.gitignore b/nixos-node/.gitignore new file mode 100644 index 00000000..3988d69e --- /dev/null +++ b/nixos-node/.gitignore @@ -0,0 +1,26 @@ +# Nix build results +result +result-* + +# Flake lock file (uncommit if you want to pin versions) +# flake.lock + +# ISO images +*.iso + +# QEMU disk images +*.qcow2 + +# Nix temporary files +.nix-defexpr +.nix-profile + +# VM state +*.sock +*.pid + +# Build logs +build.log +nix-build.log + +keys/ diff --git a/nixos-node/DEPLOYMENT.md b/nixos-node/DEPLOYMENT.md new file mode 100644 index 00000000..63a6bb5c --- /dev/null +++ b/nixos-node/DEPLOYMENT.md @@ -0,0 +1,177 @@ +# CSF-Core NixOS Deployment Guide + +## Voraussetzungen + +1. **NixOS auf dem Zielserver installiert** (z.B. mit der ISO aus diesem Projekt) +2. **SSH-Zugriff als root** mit Key-basierter Authentifizierung +3. **Nix mit Flakes** auf deinem Mac installiert + +## Schritt 1: SSH-Key einrichten + +Falls noch nicht vorhanden, generiere einen SSH-Key: + +```bash +ssh-keygen -t ed25519 -C "csf-deployment@mac" +``` + +Kopiere den Public Key auf den Server: + +```bash +ssh-copy-id root@dein-server +# oder: +cat ~/.ssh/id_ed25519.pub | ssh root@dein-server "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys" +``` + +## Schritt 2: Konfiguration anpassen + +Bearbeite [`modules/server-configuration.nix`](modules/server-configuration.nix): + +1. **SSH Public Key eintragen** (Zeile 51): + + ```nix + users.users.root = { + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1... dein-key-hier" + ]; + }; + ``` + +2. **Netzwerk-Konfiguration anpassen** (Zeile 21-35): + - Interface-Name (`eth0` → dein Interface) + - IP-Adresse (falls statisch gewünscht) + - Gateway und DNS + +3. **Boot-Loader anpassen** (Zeile 8-13): + - BIOS: `device = "/dev/sda";` + - UEFI: Kommentare entfernen und anpassen + +## Schritt 3: Deployment + +### Remote-Deployment von deinem Mac: + +```bash +cd /Volumes/CedricExterne/Coding/CSF-Core/nixos-node + +# Deployment auf den Server +nixos-rebuild switch --flake .#csf-server --target-host root@dein-server --use-remote-sudo +``` + +**Optionen:** + +- `--flake .#csf-server`: Verwendet die `csf-server`-Konfiguration aus der flake.nix +- `--target-host root@dein-server`: Deployment-Ziel (ersetze `dein-server` mit IP oder Hostname) +- `--use-remote-sudo`: Verwendet sudo auf dem Remote-Server (falls benötigt) +- `--build-host localhost`: Build lokal auf dem Mac (statt auf dem Server) + +### Lokales Build + Deployment (schneller): + +```bash +# 1. Build lokal +nix build .#nixosConfigurations.csf-server.config.system.build.toplevel + +# 2. Auf Server kopieren und aktivieren +nixos-rebuild switch --flake .#csf-server --target-host root@dein-server --build-host localhost +``` + +## Schritt 4: Verifizierung + +Nach dem Deployment, teste die Installation: + +```bash +# SSH auf den Server +ssh root@dein-server + +# Test-Script ausführen +./test-docker.sh + +# Oder manuell testen +systemctl status docker +docker ps +curl http://localhost:8080 +``` + +Von deinem Mac aus: + +```bash +# Nginx testen (ersetze IP) +curl http://dein-server:8080 +curl http://dein-server:8080/health +``` + +## Schritt 5: Kontinuierliche Updates + +Nach Änderungen an der Konfiguration: + +```bash +# Änderungen committen (optional, aber empfohlen) +git add modules/server-configuration.nix +git commit -m "Update server config" + +# Deployment +nixos-rebuild switch --flake .#csf-server --target-host root@dein-server +``` + +## Troubleshooting + +### SSH-Verbindung schlägt fehl + +```bash +# Test SSH-Verbindung +ssh -v root@dein-server + +# Prüfe authorized_keys auf dem Server +ssh root@dein-server "cat ~/.ssh/authorized_keys" +``` + +### Build schlägt fehl + +```bash +# Lokaler Test-Build +nix build .#nixosConfigurations.csf-server.config.system.build.toplevel --show-trace +``` + +### Konfiguration validieren + +```bash +# Syntax-Check ohne Deployment +nixos-rebuild dry-build --flake .#csf-server --target-host root@dein-server +``` + +### Rollback bei Problemen + +```bash +# Auf dem Server: Zur vorherigen Generation zurückkehren +ssh root@dein-server +nixos-rebuild switch --rollback +``` + +## Alternative: Lokales Deployment + +Falls du direkten Zugriff auf den Server hast: + +```bash +# 1. Repo auf den Server klonen +ssh root@dein-server +git clone https://github.com/CS-Foundry/CSF-Core.git /etc/nixos/csf-core + +# 2. Lokal auf dem Server bauen und aktivieren +cd /etc/nixos/csf-core/nixos-node +nixos-rebuild switch --flake .#csf-server +``` + +## Architektur-Wechsel + +Für ARM-Server (Raspberry Pi, etc.), ändere in `flake.nix`: + +```nix +nixosConfigurations.csf-server = nixpkgs.lib.nixosSystem { + system = "aarch64-linux"; # statt "x86_64-linux" + modules = [ ./modules/server-configuration.nix ]; +}; +``` + +Dann deployment: + +```bash +nixos-rebuild switch --flake .#csf-server --target-host root@raspberry-pi --build-host localhost +``` diff --git a/nixos-node/README.md b/nixos-node/README.md new file mode 100644 index 00000000..49368d75 --- /dev/null +++ b/nixos-node/README.md @@ -0,0 +1,131 @@ +# CSF-Core Docker Test ISO + +Diese Konfiguration erstellt ein bootfähiges NixOS ISO-Image mit Docker und Docker Compose für einfache Container-Tests. + +## 🚀 ISO bauen + +### Mit Flakes (empfohlen) + +```bash +cd nixos-node/ +nix build .#nixosConfigurations.iso.config.system.build.isoImage +``` + +Das ISO-Image wird unter `./result/iso/` erstellt. + +## 📦 Was ist enthalten? + +- **Docker & Docker Compose** - Container-Management und Orchestrierung +- **Nginx Test Container** - Über docker-compose automatisch gestartet auf Port 8080 +- **Test-Script** - Automatische Tests für Docker-Funktionalität + +## 🔧 Konfiguration + +### Ports + +- `8080` - Nginx Test Container + +### Automatisch gestartete Services + +- Docker Daemon +- Docker Compose mit nginx Container + +## 🎯 Verwendung + +### 1. ISO auf USB-Stick schreiben + +```bash +sudo dd if=result/iso/*.iso of=/dev/sdX bs=4M status=progress +``` + +### 2. System booten + +Boote von dem USB-Stick. Das System startet automatisch als Root und Docker Compose startet den nginx Container. + +### 3. Container testen + +```bash +# Webseite öffnen +curl http://localhost:8080 + +# Container-Status prüfen +docker ps -a + +# Docker Compose Status +docker-compose ps +``` + +### 4. Vollständiger Test + +```bash +# Führe das bereitgestellte Test-Script aus +./test-docker.sh +``` + +## 🐳 Docker Tests + +### Container verwalten + +```bash +# Container stoppen +docker-compose down + +# Container neu starten +docker-compose up -d + +# Logs ansehen +docker-compose logs +``` + +### Eigene Container testen + +```bash +# Eigenes docker-compose.yml erstellen +cat > test-compose.yml < /dev/null 2>&1; then + echo "❌ Cannot connect to $TARGET_HOST" + exit 1 +fi +echo "✅ SSH connection successful" +echo "" + +# Kopiere Flake zum Server +echo "📦 Copying flake to remote server..." +REMOTE_DIR="/tmp/csf-nixos-deploy-$$" +ssh "$TARGET_HOST" "mkdir -p $REMOTE_DIR" +rsync -az --delete \ + --exclude='.git' \ + --exclude='*.qcow2' \ + --exclude='result' \ + ./ "$TARGET_HOST:$REMOTE_DIR/" +echo "✅ Flake copied" +echo "" + +# Führe nixos-rebuild auf dem Server aus +echo "🔨 Building and activating configuration on remote server..." +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +ssh -t "$TARGET_HOST" "cd $REMOTE_DIR && sudo nixos-rebuild switch --flake .#$FLAKE_NAME" + +# Cleanup +echo "" +echo "🧹 Cleaning up..." +ssh "$TARGET_HOST" "rm -rf $REMOTE_DIR" + +echo "" +echo "✅ Deployment complete!" +echo "" +echo "📝 Next steps:" +echo " - Test Docker: ssh $TARGET_HOST 'docker --version'" +echo " - Run test script: ssh $TARGET_HOST 'sudo /root/test-docker.sh'" +echo " - Check nginx: curl http://192.168.1.36:8080" +nixos-version +echo "" +echo "Running containers:" +docker ps +EOF + +echo "" +echo "🎉 Deployment completed successfully!" +echo "" +echo "Test the deployment:" +echo " curl http://192.168.1.36:8080" +echo " ssh ${SERVER_HOST} ./test-docker.sh" diff --git a/nixos-node/flake.lock b/nixos-node/flake.lock new file mode 100644 index 00000000..fe08f566 --- /dev/null +++ b/nixos-node/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1751274312, + "narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nixos-node/flake.nix b/nixos-node/flake.nix new file mode 100644 index 00000000..7161f8ae --- /dev/null +++ b/nixos-node/flake.nix @@ -0,0 +1,32 @@ +{ + description = "CSF-Core Master Node NixOS Configuration"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; + }; + + outputs = { self, nixpkgs }: { + # ISO-Konfiguration (für Installation) + nixosConfigurations.iso = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + ./modules/iso-configuration.nix + ]; + }; + + # Server-Konfiguration (für Deployment) + nixosConfigurations.csf-server = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; # oder "aarch64-linux" für ARM/Raspberry Pi + modules = [ + ./modules/server-configuration.nix + ]; + }; + + # Build commands: + # ISO: nix build .#nixosConfigurations.iso.config.system.build.isoImage + packages.x86_64-linux = { + iso = self.nixosConfigurations.iso.config.system.build.isoImage; + default = self.nixosConfigurations.iso.config.system.build.isoImage; + }; + }; +} diff --git a/nixos-node/logo.txt b/nixos-node/logo.txt new file mode 100644 index 00000000..eeaf58fc --- /dev/null +++ b/nixos-node/logo.txt @@ -0,0 +1,44 @@ + + + + ..,,,,,,,,,,,,,,,,,,,,,,,;,,,,,,,,,,,,,,'.. . + ..ckXXNNNNNNNNNNNNNNNNNNNNNNNNNNXXXXXXXXKx;. + ..cONWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWOc.. + ..ckNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMW0l.. . .. + ..ckNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMW0l.. . + ..ckXWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMW0l.. + . .;kXWMMMMMMMMWKOkkkkkkkkkkkkkkkkkkkkkkkkxc.. . + .oNWMMMMMMMMNx,.......................... + .oNMMMMMMMMMK:. + .oNMMMMMMMMMK; ......................... + .oNMMMMMMMMMK; .'lddddddddddddddddddddddc'. . + .oNMMMMMMMMMK; .'o0NWWWWWWWWWWWWWWWWWWWWKd,. + .oNMMMMMMMMMK; .'l0NWMMMMMMMMMMMMMMMMMMWXx,. + .oNMMMMMMMMMK; .'l0NWMMMMMMMMMMMMMMMMMMWXx;. + .oNWMMMMMMMMK:'l0NWMMMMMMMMMMMMMMMMMMWXx;. + .cKWMMMMMMMMXOOXWMMMMWWWWWWWWWWWWWWWXx;. + .,dKWMMMMMMXo;lKMMMNOl:;;;;;;;;;;;;,. + .,dKWMMMM0; 'kWMMMNOl'. + .,dKWMM0; 'kWMMMMMN0o,... + .,dKW0; 'kWMMMMMMMWKkc. + .,ox, 'kWMMMMMMMMNXo. + ... 'kWMMMMMMMMNXd. + 'kWMMMMMMMMNXd. + 'kWMMMMMMMMNXd. + 'kWMMMMMMMMNXo. + 'kWMMMMMMMMNXo. + 'kWMMMMMMMMNXo. + 'kWMMMMMMMNkl;. + 'kWMMMMMNk:... + 'kWMMMNk:. + 'kWWXx;. + . 'kKx;. + .;,. + .. + + + + + + + diff --git a/nixos-node/modules/iso-configuration.nix b/nixos-node/modules/iso-configuration.nix new file mode 100644 index 00000000..b4c488a9 --- /dev/null +++ b/nixos-node/modules/iso-configuration.nix @@ -0,0 +1,228 @@ +{ config, pkgs, lib, ... }: + +{ + imports = [ + + ]; + + # System configuration + system.stateVersion = "24.11"; + + # Networking + networking = { + hostName = "csf-docker-test"; + firewall = { + enable = true; + allowedTCPPorts = [ + 8080 # Test nginx container + ]; + }; + }; + + # Enable Docker + virtualisation.docker.enable = true; + + # System packages + environment.systemPackages = with pkgs; [ + # Docker tools + docker-compose + docker + + # Utilities + curl + wget + vim + htop + ]; + + # Auto-login as root on boot (for ISO convenience) + services.getty.autologinUser = "root"; + + # Docker Compose service for nginx test + systemd.services.docker-compose-test = { + description = "Docker Compose Test Service (nginx)"; + after = [ "docker.service" ]; + requires = [ "docker.service" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + WorkingDirectory = "/etc/docker-test"; + ExecStart = "${pkgs.docker-compose}/bin/docker-compose up -d"; + ExecStop = "${pkgs.docker-compose}/bin/docker-compose down"; + }; + }; + + # Activation script to setup Docker Compose + system.activationScripts.docker-setup = { + text = '' + # Create docker-compose directory + mkdir -p /etc/docker-test + + # Create docker-compose.yml + cat > /etc/docker-test/docker-compose.yml < /etc/docker-test/nginx.conf < /etc/docker-test/html/index.html < + + + CSF-Core Docker Test + + + +
+

CSF-Core Docker Test

+

Docker & Docker Compose funktionieren!

+

Diese Seite wird von nginx in einem Docker Container serviert.

+

Health Check

+
+ + +EOF + + # Create test script + cat > /root/test-docker.sh <.*' || echo "Port 8080 not responding" +echo "" +echo "Health check:" +curl -s http://localhost:8080/health || echo "Health check failed" +echo "" +echo "=== Test Complete ===" +EOF + chmod +x /root/test-docker.sh + ''; + deps = []; + }; + + # Boot message with logo + environment.etc."issue".text = '' + + + + + ..,,,,,,,,,,,,,,,,,,,,,,,;,,,,,,,,,,,,,,'.. . + ..ckXXNNNNNNNNNNNNNNNNNNNNNNNNNNXXXXXXXXKx;. + ..cONWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWOc.. + ..ckNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMW0l.. . .. + ..ckNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMW0l.. . + ..ckXWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMW0l.. + . .;kXWMMMMMMMMWKOkkkkkkkkkkkkkkkkkkkkkkkkxc.. . + .oNWMMMMMMMMNx,.......................... + .oNMMMMMMMMMK:. + .oNMMMMMMMMMK; ......................... + .oNMMMMMMMMMK; .'lddddddddddddddddddddddc'. . + .oNMMMMMMMMMK; .'o0NWWWWWWWWWWWWWWWWWWWWKd,. + .oNMMMMMMMMMK; .'l0NWMMMMMMMMMMMMMMMMMMWXx,. + .oNMMMMMMMMMK; .'l0NWMMMMMMMMMMMMMMMMMMWXx;. + .oNWMMMMMMMMK:'l0NWMMMMMMMMMMMMMMMMMMWXx;. + .cKWMMMMMMMMXOOXWMMMMWWWWWWWWWWWWWWWXx;. + .,dKWMMMMMMXo;lKMMMNOl:;;;;;;;;;;;;,. + .,dKWMMMM0; 'kWMMMNOl'. + .,dKWMM0; 'kWMMMMMN0o,... + .,dKW0; 'kWMMMMMMMWKkc. + .,ox, 'kWMMMMMMMMNXo. + ... 'kWMMMMMMMMNXd. + 'kWMMMMMMMMNXd. + 'kWMMMMMMMMNXd. + 'kWMMMMMMMMNXo. + 'kWMMMMMMMMNXo. + 'kWMMMMMMMMNXo. + 'kWMMMMMMMNkl;. + 'kWMMMMMNk:... + 'kWMMMNk:. + 'kWWXx;. + . 'kKx;. + .;,. + .. + + + + + + + + + ╔═══════════════════════════════════════════════════════════╗ + ║ ║ + ║ CSF-Core Docker Test ISO ║ + ║ ║ + ║ Einfache Docker & Docker Compose Testumgebung ║ + ║ ║ + ║ Services: ║ + ║ - Docker: systemctl status docker ║ + ║ - Nginx Test: http://localhost:8080 ║ + ║ ║ + ║ Test commands: ║ + ║ ./test-docker.sh - Run comprehensive test ║ + ║ docker ps -a - List containers ║ + ║ docker-compose ps - Compose status ║ + ║ ║ + ╚═══════════════════════════════════════════════════════════╝ + + ''; +} \ No newline at end of file diff --git a/nixos-node/modules/server-configuration.nix b/nixos-node/modules/server-configuration.nix new file mode 100644 index 00000000..69e0cf16 --- /dev/null +++ b/nixos-node/modules/server-configuration.nix @@ -0,0 +1,274 @@ +{ config, pkgs, lib, ... }: + +{ + # System configuration - WICHTIG: Muss mit der ursprünglichen Installation übereinstimmen! + system.stateVersion = "25.11"; + + # Boot configuration + boot = { + loader.grub = { + enable = true; + device = "/dev/sda"; + useOSProber = true; + }; + + # Hardware-spezifische Einstellungen (von hardware-configuration.nix) + initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ]; + initrd.kernelModules = [ ]; + kernelModules = [ ]; + extraModulePackages = [ ]; + }; + + # File Systems (von hardware-configuration.nix) + fileSystems."/" = { + device = "/dev/disk/by-uuid/e4b27226-e75f-4cef-9dec-fc0c6f2185ac"; + fsType = "ext4"; + }; + + swapDevices = [ ]; + + # Platform + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + + # Networking + networking = { + hostName = "nixos"; # Match existing hostname + + # NetworkManager aktivieren (wie auf dem Zielsystem) + networkmanager.enable = true; + + firewall = { + enable = true; + allowedTCPPorts = [ + 22 # SSH + 80 # HTTP + 443 # HTTPS + 8080 # Docker nginx test + ]; + }; + }; + + # Time zone + time.timeZone = "Europe/Berlin"; + + # Locale settings + i18n.defaultLocale = "de_DE.UTF-8"; + i18n.extraLocaleSettings = { + LC_ADDRESS = "de_DE.UTF-8"; + LC_IDENTIFICATION = "de_DE.UTF-8"; + LC_MEASUREMENT = "de_DE.UTF-8"; + LC_MONETARY = "de_DE.UTF-8"; + LC_NAME = "de_DE.UTF-8"; + LC_NUMERIC = "de_DE.UTF-8"; + LC_PAPER = "de_DE.UTF-8"; + LC_TELEPHONE = "de_DE.UTF-8"; + LC_TIME = "de_DE.UTF-8"; + }; + + # Console keymap + console.keyMap = "de"; + + # X11 keymap + services.xserver.xkb = { + layout = "de"; + variant = ""; + }; + + # SSH Server für Remote-Zugriff + services.openssh = { + enable = true; + settings = { + PermitRootLogin = "yes"; # Match existing config + PasswordAuthentication = true; # Match existing config + }; + }; + + # Bestehenden User rootcsf übernehmen + users.users.rootcsf = { + isNormalUser = true; + description = "rootcsf"; + extraGroups = [ "networkmanager" "wheel" "docker" ]; + packages = with pkgs; []; + }; + + # Sudo ohne Passwort für wheel-Gruppe (für automatisiertes Deployment) + security.sudo.wheelNeedsPassword = false; + + # GnuPG Agent (wie auf dem Zielsystem) + programs.mtr.enable = true; + programs.gnupg.agent = { + enable = true; + enableSSHSupport = true; + }; + + # Docker aktivieren + virtualisation.docker = { + enable = true; + enableOnBoot = true; + }; + + # System packages + environment.systemPackages = with pkgs; [ + # Docker tools + docker-compose + + # System utilities + curl + wget + vim + htop + git + tmux + + # Debugging tools + lsof + netcat + tcpdump + ]; + + # Docker Compose service for nginx test + systemd.services.docker-compose-test = { + description = "Docker Compose Test Service (nginx)"; + after = [ "docker.service" "network-online.target" ]; + requires = [ "docker.service" "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + WorkingDirectory = "/etc/docker-test"; + ExecStart = "${pkgs.docker-compose}/bin/docker-compose up -d"; + ExecStop = "${pkgs.docker-compose}/bin/docker-compose down"; + Restart = "on-failure"; + }; + }; + + # Activation script to setup Docker Compose + system.activationScripts.docker-setup = { + text = '' + # Create docker-compose directory + mkdir -p /etc/docker-test + + # Create docker-compose.yml + cat > /etc/docker-test/docker-compose.yml <<'EOF' +version: '3.8' +services: + nginx-test: + image: nginx:alpine + ports: + - "8080:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./html:/usr/share/nginx/html:ro + restart: unless-stopped + +volumes: + nginx-logs: +EOF + + # Create nginx config + cat > /etc/docker-test/nginx.conf <<'EOF' +events { + worker_connections 1024; +} + +http { + server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html; + } + + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } +} +EOF + + # Create HTML content + mkdir -p /etc/docker-test/html + cat > /etc/docker-test/html/index.html <<'EOF' + + + + CSF-Core Server + + + +
+

CSF-Core Server läuft!

+

Docker & Docker Compose funktionieren!

+

Diese Seite wird von nginx in einem Docker Container serviert.

+

Health Check

+
+ + +EOF + + # Create test script + cat > /root/test-docker.sh <<'EOF' +#!/bin/bash +echo "=== CSF-Core Docker Test ===" +echo "Hostname: $(hostname)" +echo "Date: $(date)" +echo "" +echo "Docker version:" +docker --version +echo "" +echo "Docker Compose version:" +docker-compose --version +echo "" +echo "Docker images:" +docker images +echo "" +echo "Running containers:" +docker ps +echo "" +echo "Docker Compose status:" +cd /etc/docker-test && docker-compose ps +echo "" +echo "=== Network Test ===" +echo "Testing nginx container:" +curl -s http://localhost:8080 | grep -o '.*' || echo "Port 8080 not responding" +echo "" +echo "Health check:" +curl -s http://localhost:8080/health || echo "Health check failed" +echo "" +echo "=== Test Complete ===" +EOF + chmod +x /root/test-docker.sh + ''; + deps = []; + }; + + # Automatic updates (optional, aber empfohlen) + system.autoUpgrade = { + enable = false; # Auf true setzen für automatische Updates + dates = "04:00"; + allowReboot = false; + }; + + # Nix settings + nix = { + settings = { + experimental-features = [ "nix-command" "flakes" ]; + auto-optimise-store = true; + }; + gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; + }; +}