Compare commits
248 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eac05ad1eb | |||
| df2f4c70c9 | |||
| 6a4c4c95a4 | |||
| efaac33c47 | |||
| d0c0790cbf | |||
| 130206a3a3 | |||
| 913d7286e1 | |||
| 3c70b15831 | |||
| 04a16ab862 | |||
| 624f9017b3 | |||
| 70f2666c2e | |||
| 06732559b9 | |||
| aa5b993205 | |||
| cfdb874062 | |||
| fe9bdce3c1 | |||
| 734b55b292 | |||
| c629c5b710 | |||
| 5349cbbd05 | |||
| 80f8f4fed2 | |||
| a3b299b116 | |||
| 2d33973b5f | |||
| 6ddb511cb0 | |||
| cc21868b6c | |||
| 32803ff27c | |||
| 0bca0c7784 | |||
| 2ac40076f5 | |||
| 4375f915ca | |||
| 0cc752ff3e | |||
| ae515ecb3a | |||
| e80437ad06 | |||
| 8fe6e0a72f | |||
| 8a9e9190e6 | |||
| 1322598581 | |||
| 48ec57581e | |||
| 2f73237fd6 | |||
| 904654212f | |||
| 1fb9962c22 | |||
| f1602fde4c | |||
| 0ffdb0eb2d | |||
| 5a6165a7fe | |||
| fa01785632 | |||
| 2aa504c087 | |||
| cc67edab12 | |||
| 135c91d256 | |||
| 7f5b0c2313 | |||
| 6fab250ea8 | |||
| 58ad92d431 | |||
| d682c7c69c | |||
| ee46c48c0b | |||
| 21d01179d6 | |||
| 1e4c8e4dc2 | |||
| 891ca09f34 | |||
| 551d73204f | |||
| 07a073fb28 | |||
| b8900d1eae | |||
| dfc2370540 | |||
| 1dfea9bbde | |||
| aa721963b3 | |||
| 63b0bfce34 | |||
| f428a7cc1e | |||
| 45e28e8911 | |||
| f3fb84927a | |||
| b6809dc935 | |||
| 13da27364b | |||
| 6f6be7ef0c | |||
| 6a41eba9d8 | |||
| 20b214eb9f | |||
| 48fb8752c9 | |||
| d4f9f1bf7f | |||
| 0de47b966b | |||
| 64187b03bd | |||
| f5eb2286a9 | |||
| f57d92406f | |||
| 286f9059e2 | |||
| c3cde6745d | |||
| 1dc49bb76a | |||
| 175c21600c | |||
| 5082c21403 | |||
| f2214e3eb4 | |||
| 8bfa5f2273 | |||
| a08145ed9e | |||
| 5c670cbd0c | |||
| 75ec2b8e3c | |||
| 949cbb2632 | |||
| 432e6785b2 | |||
| 18bf40e78b | |||
| 28f3171ca3 | |||
| 8e7fa118f4 | |||
| d499824457 | |||
| 137094f56c | |||
| d28fd6ff16 | |||
| 0b8c354b3f | |||
| 165db77a14 | |||
| 385c675736 | |||
| e8d568eb19 | |||
| 42e2f8989a | |||
| 8a80a887e1 | |||
| 9098f34742 | |||
| 16fc7afd69 | |||
| 06d338f41c | |||
| 1dea4383f1 | |||
| 64e7e787f5 | |||
| 3e037f2648 | |||
| 2e00f1a160 | |||
| 296fa72223 | |||
| 705779d7ac | |||
| b4522ff2ab | |||
| bbc052947e | |||
| 7a9fb1ac55 | |||
| b2ace87ee9 | |||
| e9c9a949f9 | |||
| 4d0c5ea1a8 | |||
| 4f2c68bad2 | |||
| 09846848c6 | |||
| 9cb48a01eb | |||
| 3723d97427 | |||
| 3326fa4445 | |||
| 79b7080237 | |||
| bac1947e14 | |||
| c5e3b682f0 | |||
| 20cb6dfaee | |||
| e3064ae60d | |||
| f346793a25 | |||
| 44359c23ff | |||
| 5f5a79100f | |||
| 5c4c599c3a | |||
| 4433c90390 | |||
| 89e2b01eef | |||
| 78134210a2 | |||
| d6748fa261 | |||
| e6f1d9c863 | |||
| 96d31520b9 | |||
| 0c965d089c | |||
| fafab7ee1d | |||
| 999335d231 | |||
| ec9d887d02 | |||
| 2a2ddb329e | |||
| df504e1c0a | |||
| cf259403ad | |||
| eb8f2dc150 | |||
| 185b3901a6 | |||
| c78e2b1df9 | |||
| 44a5559a11 | |||
| ae5f998cf5 | |||
| 42b36ad319 | |||
| e351e4e30c | |||
| 710ee85c3e | |||
| 5665be0d6d | |||
| 0b38f54a5d | |||
| bb305ba74a | |||
| 8df45476a3 | |||
| 0beacdfbd2 | |||
| 53155eeb2e | |||
| 488894357a | |||
| 33a31e349f | |||
| cf6c15b0fc | |||
| a53819b996 | |||
| 097e44bace | |||
| 8f2d1972f7 | |||
| c5fb03c1c4 | |||
| 0886ba248a | |||
| 53ceca729a | |||
| 637683e6d0 | |||
| 8da407f9f2 | |||
| 1ee46b97ce | |||
| 738fee0717 | |||
| e9f47e4ed5 | |||
| 9835ea2aa0 | |||
| 45ce4c435f | |||
| 20760b139e | |||
| 3799c3c051 | |||
| ef34786c11 | |||
| ed055b3b44 | |||
| 3c9b31d575 | |||
| d0dbf50795 | |||
| 28a1830c9c | |||
| f8153d0b01 | |||
| b5eda96fd4 | |||
| d92f0f3ffd | |||
| 4037c49712 | |||
| ed05364bbf | |||
| cbb5ae38ce | |||
| 78f8882663 | |||
| f81568adf3 | |||
| 4a58850889 | |||
| 2dbd6ee165 | |||
| 0a98207edc | |||
| cc95dcfd89 | |||
| 2c5f1cd1f8 | |||
| 2d835559d6 | |||
| fd1e032e59 | |||
| 8107dc0547 | |||
| bb0f73e824 | |||
| 89fbf19c4c | |||
| 544df9483d | |||
| 7175058d26 | |||
| 97565989bb | |||
| 2d1ef16a75 | |||
| 27ec73b30f | |||
| 29b25d23c0 | |||
| 6285f29620 | |||
| c43b2e260e | |||
| f35a53550e | |||
| 3515581a9c | |||
| 97df1ba66e | |||
| 2a1ff246cc | |||
| daa8234819 | |||
| 14ef20a87b | |||
| 612494b80d | |||
| e34cb7bd8a | |||
| 9f60e670fe | |||
| 5228284772 | |||
| 514ea92912 | |||
| c2b2ee2e37 | |||
| f2f2f13b1c | |||
| 6486482858 | |||
| 7ef7ec1d89 | |||
| 6648624c1e | |||
| e9b7f78423 | |||
| 7d0021ae3e | |||
| 7eab1b1559 | |||
| bb1e59ab28 | |||
| 3052a96a8c | |||
| 409f0bdd2e | |||
| 73495aad17 | |||
| ffa468a149 | |||
| d84155c58d | |||
| 12b49acba8 | |||
| 526c36a183 | |||
| 59aab77371 | |||
| f2c6d088c8 | |||
| 409f1a4517 | |||
| 4e6848020d | |||
| 0ba2dc2310 | |||
| 17254e5217 | |||
| fa6cf0dba7 | |||
| 5cc719ed92 | |||
| 1f5d1e99d5 | |||
| 40af3c00f6 | |||
| 690ac12afb | |||
| 943aafbec2 | |||
| 7891fb8d91 | |||
| 95f8b31ba6 | |||
| b615a5639e | |||
| ab53177210 | |||
| a5b3f9b05a | |||
| adb5a1bea6 | |||
| 46dbbbbfce |
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
BIN
.a0proj/audit.db
BIN
.a0proj/audit.db
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
{"model_provider": "ollama", "model_name": "bge-m3:latest"}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
9cde4598eb68e4b1810cdf657333d8ca9e228ebcb4b4717524b62a61ae06f900
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
{"/a0/usr/knowledge/main/Iran-US-Conflict-Update-2026-04-01.md": {"file": "/a0/usr/knowledge/main/Iran-US-Conflict-Update-2026-04-01.md", "checksum": "6beea9874c3bcb846d17f9a60c29d528", "ids": ["5sQkc0Ylqa", "FeZVPLWYss", "kYBRtDfHjJ"]}, "/a0/usr/knowledge/main/ollama-27b-modelfile.txt": {"file": "/a0/usr/knowledge/main/ollama-27b-modelfile.txt", "checksum": "3f4f724d6f777e0620df9781ebc82f36", "ids": ["yZoFOCA99D"]}, "/a0/usr/knowledge/main/behavioral-rules.md": {"file": "/a0/usr/knowledge/main/behavioral-rules.md", "checksum": "ff4230d5f02891487008864de55151e8", "ids": ["5LhBKVgUXB"]}, "/a0/usr/knowledge/main/utility_test.txt": {"file": "/a0/usr/knowledge/main/utility_test.txt", "checksum": "c8c29a129e935836a77048f47e231705", "ids": ["vrbKe4D4sR"]}, "/a0/usr/knowledge/main/welcome.md": {"file": "/a0/usr/knowledge/main/welcome.md", "checksum": "d947ce81d6dcc977a3ddf52e8d5e4712", "ids": ["0Qx7U1mSZH"]}, "/a0/usr/knowledge/main/capability_test_results.txt": {"file": "/a0/usr/knowledge/main/capability_test_results.txt", "checksum": "880b2a6e355125561f22e1f0ac38a3c4", "ids": ["hmVC8arGTg"]}, "/a0/usr/knowledge/main/Iran-US-Conflict-Analysis-2026.md": {"file": "/a0/usr/knowledge/main/Iran-US-Conflict-Analysis-2026.md", "checksum": "ffa6e16f560fc2c021df9c656e8dfdcc", "ids": ["WKKtg5Rj2e", "VBSDN1KENS"]}, "/a0/knowledge/main/tool_call_reference_examples.md": {"file": "/a0/knowledge/main/tool_call_reference_examples.md", "checksum": "1558e6e118619185e31224b1ed646b9a", "ids": ["mLgFu7vH7Z"]}, "/a0/knowledge/main/about/architecture.md": {"file": "/a0/knowledge/main/about/architecture.md", "checksum": "0de7a9280419982ef5fc98d0cc6ad2dc", "ids": ["VG5QHEdqZt", "oALIWNguyG"]}, "/a0/knowledge/main/about/configuration.md": {"file": "/a0/knowledge/main/about/configuration.md", "checksum": "9f83690fdca64631d063c75fd324d42c", "ids": ["XX5kcVMvDu", "T2B8pFL10O"]}, "/a0/knowledge/main/about/capabilities.md": {"file": "/a0/knowledge/main/about/capabilities.md", "checksum": "cf4d100df544af245940971464357e0b", "ids": ["S6MH1eLPzP", "laWnXkj3Ky"]}, "/a0/knowledge/main/about/identity.md": {"file": "/a0/knowledge/main/about/identity.md", "checksum": "63a2c83c6c3bf4c4008786c396618755", "ids": ["Yi3PLqGcaj"]}, "/a0/knowledge/main/about/setup-and-deployment.md": {"file": "/a0/knowledge/main/about/setup-and-deployment.md", "checksum": "3cf57d685f11a6989a73cf041c2018a3", "ids": ["KVJ5zsWDQX", "LoANN0xNbF"]}}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"title": "Linux_Patch_API", "description": "Create an API service that will allow remote clients to securely remote manage the patching process and control software add and removal. ", "instructions": "Use Strict Spec Driven development process following the kiro standards.\nAsk questions and help build all of the spec driven files needed\nAlways get approval before taking next steps\nAlways ask questions to determine the right path for the software\nNever make assumptions, always confirm. \nCode must be build following strict security coding guidelines\n", "color": "#00bbf9", "git_url": "", "file_structure": {"enabled": true, "max_depth": 5, "max_files": 20, "max_folders": 20, "max_lines": 250, "gitignore": "# Python environments & cache\nvenv/**\n**/__pycache__/**\n\n# Node.js dependencies\n**/node_modules/**\n**/.npm/**\n\n# Version control metadata\n**/.git/**\n"}}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
EMBEDDING_MODEL=bge-m3:latest
|
|
||||||
OLLAMA_HOST=http://ares.moon-dragon.us:11434
|
|
||||||
LLM_MODEL=qwen3.5:9b
|
|
||||||
@ -3,7 +3,7 @@ name: CI/CD Pipeline
|
|||||||
"on":
|
"on":
|
||||||
push:
|
push:
|
||||||
branches: [ master, develop ]
|
branches: [ master, develop ]
|
||||||
tags: [ 'v*' ]
|
tags: [ 'v*.*.*' ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
run: |
|
run: |
|
||||||
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/git-echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
||||||
tar -xzf repo.tar.gz --strip-components=1
|
tar -xzf repo.tar.gz --strip-components=1
|
||||||
rm -f repo.tar.gz
|
rm -f repo.tar.gz
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
@ -36,7 +36,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
run: |
|
run: |
|
||||||
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/git-echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
||||||
tar -xzf repo.tar.gz --strip-components=1
|
tar -xzf repo.tar.gz --strip-components=1
|
||||||
rm -f repo.tar.gz
|
rm -f repo.tar.gz
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
@ -49,17 +49,17 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get -f install -y
|
sudo apt-get -f install -y
|
||||||
sudo apt-get install -y build-essential libsystemd-dev pkg-config
|
sudo apt-get install -y build-essential libsystemd-dev pkg-config libssl-dev
|
||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Unit Tests
|
name: All Unit Tests
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
run: |
|
run: |
|
||||||
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/git-echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
||||||
tar -xzf repo.tar.gz --strip-components=1
|
tar -xzf repo.tar.gz --strip-components=1
|
||||||
rm -f repo.tar.gz
|
rm -f repo.tar.gz
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
@ -71,7 +71,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get -f install -y
|
sudo apt-get -f install -y
|
||||||
sudo apt-get install -y build-essential libsystemd-dev pkg-config
|
sudo apt-get install -y build-essential libsystemd-dev pkg-config libssl-dev
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --all-features
|
run: cargo test --all-features
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
run: |
|
run: |
|
||||||
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/git-echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
||||||
tar -xzf repo.tar.gz --strip-components=1
|
tar -xzf repo.tar.gz --strip-components=1
|
||||||
rm -f repo.tar.gz
|
rm -f repo.tar.gz
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
@ -93,20 +93,72 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get -f install -y
|
sudo apt-get -f install -y
|
||||||
sudo apt-get install -y build-essential libsystemd-dev pkg-config
|
sudo apt-get install -y build-essential libsystemd-dev pkg-config libssl-dev
|
||||||
- name: Run cargo-audit
|
- name: Run cargo-audit
|
||||||
run: |
|
run: |
|
||||||
cargo install cargo-audit
|
cargo install cargo-audit
|
||||||
cargo audit --ignore RUSTSEC-2025-0134
|
cargo audit --ignore RUSTSEC-2025-0134
|
||||||
|
|
||||||
build-deb:
|
enrollment-tests:
|
||||||
name: Build Debian Package
|
name: Enrollment Tests
|
||||||
needs: [fmt, clippy, test]
|
needs: [fmt, clippy]
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
run: |
|
run: |
|
||||||
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/git-echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
||||||
|
tar -xzf repo.tar.gz --strip-components=1
|
||||||
|
rm -f repo.tar.gz
|
||||||
|
- name: Install Rust
|
||||||
|
run: |
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
|
||||||
|
. "$HOME/.cargo/env"
|
||||||
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get -f install -y
|
||||||
|
sudo apt-get install -y build-essential libsystemd-dev pkg-config libssl-dev
|
||||||
|
- name: Run enrollment unit tests
|
||||||
|
run: cargo test --test enroll_identity
|
||||||
|
- name: Run enrollment integration tests
|
||||||
|
run: cargo test --test enrollment_test
|
||||||
|
- name: Run enrollment E2E tests
|
||||||
|
run: cargo test --test enrollment_e2e
|
||||||
|
|
||||||
|
verify-enrollment-cli:
|
||||||
|
name: Verify Enrollment CLI Flag
|
||||||
|
needs: [clippy]
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
run: |
|
||||||
|
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/git-echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
||||||
|
tar -xzf repo.tar.gz --strip-components=1
|
||||||
|
rm -f repo.tar.gz
|
||||||
|
- name: Install Rust
|
||||||
|
run: |
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
|
||||||
|
. "$HOME/.cargo/env"
|
||||||
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get -f install -y
|
||||||
|
sudo apt-get install -y build-essential libsystemd-dev pkg-config libssl-dev
|
||||||
|
- name: Build binary
|
||||||
|
run: cargo build
|
||||||
|
- name: Verify --enroll flag exists
|
||||||
|
run: cargo run -- --help | grep -q '\-\-enroll'
|
||||||
|
|
||||||
|
build-deb:
|
||||||
|
name: Build Debian Package
|
||||||
|
needs: [fmt, clippy, test, enrollment-tests]
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
run: |
|
||||||
|
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/git-echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
||||||
tar -xzf repo.tar.gz --strip-components=1
|
tar -xzf repo.tar.gz --strip-components=1
|
||||||
rm -f repo.tar.gz
|
rm -f repo.tar.gz
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
@ -118,28 +170,38 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get -f install -y
|
sudo apt-get -f install -y
|
||||||
sudo apt-get install -y build-essential debhelper pkg-config libsystemd-dev
|
sudo apt-get install -y build-essential debhelper pkg-config libsystemd-dev libssl-dev
|
||||||
|
- name: Clean previous build artifacts
|
||||||
|
run: |
|
||||||
|
cargo clean
|
||||||
|
rm -f ../linux-patch-api_*.deb
|
||||||
- name: Build Debian package
|
- name: Build Debian package
|
||||||
run: |
|
run: |
|
||||||
sudo dpkg-buildpackage -us -uc -b -d
|
sudo dpkg-buildpackage -us -uc -b -d
|
||||||
- name: Upload to Gitea Release
|
- name: Upload to Gitea Release
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: github.ref_type == 'tag'
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEATOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEATOKEN }}
|
||||||
run: |
|
run: |
|
||||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||||
FILE=$(ls ../linux-patch-api_*.deb 2>/dev/null | head -1)
|
FILE=$(ls ../linux-patch-api_*.deb 2>/dev/null | head -1)
|
||||||
|
# Rename deb to include u2404 in filename to distinguish from u2204 build
|
||||||
|
if [ -n "$FILE" ]; then
|
||||||
|
U2404_FILE="$(echo "$FILE" | sed 's/_amd64/_u2404_amd64/')"
|
||||||
|
mv "$FILE" "$U2404_FILE"
|
||||||
|
FILE="$U2404_FILE"
|
||||||
|
fi
|
||||||
chmod +x scripts/upload-release.sh
|
chmod +x scripts/upload-release.sh
|
||||||
./scripts/upload-release.sh "$TAG_NAME" "$FILE"
|
./scripts/upload-release.sh "$TAG_NAME" "$FILE"
|
||||||
|
|
||||||
build-deb-u2204:
|
build-deb-u2204:
|
||||||
name: Build Debian Package (Ubuntu 22.04)
|
name: Build Debian Package (Ubuntu 22.04)
|
||||||
needs: [fmt, clippy, test]
|
needs: [fmt, clippy, test, enrollment-tests]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
run: |
|
run: |
|
||||||
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/git-echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
||||||
tar -xzf repo.tar.gz --strip-components=1
|
tar -xzf repo.tar.gz --strip-components=1
|
||||||
rm -f repo.tar.gz
|
rm -f repo.tar.gz
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
@ -151,28 +213,38 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get -f install -y
|
sudo apt-get -f install -y
|
||||||
sudo apt-get install -y build-essential debhelper pkg-config libsystemd-dev
|
sudo apt-get install -y build-essential debhelper pkg-config libsystemd-dev libssl-dev
|
||||||
|
- name: Clean previous build artifacts
|
||||||
|
run: |
|
||||||
|
cargo clean
|
||||||
|
rm -f ../linux-patch-api_*.deb
|
||||||
- name: Build Debian package
|
- name: Build Debian package
|
||||||
run: |
|
run: |
|
||||||
sudo dpkg-buildpackage -us -uc -b -d
|
sudo dpkg-buildpackage -us -uc -b -d
|
||||||
- name: Upload to Gitea Release
|
- name: Upload to Gitea Release
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: github.ref_type == 'tag'
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEATOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEATOKEN }}
|
||||||
run: |
|
run: |
|
||||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||||
FILE=$(ls ../linux-patch-api_*.deb 2>/dev/null | head -1)
|
FILE=$(ls ../linux-patch-api_*.deb 2>/dev/null | head -1)
|
||||||
|
# Rename deb to include u2204 in filename to avoid collision with main build
|
||||||
|
if [ -n "$FILE" ]; then
|
||||||
|
U2204_FILE="$(echo "$FILE" | sed 's/_amd64/_u2204_amd64/')"
|
||||||
|
mv "$FILE" "$U2204_FILE"
|
||||||
|
FILE="$U2204_FILE"
|
||||||
|
fi
|
||||||
chmod +x scripts/upload-release.sh
|
chmod +x scripts/upload-release.sh
|
||||||
./scripts/upload-release.sh "${TAG_NAME}-u2204" "$FILE"
|
./scripts/upload-release.sh "$TAG_NAME" "$FILE"
|
||||||
|
|
||||||
build-rpm:
|
build-rpm:
|
||||||
name: Build RPM Package
|
name: Build RPM Package
|
||||||
needs: [fmt, clippy, test]
|
needs: [fmt, clippy, test, enrollment-tests]
|
||||||
runs-on: fedora
|
runs-on: fedora
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
run: |
|
run: |
|
||||||
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/git-echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
||||||
tar -xzf repo.tar.gz --strip-components=1
|
tar -xzf repo.tar.gz --strip-components=1
|
||||||
rm -f repo.tar.gz
|
rm -f repo.tar.gz
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
@ -182,68 +254,115 @@ jobs:
|
|||||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo dnf install -y gcc rpm-build systemd-devel pkg-config
|
sudo dnf install -y gcc rpm-build systemd-devel pkg-config openssl-devel
|
||||||
|
- name: Clean stale RPM artifacts
|
||||||
|
run: |
|
||||||
|
rm -f ~/rpmbuild/RPMS/x86_64/linux-patch-api-*.rpm
|
||||||
|
rm -f releases/linux-patch-api-*.rpm
|
||||||
- name: Build release binary
|
- name: Build release binary
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
- name: Build RPM package
|
- name: Build RPM package
|
||||||
run: |
|
run: |
|
||||||
chmod +x build-rpm.sh
|
chmod +x build-rpm.sh
|
||||||
./build-rpm.sh
|
SKIP_CARGO_BUILD=1 ./build-rpm.sh
|
||||||
|
- name: Verify RPM package
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
||||||
|
RPM_FILE=$(ls ~/rpmbuild/RPMS/x86_64/linux-patch-api-${VERSION}-*.rpm 2>/dev/null | head -1)
|
||||||
|
if [ -z "$RPM_FILE" ]; then
|
||||||
|
echo "ERROR: RPM package not found for version $VERSION!"
|
||||||
|
ls -la ~/rpmbuild/RPMS/x86_64/ 2>/dev/null || echo "RPM directory empty or missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
RPM_VERSION=$(rpm -qp --queryformat '%{VERSION}' "$RPM_FILE" 2>/dev/null || true)
|
||||||
|
echo "RPM file: $RPM_FILE"
|
||||||
|
echo "RPM version: $RPM_VERSION"
|
||||||
|
echo "Expected version: $VERSION"
|
||||||
|
if [ "$RPM_VERSION" != "$VERSION" ]; then
|
||||||
|
echo "ERROR: RPM version ($RPM_VERSION) does not match expected version ($VERSION)!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "RPM verification passed"
|
||||||
- name: Upload to Gitea Release
|
- name: Upload to Gitea Release
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: github.ref_type == 'tag'
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEATOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEATOKEN }}
|
||||||
run: |
|
run: |
|
||||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||||
FILE=$(ls ~/rpmbuild/RPMS/x86_64/*.rpm 2>/dev/null | head -1)
|
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
||||||
|
FILE=$(ls ~/rpmbuild/RPMS/x86_64/linux-patch-api-${VERSION}-*.rpm 2>/dev/null | head -1)
|
||||||
|
if [ -z "$FILE" ]; then
|
||||||
|
echo "ERROR: No RPM found with version $VERSION for upload!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
chmod +x scripts/upload-release.sh
|
chmod +x scripts/upload-release.sh
|
||||||
./scripts/upload-release.sh "$TAG_NAME" "$FILE"
|
./scripts/upload-release.sh "$TAG_NAME" "$FILE"
|
||||||
|
|
||||||
build-apk:
|
build-apk:
|
||||||
name: Build Alpine Package
|
name: Build Alpine Package
|
||||||
needs: [fmt, clippy, test]
|
needs: [fmt, clippy, test, enrollment-tests]
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
run: |
|
run: |
|
||||||
apk add --no-cache curl
|
apk add --no-cache curl
|
||||||
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/git-echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
||||||
tar -xzf repo.tar.gz --strip-components=1
|
tar -xzf repo.tar.gz --strip-components=1
|
||||||
rm -f repo.tar.gz
|
rm -f repo.tar.gz
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
run: |
|
run: |
|
||||||
apk add --no-cache curl bash
|
apk add --no-cache curl bash
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
|
curl --ipv4 --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
|
||||||
. "$HOME/.cargo/env"
|
. "$HOME/.cargo/env"
|
||||||
rustup target add x86_64-unknown-linux-musl
|
rustup target add x86_64-unknown-linux-musl
|
||||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
apk add --no-cache alpine-sdk rust cargo openssl-dev elogind-dev musl-dev abuild gcc
|
apk add --no-cache alpine-sdk rust cargo openssl openssl-dev elogind-dev musl-dev abuild gcc
|
||||||
- name: Build release binary
|
- name: Build release binary
|
||||||
run: cargo build --release --target x86_64-unknown-linux-musl
|
run: cargo build --release --target x86_64-unknown-linux-musl
|
||||||
- name: Build Alpine package
|
- name: Build Alpine package
|
||||||
run: |
|
run: |
|
||||||
chmod +x build-alpine.sh
|
chmod +x build-alpine.sh
|
||||||
SKIP_CARGO_BUILD=1 ./build-alpine.sh
|
SKIP_CARGO_BUILD=1 ./build-alpine.sh
|
||||||
|
- name: Verify Alpine package
|
||||||
|
run: |
|
||||||
|
EXPECTED_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
||||||
|
FILE=$(ls releases/linux-patch-api-*.apk 2>/dev/null | head -1)
|
||||||
|
if [ -z "$FILE" ]; then
|
||||||
|
echo "ERROR: No Alpine package found!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Expected version: $EXPECTED_VERSION"
|
||||||
|
echo "Package file: $FILE"
|
||||||
|
# Verify filename contains expected version
|
||||||
|
if ! echo "$FILE" | grep -q "$EXPECTED_VERSION"; then
|
||||||
|
echo "ERROR: Alpine package version ($FILE) does not match expected version ($EXPECTED_VERSION)!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Alpine package verification passed"
|
||||||
- name: Upload to Gitea Release
|
- name: Upload to Gitea Release
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEATOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEATOKEN }}
|
||||||
run: |
|
run: |
|
||||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||||
FILE=$(ls releases/*.apk 2>/dev/null | head -1)
|
FILE=$(ls releases/linux-patch-api-*.apk 2>/dev/null | head -1)
|
||||||
|
if [ -z "$FILE" ]; then
|
||||||
|
echo "ERROR: No Alpine package found for upload!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
chmod +x scripts/upload-release.sh
|
chmod +x scripts/upload-release.sh
|
||||||
./scripts/upload-release.sh "$TAG_NAME" "$FILE"
|
./scripts/upload-release.sh "$TAG_NAME" "$FILE"
|
||||||
|
|
||||||
build-arch:
|
build-arch:
|
||||||
name: Build Arch Package
|
name: Build Arch Package
|
||||||
needs: [fmt, clippy, test]
|
needs: [fmt, clippy, test, enrollment-tests]
|
||||||
runs-on: arch
|
runs-on: arch
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
run: |
|
run: |
|
||||||
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
curl -sfL -H "Authorization: token ${{ secrets.GITEATOKEN }}" "https://gitea-lxc.moon-dragon.us/git-echo/linux_patch_api/archive/${GITHUB_SHA}.tar.gz" -o repo.tar.gz
|
||||||
tar -xzf repo.tar.gz --strip-components=1
|
tar -xzf repo.tar.gz --strip-components=1
|
||||||
rm -f repo.tar.gz
|
rm -f repo.tar.gz
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
@ -254,12 +373,28 @@ jobs:
|
|||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo pacman -Syu --noconfirm rust cargo systemd git base-devel gcc
|
sudo pacman -Syu --noconfirm rust cargo systemd git base-devel gcc
|
||||||
|
- name: Clean previous build artifacts
|
||||||
|
run: |
|
||||||
|
cargo clean
|
||||||
|
rm -f releases/linux-patch-api-*.pkg.tar.zst
|
||||||
- name: Build release binary
|
- name: Build release binary
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
- name: Build Arch package
|
- name: Build Arch package
|
||||||
run: |
|
run: |
|
||||||
chmod +x build-arch.sh
|
chmod +x build-arch.sh
|
||||||
SKIP_CARGO_BUILD=1 ./build-arch.sh
|
SKIP_CARGO_BUILD=1 ./build-arch.sh
|
||||||
|
- name: Verify Arch package
|
||||||
|
run: |
|
||||||
|
FILE=$(ls releases/*.pkg.tar.zst 2>/dev/null | head -1)
|
||||||
|
if [ -z "$FILE" ]; then
|
||||||
|
echo "ERROR: No Arch package found!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
EXPECTED_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
||||||
|
echo "Expected version: $EXPECTED_VERSION"
|
||||||
|
echo "Package file: $FILE"
|
||||||
|
# Verify the package contains the correct binary version
|
||||||
|
pacman -Qip "$FILE" 2>/dev/null | grep -i version || true
|
||||||
- name: Upload to Gitea Release
|
- name: Upload to Gitea Release
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
env:
|
env:
|
||||||
|
|||||||
294
.github/workflows/ci.yml
vendored
Normal file
294
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
tags: ['v*.*.*']
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ── Quality Gates (GitHub-hosted, all triggers) ──────────────────────────
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
name: fmt
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
name: Clippy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: clippy
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y build-essential libsystemd-dev pkg-config libssl-dev
|
||||||
|
- run: cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y build-essential libsystemd-dev pkg-config libssl-dev
|
||||||
|
- run: cargo test --all-features
|
||||||
|
|
||||||
|
audit:
|
||||||
|
name: Security Audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- run: cargo install cargo-audit && cargo audit --ignore RUSTSEC-2025-0134
|
||||||
|
|
||||||
|
gitleaks:
|
||||||
|
name: Secret scanning
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Gitleaks
|
||||||
|
uses: gitleaks/gitleaks-action@v2
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
enrollment-tests:
|
||||||
|
name: Enrollment Tests
|
||||||
|
needs: [fmt, clippy]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y build-essential libsystemd-dev pkg-config libssl-dev
|
||||||
|
- run: cargo test --test enroll_identity
|
||||||
|
- run: cargo test --test enrollment_test
|
||||||
|
- run: cargo test --test enrollment_e2e
|
||||||
|
|
||||||
|
# ── Release Preparation (tag push only) ───────────────────────────────────
|
||||||
|
|
||||||
|
prepare-release:
|
||||||
|
name: Prepare Release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
needs: [fmt, clippy, test, enrollment-tests, audit]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Generate release notes
|
||||||
|
id: release_notes
|
||||||
|
run: |
|
||||||
|
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||||
|
if [ -n "$PREV_TAG" ]; then
|
||||||
|
NOTES=$(git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --no-merges)
|
||||||
|
else
|
||||||
|
NOTES=$(git log --pretty=format:"- %s (%h)" --no-merges)
|
||||||
|
fi
|
||||||
|
echo "notes<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
echo "$NOTES" >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
body: ${{ steps.release_notes.outputs.notes }}
|
||||||
|
|
||||||
|
# ── Build Jobs (tag push only, self-hosted runners) ───────────────────────
|
||||||
|
|
||||||
|
build-deb-u2404:
|
||||||
|
name: Build .deb (Ubuntu 24.04)
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
needs: [fmt, clippy, test, enrollment-tests, audit, prepare-release]
|
||||||
|
runs-on: [self-hosted, linux, ubuntu-24.04]
|
||||||
|
steps:
|
||||||
|
- name: Clean previous build artifacts from root
|
||||||
|
run: sudo rm -rf releases/ || true
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y build-essential libsystemd-dev pkg-config libssl-dev
|
||||||
|
- name: Add Rust to PATH
|
||||||
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: Build .deb package
|
||||||
|
run: chmod +x scripts/build-package.sh && scripts/build-package.sh
|
||||||
|
- name: Rename package with distro suffix
|
||||||
|
run: |
|
||||||
|
FILE=$(ls linux-patch-api_*_amd64.deb 2>/dev/null | head -1)
|
||||||
|
if [ -n "$FILE" ]; then
|
||||||
|
mv "$FILE" "$(echo "$FILE" | sed 's/_amd64/_u2404_amd64/')"
|
||||||
|
fi
|
||||||
|
- name: Upload to GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: linux-patch-api_*_u2404_amd64.deb
|
||||||
|
|
||||||
|
build-deb-u2204:
|
||||||
|
name: Build .deb (Ubuntu 22.04)
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
needs: [fmt, clippy, test, enrollment-tests, audit, prepare-release]
|
||||||
|
runs-on: [self-hosted, linux, ubuntu-22.04]
|
||||||
|
steps:
|
||||||
|
- name: Clean previous build artifacts from root
|
||||||
|
run: sudo rm -rf releases/ || true
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y build-essential libsystemd-dev pkg-config libssl-dev
|
||||||
|
- name: Add Rust to PATH
|
||||||
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: Build .deb package
|
||||||
|
run: chmod +x scripts/build-package.sh && scripts/build-package.sh
|
||||||
|
- name: Rename package with distro suffix
|
||||||
|
run: |
|
||||||
|
FILE=$(ls linux-patch-api_*_amd64.deb 2>/dev/null | head -1)
|
||||||
|
if [ -n "$FILE" ]; then
|
||||||
|
mv "$FILE" "$(echo "$FILE" | sed 's/_amd64/_u2204_amd64/')"
|
||||||
|
fi
|
||||||
|
- name: Upload to GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: linux-patch-api_*_u2204_amd64.deb
|
||||||
|
|
||||||
|
build-deb-debian13:
|
||||||
|
name: Build .deb (Debian 13)
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
needs: [fmt, clippy, test, enrollment-tests, audit, prepare-release]
|
||||||
|
runs-on: [self-hosted, linux, debian-13]
|
||||||
|
steps:
|
||||||
|
- name: Clean previous build artifacts from root
|
||||||
|
run: sudo rm -rf releases/ || true
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y build-essential libsystemd-dev pkg-config libssl-dev
|
||||||
|
- name: Add Rust to PATH
|
||||||
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: Build .deb package
|
||||||
|
run: chmod +x scripts/build-package.sh && scripts/build-package.sh
|
||||||
|
- name: Rename package with distro suffix
|
||||||
|
run: |
|
||||||
|
FILE=$(ls linux-patch-api_*_amd64.deb 2>/dev/null | head -1)
|
||||||
|
if [ -n "$FILE" ]; then
|
||||||
|
mv "$FILE" "$(echo "$FILE" | sed 's/_amd64/_debian13_amd64/')"
|
||||||
|
fi
|
||||||
|
- name: Upload to GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: linux-patch-api_*_debian13_amd64.deb
|
||||||
|
|
||||||
|
build-rpm-fedora:
|
||||||
|
name: Build .rpm (Fedora)
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
needs: [fmt, clippy, test, enrollment-tests, audit, prepare-release]
|
||||||
|
runs-on: [self-hosted, linux, fedora]
|
||||||
|
steps:
|
||||||
|
- name: Clean previous build artifacts from root
|
||||||
|
run: sudo rm -rf releases/ || true
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: sudo dnf install -y systemd-devel openssl-devel pkg-config gcc make
|
||||||
|
- name: Add Rust to PATH
|
||||||
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: Build release binary
|
||||||
|
run: cargo build --release
|
||||||
|
- name: Build RPM package
|
||||||
|
run: chmod +x build-rpm.sh && SKIP_CARGO_BUILD=1 sudo -E ./build-rpm.sh
|
||||||
|
- name: Upload to GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: releases/linux-patch-api-*.rpm
|
||||||
|
|
||||||
|
build-rpm-almalinux:
|
||||||
|
name: Build .rpm (AlmaLinux 10)
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
needs: [fmt, clippy, test, enrollment-tests, audit, prepare-release]
|
||||||
|
runs-on: [self-hosted, linux, almalinux-10]
|
||||||
|
steps:
|
||||||
|
- name: Clean previous build artifacts from root
|
||||||
|
run: sudo rm -rf releases/ || true
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: sudo dnf install -y systemd-devel openssl-devel pkg-config gcc make
|
||||||
|
- name: Add Rust to PATH
|
||||||
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: Build release binary
|
||||||
|
run: cargo build --release
|
||||||
|
- name: Build RPM package
|
||||||
|
run: chmod +x build-rpm.sh && SKIP_CARGO_BUILD=1 sudo -E ./build-rpm.sh
|
||||||
|
- name: Upload to GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: releases/linux-patch-api-*.rpm
|
||||||
|
|
||||||
|
build-arch:
|
||||||
|
name: Build .pkg.tar.zst (Arch Linux)
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
needs: [fmt, clippy, test, enrollment-tests, audit, prepare-release]
|
||||||
|
runs-on: [self-hosted, linux, arch]
|
||||||
|
steps:
|
||||||
|
- name: Clean previous build artifacts from root
|
||||||
|
run: sudo rm -rf releases/ || true
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: sudo pacman -Syu --noconfirm systemd openssl pkg-config gcc
|
||||||
|
- name: Add Rust to PATH
|
||||||
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: Build release binary
|
||||||
|
run: cargo build --release
|
||||||
|
- name: Build Arch package
|
||||||
|
run: chmod +x build-arch.sh && SKIP_CARGO_BUILD=1 ./build-arch.sh
|
||||||
|
- name: Upload to GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: releases/*.pkg.tar.zst
|
||||||
|
|
||||||
|
build-alpine:
|
||||||
|
name: Build .apk (Alpine)
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
needs: [fmt, clippy, test, enrollment-tests, audit, prepare-release]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: alpine:latest
|
||||||
|
env:
|
||||||
|
HOME: /root
|
||||||
|
steps:
|
||||||
|
- name: Install prerequisites for actions/checkout
|
||||||
|
run: apk add --no-cache bash git curl tar
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install Alpine build dependencies
|
||||||
|
run: apk add --no-cache gcc musl-dev openssl-dev openssl elogind-dev alpine-sdk abuild
|
||||||
|
- name: Install Rust via rustup
|
||||||
|
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
- name: Add Rust to PATH
|
||||||
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: Add musl target
|
||||||
|
run: rustup target add x86_64-unknown-linux-musl
|
||||||
|
- name: Build release binary (musl target)
|
||||||
|
run: cargo build --release --target x86_64-unknown-linux-musl
|
||||||
|
- name: Build Alpine package
|
||||||
|
run: |
|
||||||
|
chmod +x build-alpine.sh
|
||||||
|
SKIP_CARGO_BUILD=1 ./build-alpine.sh
|
||||||
|
- name: Upload to GitHub Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: releases/linux-patch-api-*.apk
|
||||||
24
.gitignore
vendored
24
.gitignore
vendored
@ -1 +1,25 @@
|
|||||||
/target
|
/target
|
||||||
|
/releases/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
debian/tmp/
|
||||||
|
debian/linux-patch-api/
|
||||||
|
debian/.debhelper/
|
||||||
|
debian/debhelper-build-stamp
|
||||||
|
debian/files
|
||||||
|
debian/linux-patch-api.debhelper.log
|
||||||
|
debian/linux-patch-api.postrm.debhelper
|
||||||
|
debian/linux-patch-api.substvars
|
||||||
|
*.deb
|
||||||
|
*.buildinfo
|
||||||
|
*.changes
|
||||||
|
|
||||||
|
# Private key material - NEVER commit
|
||||||
|
*.key
|
||||||
|
*.key.pem
|
||||||
|
configs/certs/*.pem
|
||||||
|
configs/certs/*.srl
|
||||||
|
tests/e2e/certs/*.key
|
||||||
|
|
||||||
|
# Agent Zero project data
|
||||||
|
.a0proj/
|
||||||
|
|||||||
@ -15,6 +15,7 @@ Complete API reference for the Linux Patch API service.
|
|||||||
- [Authentication](#authentication)
|
- [Authentication](#authentication)
|
||||||
- [Standard Response Format](#standard-response-format)
|
- [Standard Response Format](#standard-response-format)
|
||||||
- [Error Handling](#error-handling)
|
- [Error Handling](#error-handling)
|
||||||
|
- [Enrollment Endpoints](#enrollment-endpoints)
|
||||||
- [Package Management Endpoints](#package-management-endpoints)
|
- [Package Management Endpoints](#package-management-endpoints)
|
||||||
- [Patch Management Endpoints](#patch-management-endpoints)
|
- [Patch Management Endpoints](#patch-management-endpoints)
|
||||||
- [System Management Endpoints](#system-management-endpoints)
|
- [System Management Endpoints](#system-management-endpoints)
|
||||||
@ -882,6 +883,262 @@ def wait_for_job(job_id, base_url, certs, poll_interval=2):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Enrollment Endpoints
|
||||||
|
|
||||||
|
Enrollment endpoints enable new hosts to register with the Patch Manager and receive mTLS certificates for authenticated API access. These endpoints operate **without client certificate authentication** — security is enforced through rate limiting, single-use tokens, and admin approval workflows.
|
||||||
|
|
||||||
|
**Base path:** `/api/v1/` (on the Patch Manager server)
|
||||||
|
**Authentication:** None (pre-provisioning phase)
|
||||||
|
**Transport:** HTTPS recommended; TLS verification intentionally relaxed on initial connection per security model
|
||||||
|
|
||||||
|
> **Cross-reference:** [SPEC.md §4.2 Enrollment Workflow](./SPEC.md) · [DEPLOYMENT_SECURITY_GUIDE.md](./DEPLOYMENT_SECURITY_GUIDE.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### POST /api/v1/enroll
|
||||||
|
|
||||||
|
**Description:** Initiates a host self-enrollment request with the Patch Manager. The manager assigns a unique polling token that the host uses to check approval status.
|
||||||
|
|
||||||
|
**Authentication:** None (unauthenticated public endpoint)
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `machine_id` | string | Yes | Linux machine-id from `/etc/machine-id` |
|
||||||
|
| `fqdn` | string | Yes | Fully qualified domain name of the host |
|
||||||
|
| `ip_address` | string | Yes | Primary non-loopback IPv4 address |
|
||||||
|
| `os_details` | object | Yes | OS metadata (free-form JSON object) |
|
||||||
|
| `hostname` | string | No | Short hostname (without domain). Used by the manager to populate `display_name` on approval. If omitted, the manager falls back to the FQDN. |
|
||||||
|
|
||||||
|
**`os_details` common fields:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `name` | string | Distribution name (e.g., `Debian`, `Ubuntu`) |
|
||||||
|
| `version_id` | string | OS version identifier (e.g., `12`, `24.04`) |
|
||||||
|
| `kernel` | string | Kernel release string (e.g., `6.1.0-kali9-amd64`) |
|
||||||
|
| `id_like` | string | Family identifier (e.g., `debian`) |
|
||||||
|
|
||||||
|
#### Request Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://manager.example.com/api/v1/enroll \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"machine_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
|
||||||
|
"fqdn": "host-01.example.com",
|
||||||
|
"ip_address": "192.168.1.50",
|
||||||
|
"os_details": {
|
||||||
|
"name": "Debian",
|
||||||
|
"version_id": "12",
|
||||||
|
"kernel": "6.1.0-kali9-amd64",
|
||||||
|
"id_like": "debian"
|
||||||
|
},
|
||||||
|
"hostname": "host-01"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Success Response (202 Accepted)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"polling_token": "aB3dE6gH9jK2mN5pQ8rS1tU4vW7xY0zA"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `polling_token` | string | 64-character alphanumeric bearer token for status polling. **Treat as secret credential.** |
|
||||||
|
|
||||||
|
#### Error Responses
|
||||||
|
|
||||||
|
| HTTP Status | Body | Description |
|
||||||
|
|-------------|------|-------------|
|
||||||
|
| `429` | `{ "error": "Rate limit exceeded. Try again in a minute." }` | Rate limit exceeded: 1 request/minute per source IP |
|
||||||
|
| `500` | `{ "error": "Database error" }` | Internal server or database error |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GET /api/v1/enroll/status/{token}
|
||||||
|
|
||||||
|
**Description:** Returns the current approval status of an enrollment request. When approved, the response includes the complete PKI bundle (CA certificate, server certificate, and server private key) needed for mTLS provisioning.
|
||||||
|
|
||||||
|
**Authentication:** None (token serves as bearer credential)
|
||||||
|
|
||||||
|
#### Path Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `token` | string | 64-character alphanumeric polling token from `POST /enroll` response |
|
||||||
|
|
||||||
|
#### Response Format
|
||||||
|
|
||||||
|
The endpoint returns a **tagged JSON object** with a `status` discriminator field. All responses return HTTP `200 OK` — the `status` value determines the outcome.
|
||||||
|
|
||||||
|
##### Pending (Awaiting Admin Approval)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "status": "pending" }
|
||||||
|
```
|
||||||
|
|
||||||
|
The enrollment request has been received and is awaiting administrator review. The host should continue polling at regular intervals.
|
||||||
|
|
||||||
|
##### Approved (PKI Bundle Provided)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "approved",
|
||||||
|
"ca_crt": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
|
||||||
|
"server_crt": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
|
||||||
|
"server_key": "-----BEGIN PRIVATE KEY-----\nMIGH...\n-----END PRIVATE KEY-----"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `status` | string | Always `"approved"` for this variant |
|
||||||
|
| `ca_crt` | string | PEM-encoded CA root certificate (for TLS verification) |
|
||||||
|
| `server_crt` | string | PEM-encoded server certificate (manager's TLS leaf) |
|
||||||
|
| `server_key` | string | PEM-encoded server private key (PKCS#8 format) |
|
||||||
|
|
||||||
|
##### Denied
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "status": "denied" }
|
||||||
|
```
|
||||||
|
|
||||||
|
The administrator has rejected the enrollment request. The host should abort the enrollment process.
|
||||||
|
|
||||||
|
##### Not Found (Token Expired or Invalid)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "status": "not_found" }
|
||||||
|
```
|
||||||
|
|
||||||
|
The polling token does not match any pending or approved enrollment. This occurs when:
|
||||||
|
- The token has expired (default TTL: 24 hours)
|
||||||
|
- The token was never issued
|
||||||
|
- The enrollment was already fulfilled and purged
|
||||||
|
|
||||||
|
#### curl Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check enrollment status
|
||||||
|
curl https://manager.example.com/api/v1/enroll/status/aB3dE6gH9jK2mN5pQ8rS1tU4vW7xY0zA
|
||||||
|
|
||||||
|
# Extract PKI bundle when approved
|
||||||
|
curl -s https://manager.example.com/api/v1/enroll/status/$TOKEN | jq -r '.ca_crt' > /etc/linux_patch_api/certs/ca.crt
|
||||||
|
curl -s https://manager.example.com/api/v1/enroll/status/$TOKEN | jq -r '.server_crt' > /etc/linux_patch_api/certs/server.crt
|
||||||
|
curl -s https://manager.example.com/api/v1/enroll/status/$TOKEN | jq -r '.server_key' > /etc/linux_patch_api/certs/server.key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Enrollment Flow Sequence
|
||||||
|
|
||||||
|
Complete step-by-step enrollment lifecycle from initial registration to mTLS provisioning:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||||
|
│ Linux Host │ │ Patch Manager │ │ Admin UI │
|
||||||
|
│ (linux_patch │ │ Server │ │ │
|
||||||
|
│ _api) │ │ │ │ │
|
||||||
|
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
||||||
|
│ │ │
|
||||||
|
│ 1. POST /enroll │ │
|
||||||
|
│ { machine_id, fqdn, │ │
|
||||||
|
│ ip_address, │ │
|
||||||
|
│ os_details } │ │
|
||||||
|
│──────────────────────▶│ │
|
||||||
|
│ │ │
|
||||||
|
│ │ Store request + token │
|
||||||
|
│ │ (SHA256 hashed) │
|
||||||
|
│ │ │
|
||||||
|
│ 2. 202 Accepted │ │
|
||||||
|
│ { polling_token } │ │
|
||||||
|
│──────────────────────▶│ │
|
||||||
|
│ │ │
|
||||||
|
│ │ 3. List pending │
|
||||||
|
│ │ enrollments │
|
||||||
|
│ │─────────────────────▶│
|
||||||
|
│ │ │
|
||||||
|
│ │ Admin reviews & │
|
||||||
|
│ │ approves request │
|
||||||
|
│ │◀──────────────────────│
|
||||||
|
│ │ │
|
||||||
|
│ │ Generate PKI bundle │
|
||||||
|
│ │ (CA cert + server │
|
||||||
|
│ │ cert + server key) │
|
||||||
|
│ │ │
|
||||||
|
│ 4. GET /enroll/status │ │
|
||||||
|
│ /{token} │ │
|
||||||
|
│──────────────────────▶│ │
|
||||||
|
│ │ │
|
||||||
|
│ 5. 200 { status: │ │
|
||||||
|
│ "approved", │ │
|
||||||
|
│ ca_crt, │ │
|
||||||
|
│ server_crt, │ │
|
||||||
|
│ server_key } │ │
|
||||||
|
│◀──────────────────────│ │
|
||||||
|
│ │ │
|
||||||
|
│ 6. Provision: │ │
|
||||||
|
│ - Write certs to disk │ │
|
||||||
|
│ - Update whitelist │ │
|
||||||
|
│ - Restart with mTLS │ │
|
||||||
|
│ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step Details:**
|
||||||
|
|
||||||
|
| Step | Action | Details |
|
||||||
|
|------|--------|---------|
|
||||||
|
| 1 | Host sends enrollment request | Extracts identity from `/etc/machine-id`, hostname, network interfaces, and OS release data |
|
||||||
|
| 2 | Manager returns polling token | Token is 64-character random alphanumeric string; SHA256 hash stored in database |
|
||||||
|
| 3 | Admin reviews pending requests | Manager exposes admin API for listing/approving/denying enrollment requests |
|
||||||
|
| 4 | Host polls status periodically | Default interval: 60 seconds. Configurable via `--poll-interval` flag |
|
||||||
|
| 5 | Host receives PKI bundle on approval | Complete CA chain, server certificate, and private key in PEM format |
|
||||||
|
| 6 | Host provisions mTLS infrastructure | Writes certificates to configured paths, updates IP whitelist, transitions to authenticated mode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
| Endpoint | Limit | Window | Scope |
|
||||||
|
|----------|-------|--------|-------|
|
||||||
|
| `POST /api/v1/enroll` | 1 request | Per minute | Per source IP address |
|
||||||
|
| `GET /api/v1/enroll/status/{token}` | No explicit limit | — | Host-controlled polling interval |
|
||||||
|
|
||||||
|
**Rate Limit Enforcement:**
|
||||||
|
- POST `/enroll`: Enforced by manager using in-memory LRU cache keyed on source IP (or `X-Forwarded-For` first entry when behind reverse proxy)
|
||||||
|
- Status endpoint: No server-side rate limiting; client controls poll frequency (default: 60s interval)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Security Notes
|
||||||
|
|
||||||
|
| Concern | Mitigation |
|
||||||
|
|---------|------------|
|
||||||
|
| **Initial connection security** | TLS verification disabled on enrollment client (`danger_accept_invalid_certs`). Manager approval workflow provides authorization — transport encryption is secondary during pre-provisioning phase |
|
||||||
|
| **Token secrecy** | Polling token is a 64-character random alphanumeric bearer credential. Never log the raw token value (only hash stored in DB). Tokens expire after 24 hours by default |
|
||||||
|
| **Host identity** | `machine_id` from `/etc/machine-id` provides unique host identification. Combined with FQDN and IP for collision detection during admin approval |
|
||||||
|
| **FQDN/IP collision** | Admin approval checks existing hosts table — rejects enrollment if FQDN or IP already registered to another host (HTTP 409 Conflict) |
|
||||||
|
| **Certificate isolation** | Each approved host receives a unique client certificate signed by internal CA. Certificates have max 1-year validity |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Error Reference Table
|
||||||
|
|
||||||
|
| HTTP Status | Error Context | Description | Retryable |
|
||||||
|
|-------------|---------------|-------------|----------|
|
||||||
|
| `429` | POST /enroll | Rate limit exceeded (1/min per IP) | Yes — wait 60s |
|
||||||
|
| `409` | Admin approve endpoint | FQDN or IP collision with existing host | No — resolve conflict first |
|
||||||
|
| `500` | Any enrollment endpoint | Database error or internal server failure | Yes — transient |
|
||||||
|
| `200` `{ "status": "denied" }` | GET /enroll/status/{token} | Administrator rejected request | No — contact administrator |
|
||||||
|
| `200` `{ "status": "not_found" }` | GET /enroll/status/{token} | Token expired, invalid, or already consumed | No — re-enroll with new request |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
- **Documentation:** [README.md](./README.md)
|
- **Documentation:** [README.md](./README.md)
|
||||||
|
|||||||
@ -269,18 +269,37 @@ Client → [mTLS + IP Check] → [API Layer] → [GET /jobs/{id}]
|
|||||||
|
|
||||||
### Endpoint: GET /health
|
### Endpoint: GET /health
|
||||||
|
|
||||||
**Purpose:** General service status check
|
**Purpose:** General service status check with package cache status
|
||||||
|
|
||||||
**Response (200 OK - Healthy):**
|
**Response (200 OK - Healthy):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"request_id": "uuid",
|
"request_id": "uuid",
|
||||||
"timestamp": "2026-04-09T13:04:02Z",
|
"timestamp": "2026-05-27T14:00:00Z",
|
||||||
"data": {
|
"data": {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"uptime_seconds": 12345,
|
"uptime_seconds": 12345,
|
||||||
"version": "0.0.1"
|
"version": "1.1.17",
|
||||||
|
"last_cache_update": "2026-05-27T13:30:00+00:00",
|
||||||
|
"cache_status": "fresh"
|
||||||
|
},
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200 OK - Degraded):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"request_id": "uuid",
|
||||||
|
"timestamp": "2026-05-27T14:00:00Z",
|
||||||
|
"data": {
|
||||||
|
"status": "degraded",
|
||||||
|
"uptime_seconds": 12345,
|
||||||
|
"version": "1.1.17",
|
||||||
|
"last_cache_update": "2026-05-27T09:00:00+00:00",
|
||||||
|
"cache_status": "failed"
|
||||||
},
|
},
|
||||||
"error": null
|
"error": null
|
||||||
}
|
}
|
||||||
@ -291,6 +310,19 @@ Client → [mTLS + IP Check] → [API Layer] → [GET /jobs/{id}]
|
|||||||
- mTLS is configured and valid
|
- mTLS is configured and valid
|
||||||
- Config file is loaded and valid
|
- Config file is loaded and valid
|
||||||
- Package manager backend is accessible
|
- Package manager backend is accessible
|
||||||
|
- Package cache is fresh (refreshed within 4 hours)
|
||||||
|
|
||||||
|
**Cache Refresh on Health Check:**
|
||||||
|
- If cache is stale (>4 hours since last update), health check triggers a cache refresh
|
||||||
|
- If refresh succeeds: status="healthy", cache_status="fresh"
|
||||||
|
- If refresh fails: status="degraded", cache_status="failed"
|
||||||
|
- If cache is fresh: status="healthy", cache_status="fresh"
|
||||||
|
|
||||||
|
**Cache Status Values:**
|
||||||
|
- `fresh` - Cache was updated within the last 4 hours
|
||||||
|
- `stale` - Cache is older than 4 hours (triggers refresh)
|
||||||
|
- `unknown` - No cache update has occurred yet
|
||||||
|
- `failed` - Last cache refresh attempt failed
|
||||||
|
|
||||||
**NOT Required:**
|
**NOT Required:**
|
||||||
- Metrics collection
|
- Metrics collection
|
||||||
@ -299,4 +331,41 @@ Client → [mTLS + IP Check] → [API Layer] → [GET /jobs/{id}]
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Package Cache Management
|
||||||
|
|
||||||
|
### Module: `src/packages/cache.rs`
|
||||||
|
|
||||||
|
The package cache module manages the local package index state, ensuring that package metadata is current before performing operations.
|
||||||
|
|
||||||
|
**Key Components:**
|
||||||
|
- `PackageCacheState` - Thread-safe in-memory cache state with Mutex protection
|
||||||
|
- `PackageCacheStatus` - Snapshot of cache state for reporting
|
||||||
|
- `CacheStateFile` - Persistent state format for serialization
|
||||||
|
- `is_fetch_error()` - Detects 404/fetch errors for automatic retry
|
||||||
|
- `apply_with_cache_retry()` - Generic retry wrapper for cache-related failures
|
||||||
|
- `run_command_with_timeout()` - Executes cache refresh commands with timeout
|
||||||
|
|
||||||
|
**State Persistence:**
|
||||||
|
- Cache state persists to `/var/lib/linux_patch_api/state/cache.json`
|
||||||
|
- State is loaded on service startup and saved after every update
|
||||||
|
- Persists `last_cache_update` timestamp and `last_update_success` flag
|
||||||
|
- Parent directory is auto-created if missing
|
||||||
|
|
||||||
|
**Stale Detection:**
|
||||||
|
- Cache is considered stale after 4 hours (`STALE_THRESHOLD_SECS = 14400`)
|
||||||
|
- Health check automatically refreshes stale cache
|
||||||
|
- Patch apply operations always refresh cache before proceeding (mandatory)
|
||||||
|
|
||||||
|
**Refresh-Before-Apply Flow:**
|
||||||
|
1. `POST /patches/apply` creates a job and spawns background task
|
||||||
|
2. Background task refreshes package cache (mandatory, not configurable)
|
||||||
|
3. If refresh fails: job fails immediately with error message
|
||||||
|
4. If refresh succeeds: job progresses to 10%, applies patches
|
||||||
|
5. If apply fails with 404/fetch error: refresh cache and retry once
|
||||||
|
6. If retry also fails: job fails with error
|
||||||
|
|
||||||
|
**Cache Refresh Timeout:** 120 seconds (`CACHE_REFRESH_TIMEOUT_SECS`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
*Following kiro spec-driven development standards*
|
*Following kiro spec-driven development standards*
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Linux Patch API - Package Build Guide
|
# Linux Patch API - Package Build Guide
|
||||||
|
|
||||||
This document provides comprehensive instructions for building production-ready Debian (.deb) and RPM (.rpm) packages for the Linux Patch API.
|
This document provides comprehensive instructions for building production-ready packages for the Linux Patch API across all supported platforms: Debian/Ubuntu (.deb), RHEL/CentOS/Fedora (.rpm), Arch Linux (.pkg.tar.zst), and Alpine Linux (.apk).
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@ -173,6 +173,152 @@ rpm -ql linux-patch-api
|
|||||||
rpm -e linux-patch-api
|
rpm -e linux-patch-api
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Building Arch Package (.pkg.tar.zst)
|
||||||
|
|
||||||
|
### Quick Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/linux_patch_api
|
||||||
|
|
||||||
|
# Build release binary
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Build Arch package
|
||||||
|
chmod +x build-arch.sh
|
||||||
|
SKIP_CARGO_BUILD=1 ./build-arch.sh
|
||||||
|
|
||||||
|
# Package will be created in releases/
|
||||||
|
ls -la releases/*.pkg.tar.zst
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detailed Build Process
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install build dependencies (Arch Linux)
|
||||||
|
sudo pacman -Syu --noconfirm rust cargo systemd git base-devel gcc
|
||||||
|
|
||||||
|
# 2. Build release binary
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# 3. Run build script
|
||||||
|
chmod +x build-arch.sh
|
||||||
|
./build-arch.sh
|
||||||
|
|
||||||
|
# 4. Verify package contents
|
||||||
|
bsdtar -tf releases/linux-patch-api-*.pkg.tar.zst
|
||||||
|
|
||||||
|
# 5. Verify package info
|
||||||
|
pacman -Qi releases/linux-patch-api-*.pkg.tar.zst
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Script Hooks
|
||||||
|
|
||||||
|
The Arch package includes an `.install` file (`configs/linux-patch-api.install`) that runs automatically on install:
|
||||||
|
|
||||||
|
- **post_install**: Creates directories, copies example configs, enables systemd service
|
||||||
|
- **post_upgrade**: Reloads systemd daemon
|
||||||
|
- **pre_remove**: Stops and disables service
|
||||||
|
- **post_remove**: Cleans up empty directories
|
||||||
|
|
||||||
|
### Installation Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install the package
|
||||||
|
sudo pacman -U ./releases/linux-patch-api-*.pkg.tar.zst
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
systemctl status linux-patch-api
|
||||||
|
linux-patch-api --version
|
||||||
|
|
||||||
|
# Check installed files
|
||||||
|
pacman -Ql linux-patch-api
|
||||||
|
|
||||||
|
# Verify config files exist
|
||||||
|
ls -la /etc/linux_patch_api/
|
||||||
|
|
||||||
|
# Remove package
|
||||||
|
sudo pacman -R linux-patch-api
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** The build script creates a `builduser` for `makepkg` when running as root (typical in CI environments). The `.install` hook handles directory creation, config copying, and service enablement.
|
||||||
|
|
||||||
|
## Building Alpine Package (.apk)
|
||||||
|
|
||||||
|
### Quick Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/linux_patch_api
|
||||||
|
|
||||||
|
# Build release binary (MUSL target for Alpine)
|
||||||
|
cargo build --release --target x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
# Build Alpine package
|
||||||
|
chmod +x build-alpine.sh
|
||||||
|
SKIP_CARGO_BUILD=1 ./build-alpine.sh
|
||||||
|
|
||||||
|
# Package will be created in releases/
|
||||||
|
ls -la releases/*.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detailed Build Process
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install build dependencies (Alpine Linux)
|
||||||
|
apk add --no-cache alpine-sdk rust cargo openssl openssl-dev elogind-dev musl-dev abuild gcc
|
||||||
|
|
||||||
|
# 2. Add Rust MUSL target
|
||||||
|
rustup target add x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
# 3. Build release binary
|
||||||
|
cargo build --release --target x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
# 4. Run build script
|
||||||
|
chmod +x build-alpine.sh
|
||||||
|
./build-alpine.sh
|
||||||
|
|
||||||
|
# 5. Verify package contents
|
||||||
|
apk verify releases/*.apk
|
||||||
|
|
||||||
|
# 6. List package contents
|
||||||
|
tar -tzf releases/*.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Script Hooks
|
||||||
|
|
||||||
|
The Alpine package includes an install script (`configs/linux-patch-api.apk-install`) that runs automatically on install:
|
||||||
|
|
||||||
|
- **pre_install**: Creates directories, sets ownership and permissions
|
||||||
|
- **post_install**: Copies example configs, adds service to default runlevel
|
||||||
|
- **pre_deinstall**: Stops and removes service from runlevel
|
||||||
|
- **post_deinstall**: Cleans up empty directories
|
||||||
|
|
||||||
|
### Installation Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install the package
|
||||||
|
sudo apk add --allow-unstable ./releases/linux-patch-api-*.apk
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
rc-service linux-patch-api status
|
||||||
|
linux-patch-api --version
|
||||||
|
|
||||||
|
# Check installed files
|
||||||
|
apk info -L linux-patch-api
|
||||||
|
|
||||||
|
# Verify config files exist
|
||||||
|
ls -la /etc/linux_patch_api/
|
||||||
|
|
||||||
|
# Remove package
|
||||||
|
sudo apk del linux-patch-api
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Alpine uses **OpenRC** instead of systemd. Key differences:
|
||||||
|
- Start service: `rc-service linux-patch-api start`
|
||||||
|
- Stop service: `rc-service linux-patch-api stop`
|
||||||
|
- Check status: `rc-service linux-patch-api status`
|
||||||
|
- Service init script: `/etc/init.d/linux-patch-api`
|
||||||
|
- The `abuild` tool generates signing keys automatically for CI builds
|
||||||
|
|
||||||
## Using the Interactive Installer
|
## Using the Interactive Installer
|
||||||
|
|
||||||
For manual deployment without package managers:
|
For manual deployment without package managers:
|
||||||
@ -209,15 +355,17 @@ The installer will:
|
|||||||
| `/var/lib/linux_patch_api/` | Data directory | 755 |
|
| `/var/lib/linux_patch_api/` | Data directory | 755 |
|
||||||
| `/var/log/linux_patch_api/` | Log directory | 755 |
|
| `/var/log/linux_patch_api/` | Log directory | 755 |
|
||||||
|
|
||||||
### System User/Group
|
### Service Account
|
||||||
|
|
||||||
| Property | Value |
|
| Property | Value |
|
||||||
|----------|-------|
|
|----------|-------|
|
||||||
| User | linux-patch-api |
|
| User | root |
|
||||||
| Group | linux-patch-api |
|
| Group | root |
|
||||||
| Home | /var/lib/linux_patch_api |
|
| Home | /var/lib/linux_patch_api |
|
||||||
| Shell | /usr/sbin/nologin |
|
| Shell | N/A (systemd service) |
|
||||||
| Type | System account |
|
| Type | Runs as root (required for package management) |
|
||||||
|
|
||||||
|
**Note:** The service runs as root because package management operations (apt, dnf, apk, pacman) require root privileges. Security is provided by mTLS + IP whitelist, not process isolation.
|
||||||
|
|
||||||
## Supported Distributions
|
## Supported Distributions
|
||||||
|
|
||||||
@ -240,6 +388,19 @@ The installer will:
|
|||||||
| AlmaLinux | 8, 9 | ✅ Supported |
|
| AlmaLinux | 8, 9 | ✅ Supported |
|
||||||
| Rocky Linux | 8, 9 | ✅ Supported |
|
| Rocky Linux | 8, 9 | ✅ Supported |
|
||||||
|
|
||||||
|
### Arch Package (.pkg.tar.zst)
|
||||||
|
|
||||||
|
| Distribution | Versions | Status |
|
||||||
|
|--------------|----------|--------|
|
||||||
|
| Arch Linux | Rolling | ✅ Supported |
|
||||||
|
| Manjaro | Rolling | ✅ Supported |
|
||||||
|
|
||||||
|
### Alpine Package (.apk)
|
||||||
|
|
||||||
|
| Distribution | Versions | Status |
|
||||||
|
|--------------|----------|--------|
|
||||||
|
| Alpine Linux | 3.18+ | ✅ Supported |
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Debian Package Issues
|
### Debian Package Issues
|
||||||
@ -276,9 +437,62 @@ cat ~/rpmbuild/BUILDROOT/*/var/log/rpmbuild.log
|
|||||||
dnf install -y systemd-devel pkgconfig
|
dnf install -y systemd-devel pkgconfig
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Arch Package Issues
|
||||||
|
|
||||||
|
**Error: `makepkg: cannot run as root`**
|
||||||
|
```bash
|
||||||
|
# The build script handles this automatically by creating builduser
|
||||||
|
# If running manually:
|
||||||
|
useradd -m builduser
|
||||||
|
su - builduser -c "cd /path/to/repo && makepkg -f --noconfirm"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error: `install script not found`**
|
||||||
|
```bash
|
||||||
|
# Ensure linux-patch-api.install is in the same directory as PKGBUILD
|
||||||
|
ls -la configs/linux-patch-api.install
|
||||||
|
# The build script copies it automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error: `Permission denied` on config files**
|
||||||
|
```bash
|
||||||
|
# Verify ownership is root:root
|
||||||
|
ls -la /etc/linux_patch_api/
|
||||||
|
# Fix if needed:
|
||||||
|
sudo chown -R root:root /etc/linux_patch_api/
|
||||||
|
sudo chmod 750 /etc/linux_patch_api /etc/linux_patch_api/certs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alpine Package Issues
|
||||||
|
|
||||||
|
**Error: `abuild: UNTRUSTED signature`**
|
||||||
|
```bash
|
||||||
|
# The build script handles key generation automatically
|
||||||
|
# If running manually:
|
||||||
|
abuild-keygen -a -n
|
||||||
|
cp /root/.abuild/*.rsa.pub /etc/apk/keys/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error: `apk add: ERROR: failed to create directory`**
|
||||||
|
```bash
|
||||||
|
# Verify the install script ran correctly
|
||||||
|
ls -la /etc/linux_patch_api/
|
||||||
|
ls -la /var/lib/linux_patch_api/
|
||||||
|
# Manually create if needed:
|
||||||
|
sudo mkdir -p /etc/linux_patch_api/certs /var/lib/linux_patch_api /var/log/linux_patch_api
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error: `rc-service: service not found`**
|
||||||
|
```bash
|
||||||
|
# Verify the init script exists
|
||||||
|
ls -la /etc/init.d/linux-patch-api
|
||||||
|
# Re-add to default runlevel
|
||||||
|
sudo rc-update add linux-patch-api default
|
||||||
|
```
|
||||||
|
|
||||||
### Service Issues
|
### Service Issues
|
||||||
|
|
||||||
**Service fails to start:**
|
**Service fails to start (systemd):**
|
||||||
```bash
|
```bash
|
||||||
# Check service status
|
# Check service status
|
||||||
systemctl status linux-patch-api
|
systemctl status linux-patch-api
|
||||||
@ -293,6 +507,22 @@ linux-patch-api --config /etc/linux_patch_api/config.yaml --check
|
|||||||
ls -la /etc/linux_patch_api/certs/
|
ls -la /etc/linux_patch_api/certs/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Service fails to start (OpenRC/Alpine):**
|
||||||
|
```bash
|
||||||
|
# Check service status
|
||||||
|
rc-service linux-patch-api status
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
cat /var/log/linux_patch_api/linux-patch-api.log
|
||||||
|
cat /var/log/linux_patch_api/linux-patch-api.err
|
||||||
|
|
||||||
|
# Check configuration
|
||||||
|
linux-patch-api --config /etc/linux_patch_api/config.yaml --check
|
||||||
|
|
||||||
|
# Verify certificates
|
||||||
|
ls -la /etc/linux_patch_api/certs/
|
||||||
|
```
|
||||||
|
|
||||||
## CI/CD Integration
|
## CI/CD Integration
|
||||||
|
|
||||||
### GitHub Actions Example
|
### GitHub Actions Example
|
||||||
@ -383,7 +613,7 @@ jobs:
|
|||||||
- Packages are signed with maintainer GPG key for production deployments
|
- Packages are signed with maintainer GPG key for production deployments
|
||||||
- All maintainer scripts run with `set -e` for fail-fast behavior
|
- All maintainer scripts run with `set -e` for fail-fast behavior
|
||||||
- Configuration files are marked as conffiles to preserve user modifications
|
- Configuration files are marked as conffiles to preserve user modifications
|
||||||
- System user has minimal privileges (nologin shell, no home directory)
|
- Service runs as root (required for package management operations)
|
||||||
- Directory permissions follow principle of least privilege
|
- Directory permissions follow principle of least privilege
|
||||||
- TLS certificates should be replaced with CA-signed certs in production
|
- TLS certificates should be replaced with CA-signed certs in production
|
||||||
|
|
||||||
|
|||||||
19
CHANGELOG.md
19
CHANGELOG.md
@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Self-enrollment workflow**: Automated host registration with linux_patch_manager
|
||||||
|
- CLI flag: `--enroll <MANAGER_URL>` for enrollment mode
|
||||||
|
- Three-phase enrollment: Registration → Polling (24h timeout) → PKI Provisioning
|
||||||
|
- Automatic certificate provisioning to configured mTLS paths
|
||||||
|
- Automatic manager IP whitelist append after successful enrollment
|
||||||
|
- Configurable polling interval (default 60s) and max attempts (default 1440/24h)
|
||||||
|
- Signal handling for graceful shutdown during enrollment
|
||||||
|
- Enrollment configuration section in config.yaml (`enrollment.*`)
|
||||||
|
- Identity extraction module (machine-id, FQDN, IP addresses, OS details)
|
||||||
|
- PKI bundle validation with PEM format checking
|
||||||
|
- Atomic certificate file writing with secure permissions (key=0600, certs=0644)
|
||||||
|
- Whitelist auto-append with file locking and duplicate detection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.0.0] - 2026-07-17
|
## [1.0.0] - 2026-07-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -191,6 +209,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
| Version | Release Date | Status | Key Milestone |
|
| Version | Release Date | Status | Key Milestone |
|
||||||
|---------|--------------|--------|---------------|
|
|---------|--------------|--------|---------------|
|
||||||
|
| Unreleased | TBD | In Development | Self-enrollment feature complete |
|
||||||
| 1.0.0 | 2026-07-17 | Production | Initial production release |
|
| 1.0.0 | 2026-07-17 | Production | Initial production release |
|
||||||
| 0.1.0 | 2026-04-09 | Development | Initial development release |
|
| 0.1.0 | 2026-04-09 | Development | Initial development release |
|
||||||
|
|
||||||
|
|||||||
79
CONTRIBUTING.md
Normal file
79
CONTRIBUTING.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# Contributing to Linux-Patch-Api
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to Linux-Patch-Api! We appreciate every contribution — from bug reports and documentation improvements to new features and security fixes.
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
This project follows the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) code of conduct. By participating, you are expected to uphold this standard. Please report unacceptable behavior to the maintainers.
|
||||||
|
|
||||||
|
## How to Contribute
|
||||||
|
|
||||||
|
1. **Fork** the repository
|
||||||
|
2. Create a **feature branch** from `main`:
|
||||||
|
```bash
|
||||||
|
git checkout -b feat/my-feature
|
||||||
|
```
|
||||||
|
3. Make your changes
|
||||||
|
4. Ensure all CI checks pass:
|
||||||
|
```bash
|
||||||
|
cargo fmt --check
|
||||||
|
cargo clippy -- -D warnings
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
5. **Commit** using conventional commit format (see below)
|
||||||
|
6. Open a **Pull Request** against `main`
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Rust toolchain** (stable) — [rustup](https://rustup.rs/)
|
||||||
|
- **System dependencies**:
|
||||||
|
```bash
|
||||||
|
sudo apt-get install build-essential libsystemd-dev pkg-config libssl-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build & Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
We use [Conventional Commits](https://www.conventionalcommits.org/):
|
||||||
|
|
||||||
|
| Prefix | Usage |
|
||||||
|
|----------|------------------------|
|
||||||
|
| `feat:` | New feature |
|
||||||
|
| `fix:` | Bug fix |
|
||||||
|
| `docs:` | Documentation changes |
|
||||||
|
| `chore:` | Maintenance tasks |
|
||||||
|
| `refactor:` | Code refactoring |
|
||||||
|
| `test:` | Adding or updating tests |
|
||||||
|
| `ci:` | CI configuration changes |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
feat: add endpoint for patch rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pull Request Requirements
|
||||||
|
|
||||||
|
- All CI checks must pass (fmt, clippy, test, audit, build)
|
||||||
|
- One feature or fix per PR — keep changes focused
|
||||||
|
- Include a clear description of what changed and why
|
||||||
|
- Update documentation if your change affects behavior
|
||||||
|
|
||||||
|
## Reporting Issues
|
||||||
|
|
||||||
|
Use [GitHub Issues](https://github.com/Draco-Lunaris/Linux-Patch-Api/issues) to report bugs, request features, or ask questions. Please include:
|
||||||
|
|
||||||
|
- Steps to reproduce (for bugs)
|
||||||
|
- Expected vs. actual behavior
|
||||||
|
- Relevant logs or error messages
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing, you agree that your contributions are licensed under the [Apache License 2.0](LICENSE), the same license as this project.
|
||||||
467
Cargo.lock
generated
467
Cargo.lock
generated
@ -44,6 +44,18 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-governor"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0954b0f27aabd8f56bb03f2a77b412ddf3f8c034a3c27b2086c1fc75415760df"
|
||||||
|
dependencies = [
|
||||||
|
"actix-http",
|
||||||
|
"actix-web",
|
||||||
|
"futures",
|
||||||
|
"governor",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.12.1"
|
version = "3.12.1"
|
||||||
@ -390,6 +402,15 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arc-swap"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arraydeque"
|
name = "arraydeque"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@ -959,6 +980,19 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dashmap"
|
||||||
|
version = "5.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
"lock_api",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-encoding"
|
name = "data-encoding"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
@ -1209,6 +1243,16 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs2"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fs_extra"
|
name = "fs_extra"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@ -1295,6 +1339,12 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-timer"
|
||||||
|
version = "3.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@ -1329,8 +1379,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1340,9 +1392,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 5.3.0",
|
"r-efi 5.3.0",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1359,6 +1413,26 @@ dependencies = [
|
|||||||
"wasip3",
|
"wasip3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "governor"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"dashmap",
|
||||||
|
"futures",
|
||||||
|
"futures-timer",
|
||||||
|
"no-std-compat",
|
||||||
|
"nonzero_ext",
|
||||||
|
"parking_lot",
|
||||||
|
"portable-atomic",
|
||||||
|
"quanta",
|
||||||
|
"rand 0.8.6",
|
||||||
|
"smallvec",
|
||||||
|
"spinning_top",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.27"
|
version = "0.3.27"
|
||||||
@ -1454,6 +1528,12 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@ -1541,18 +1621,43 @@ dependencies = [
|
|||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-rustls"
|
||||||
|
version = "0.27.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||||
|
dependencies = [
|
||||||
|
"http 1.4.0",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower-service",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
"http 1.4.0",
|
"http 1.4.0",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
"ipnet",
|
||||||
|
"libc",
|
||||||
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"socket2 0.6.3",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1688,6 +1793,16 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "if-addrs"
|
||||||
|
version = "0.13.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69b2eeee38fef3aa9b4cc5f1beea8a2444fc00e7377cafae396de3f5c2065e24"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "impl-more"
|
name = "impl-more"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@ -1726,6 +1841,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipnet"
|
||||||
|
version = "2.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is-terminal"
|
name = "is-terminal"
|
||||||
version = "0.4.17"
|
version = "0.4.17"
|
||||||
@ -1774,6 +1895,8 @@ version = "0.3.95"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
|
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@ -1859,33 +1982,44 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-patch-api"
|
name = "linux-patch-api"
|
||||||
version = "0.3.2"
|
version = "1.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix",
|
"actix",
|
||||||
|
"actix-governor",
|
||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix-tls",
|
"actix-tls",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"actix-web-actors",
|
"actix-web-actors",
|
||||||
"addr",
|
"addr",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"arc-swap",
|
||||||
"async-channel",
|
"async-channel",
|
||||||
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"config",
|
"config",
|
||||||
"criterion",
|
"criterion",
|
||||||
|
"fs2",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"hex",
|
||||||
|
"if-addrs",
|
||||||
"notify",
|
"notify",
|
||||||
"pidlock",
|
"pidlock",
|
||||||
|
"rand 0.8.6",
|
||||||
|
"rcgen",
|
||||||
|
"reqwest",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
|
"socket2 0.5.10",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"systemd",
|
"systemd",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tokio-test",
|
"tokio-test",
|
||||||
@ -1893,6 +2027,7 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wiremock",
|
"wiremock",
|
||||||
"x509-parser",
|
"x509-parser",
|
||||||
@ -1942,6 +2077,12 @@ version = "0.4.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru-slab"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -2015,6 +2156,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "no-std-compat"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
@ -2025,6 +2172,12 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nonzero_ext"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify"
|
name = "notify"
|
||||||
version = "6.1.1"
|
version = "6.1.1"
|
||||||
@ -2178,6 +2331,16 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
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]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@ -2284,6 +2447,12 @@ dependencies = [
|
|||||||
"plotters-backend",
|
"plotters-backend",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@ -2342,6 +2511,76 @@ version = "2.0.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quanta"
|
||||||
|
version = "0.12.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"raw-cpuid",
|
||||||
|
"wasi",
|
||||||
|
"web-sys",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn"
|
||||||
|
version = "0.11.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"cfg_aliases",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn-proto",
|
||||||
|
"quinn-udp",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"socket2 0.6.3",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-proto"
|
||||||
|
version = "0.11.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"lru-slab",
|
||||||
|
"rand 0.9.4",
|
||||||
|
"ring",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"slab",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tinyvec",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-udp"
|
||||||
|
version = "0.5.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"socket2 0.6.3",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.45"
|
||||||
@ -2370,10 +2609,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha",
|
"rand_chacha 0.3.1",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@ -2395,6 +2644,16 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@ -2404,12 +2663,30 @@ dependencies = [
|
|||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "raw-cpuid"
|
||||||
|
version = "11.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
@ -2430,6 +2707,20 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@ -2483,6 +2774,44 @@ version = "0.8.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.12.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http 1.4.0",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
|
"hyper-util",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@ -2519,6 +2848,12 @@ dependencies = [
|
|||||||
"ordered-multimap",
|
"ordered-multimap",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@ -2559,6 +2894,7 @@ dependencies = [
|
|||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
"subtle",
|
"subtle",
|
||||||
@ -2580,6 +2916,7 @@ version = "1.14.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"web-time",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2842,6 +3179,15 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spinning_top"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@ -2877,6 +3223,15 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sync_wrapper"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "synstructure"
|
name = "synstructure"
|
||||||
version = "0.13.2"
|
version = "0.13.2"
|
||||||
@ -3040,6 +3395,21 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec_macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.52.1"
|
version = "1.52.1"
|
||||||
@ -3166,6 +3536,51 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.6.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http 1.4.0",
|
||||||
|
"http-body",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-layer"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-service"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.44"
|
version = "0.1.44"
|
||||||
@ -3437,6 +3852,16 @@ dependencies = [
|
|||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-futures"
|
||||||
|
version = "0.4.68"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.118"
|
version = "0.2.118"
|
||||||
@ -3513,6 +3938,25 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@ -3682,6 +4126,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@ -3965,6 +4418,7 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"nom",
|
"nom",
|
||||||
"oid-registry",
|
"oid-registry",
|
||||||
|
"ring",
|
||||||
"rusticata-macros",
|
"rusticata-macros",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"time",
|
"time",
|
||||||
@ -3981,6 +4435,15 @@ dependencies = [
|
|||||||
"hashlink",
|
"hashlink",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yasna"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||||
|
dependencies = [
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|||||||
52
Cargo.toml
52
Cargo.toml
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "linux-patch-api"
|
name = "linux-patch-api"
|
||||||
version = "0.3.2"
|
version = "1.3.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Echo <echo@moon-dragon.us>"]
|
authors = ["Echo <echo@moon-dragon.us>"]
|
||||||
description = "Secure remote package management API for Linux systems"
|
description = "Secure remote package management API for Linux systems"
|
||||||
@ -16,6 +16,9 @@ actix-web-actors = "4"
|
|||||||
actix = "0.13"
|
actix = "0.13"
|
||||||
actix-tls = { version = "3", features = ["rustls-0_23"] }
|
actix-tls = { version = "3", features = ["rustls-0_23"] }
|
||||||
|
|
||||||
|
# Rate limiting (actix-governor for per-IP rate limiting)
|
||||||
|
actix-governor = "0.6"
|
||||||
|
|
||||||
# Async runtime
|
# Async runtime
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
||||||
@ -23,7 +26,7 @@ tokio = { version = "1", features = ["full"] }
|
|||||||
rustls = { version = "0.23", features = ["aws_lc_rs"] }
|
rustls = { version = "0.23", features = ["aws_lc_rs"] }
|
||||||
rustls-pemfile = "2"
|
rustls-pemfile = "2"
|
||||||
tokio-rustls = "0.26"
|
tokio-rustls = "0.26"
|
||||||
x509-parser = "0.16"
|
x509-parser = { version = "0.16", features = ["verify"] }
|
||||||
|
|
||||||
# WebSocket support (actix-web-actors provides WebSocket for Actix-web)
|
# WebSocket support (actix-web-actors provides WebSocket for Actix-web)
|
||||||
tokio-tungstenite = "0.21"
|
tokio-tungstenite = "0.21"
|
||||||
@ -48,6 +51,7 @@ uuid = { version = "1", features = ["v4", "serde"] }
|
|||||||
|
|
||||||
# Time/Date
|
# Time/Date
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
time = "0.3"
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
@ -61,6 +65,10 @@ sysinfo = "0.30"
|
|||||||
|
|
||||||
# Network utilities
|
# Network utilities
|
||||||
addr = "0.15"
|
addr = "0.15"
|
||||||
|
if-addrs = "0.13"
|
||||||
|
|
||||||
|
# HTTP client for enrollment communication
|
||||||
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
|
|
||||||
# Clap for CLI arguments
|
# Clap for CLI arguments
|
||||||
clap = { version = "4", features = ["derive", "env"] }
|
clap = { version = "4", features = ["derive", "env"] }
|
||||||
@ -69,14 +77,54 @@ clap = { version = "4", features = ["derive", "env"] }
|
|||||||
systemd = "0.10"
|
systemd = "0.10"
|
||||||
pidlock = "0.2"
|
pidlock = "0.2"
|
||||||
|
|
||||||
|
# URL parsing
|
||||||
|
url = "2"
|
||||||
|
|
||||||
|
# Socket options (SO_REUSEADDR)
|
||||||
|
socket2 = { version = "0.5", features = ["all"] }
|
||||||
|
|
||||||
|
# File locking for concurrent-safe whitelist modifications
|
||||||
|
fs2 = "0.4"
|
||||||
|
|
||||||
|
# Atomic swapping for CRL state updates without rebuilding ServerConfig
|
||||||
|
arc-swap = "1"
|
||||||
|
|
||||||
|
# Base64 decoding for PEM CRL parsing
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2"
|
actix-rt = "2"
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
wiremock = "0.6"
|
wiremock = "0.6"
|
||||||
serial_test = "3"
|
serial_test = "3"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
rcgen = { version = "0.13", features = ["pem", "x509-parser"] }
|
||||||
|
rand = "0.8"
|
||||||
|
hex = "0.4"
|
||||||
|
time = { version = "0.3", features = ["std"] }
|
||||||
criterion = { version = "0.5", features = ["html_reports"] }
|
criterion = { version = "0.5", features = ["html_reports"] }
|
||||||
|
|
||||||
|
# Integration tests in subdirectories
|
||||||
|
[[test]]
|
||||||
|
name = "enroll_identity"
|
||||||
|
path = "tests/unit/enroll_identity.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "enrollment_test"
|
||||||
|
path = "tests/integration/enrollment_test.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "enrollment_e2e"
|
||||||
|
path = "tests/e2e/test_enrollment_e2e.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "auth_test"
|
||||||
|
path = "tests/integration/auth_test.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "rate_limit_test"
|
||||||
|
path = "tests/unit/rate_limit_test.rs"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "api_benchmarks"
|
name = "api_benchmarks"
|
||||||
harness = false
|
harness = false
|
||||||
|
|||||||
@ -16,6 +16,7 @@ Complete guide for deploying Linux Patch API to production environments.
|
|||||||
- [RHEL/CentOS/Fedora Deployment](#rhelcentosfedora-deployment)
|
- [RHEL/CentOS/Fedora Deployment](#rhelcentosfedora-deployment)
|
||||||
- [Manual Deployment](#manual-deployment)
|
- [Manual Deployment](#manual-deployment)
|
||||||
- [Certificate Deployment](#certificate-deployment)
|
- [Certificate Deployment](#certificate-deployment)
|
||||||
|
- [Self-Enrollment Deployment](#self-enrollment-deployment)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [systemd Service Management](#systemd-service-management)
|
- [systemd Service Management](#systemd-service-management)
|
||||||
- [Monitoring and Logging](#monitoring-and-logging)
|
- [Monitoring and Logging](#monitoring-and-logging)
|
||||||
@ -180,7 +181,7 @@ tls:
|
|||||||
ca_cert: "/etc/linux_patch_api/certs/ca.pem"
|
ca_cert: "/etc/linux_patch_api/certs/ca.pem"
|
||||||
server_cert: "/etc/linux_patch_api/certs/server.pem"
|
server_cert: "/etc/linux_patch_api/certs/server.pem"
|
||||||
server_key: "/etc/linux_patch_api/certs/server.key"
|
server_key: "/etc/linux_patch_api/certs/server.key"
|
||||||
min_tls_version: "1.3"
|
# TLS 1.3 is the only supported version (hardcoded, not configurable)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
max_concurrent: 5
|
max_concurrent: 5
|
||||||
@ -445,6 +446,328 @@ shred -u /tmp/client001.key.pem
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Self-Enrollment Deployment
|
||||||
|
|
||||||
|
Self-enrollment allows a new host to automatically request and receive mTLS certificates from the `linux_patch_manager` without manual PKI distribution. The daemon supports two enrollment modes:
|
||||||
|
|
||||||
|
1. **Auto-enrollment (recommended):** When `enrollment.manager_url` is configured in `config.yaml`, the daemon automatically enrolls on startup when certificates are missing or invalid. After provisioning, it continues to normal mTLS server startup.
|
||||||
|
|
||||||
|
2. **Manual enrollment:** Run `linux-patch-api --enroll <manager_url>` explicitly. The process provisions certificates and **exits** — it does NOT start the server. Start the service separately after enrollment completes.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
The enrollment workflow operates in three phases:
|
||||||
|
|
||||||
|
1. **Registration:** Extracts `/etc/machine-id`, FQDN, IP address, and OS details. Submits an unauthenticated `POST /api/v1/enroll` request to the manager. Receives a temporary `polling_token`.
|
||||||
|
2. **Polling & Approval:** Enters a polling loop querying `GET /api/v1/enroll/status/{token}` (default: every 60 seconds, up to 1440 attempts = 24 hours). Aborts on HTTP 403/404 (denied/purged). The polling token is persisted to `config.yaml` for resume after service restart.
|
||||||
|
3. **Provisioning:** On HTTP 200, downloads the PKI bundle (`ca.crt`, `server.crt`, `server.key`), writes certificates to configured mTLS paths, appends manager IP to whitelist. For auto-enrollment, transitions to standard mTLS listening mode. For `--enroll`, exits with code 0.
|
||||||
|
|
||||||
|
### Certificate Validation
|
||||||
|
|
||||||
|
On startup, the daemon validates all configured TLS certificates before attempting to bind the listening port:
|
||||||
|
|
||||||
|
1. **Existence:** All three cert files must exist at configured paths
|
||||||
|
2. **Parse:** Each file must be valid PEM (X.509 for certs, PKCS#8/PKCS#1 for keys)
|
||||||
|
3. **Expiry:** Certs must not be expired. Certs expiring within `cert_renewal_threshold_days` (default 7) trigger a warning
|
||||||
|
4. **Key match:** Server cert public key must correspond to server key private key
|
||||||
|
5. **CA trust:** Server cert must be signed by the CA cert
|
||||||
|
|
||||||
|
Validation results determine startup behavior:
|
||||||
|
|
||||||
|
| Result | Action |
|
||||||
|
|--------|--------|
|
||||||
|
| Valid | Start normally with mTLS |
|
||||||
|
| ExpiringSoon | Log warning, start normally, schedule re-enrollment |
|
||||||
|
| Missing/Corrupt/Expired/KeyMismatch/Untrusted | Auto-enroll if `enrollment.manager_url` configured, otherwise exit with guidance |
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
| Requirement | Details |
|
||||||
|
|-------------|---------|
|
||||||
|
| Manager URL | Must be accessible from the host (HTTPS) |
|
||||||
|
| Network Connectivity | Outbound HTTPS to manager endpoint |
|
||||||
|
| DNS Resolution | Manager hostname must resolve correctly |
|
||||||
|
| systemd | Version 237+ for service management |
|
||||||
|
| Root Access | Required for certificate file writes |
|
||||||
|
|
||||||
|
**Verification before enrollment:**
|
||||||
|
```bash
|
||||||
|
# Verify network connectivity to manager
|
||||||
|
curl -I https://manager.example.com
|
||||||
|
|
||||||
|
# Verify DNS resolution
|
||||||
|
nslookup manager.example.com
|
||||||
|
|
||||||
|
# Verify outbound HTTPS works
|
||||||
|
curl -ks https://manager.example.com/api/v1/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment Method 1: Auto-Enrollment (Recommended)
|
||||||
|
|
||||||
|
The simplest deployment. Just install the package, configure the manager URL, and start the service. The daemon handles the rest.
|
||||||
|
|
||||||
|
#### Step 1: Install Package
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian/Ubuntu
|
||||||
|
dpkg -i linux-patch-api_1.2.0-1_amd64.deb
|
||||||
|
|
||||||
|
# RHEL/CentOS/Fedora
|
||||||
|
rpm -ivh linux-patch-api-1.2.0-1.x86_64.rpm
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: Configure Enrollment URL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Edit the config to add manager URL
|
||||||
|
cat >> /etc/linux_patch_api/config.yaml <<EOF
|
||||||
|
|
||||||
|
enrollment:
|
||||||
|
manager_url: "https://linux-patch-manager-dev.moon-dragon.us"
|
||||||
|
polling_interval_seconds: 60
|
||||||
|
max_poll_attempts: 1440
|
||||||
|
cert_renewal_threshold_days: 7
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: Start Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable and start
|
||||||
|
systemctl enable linux-patch-api
|
||||||
|
systemctl start linux-patch-api
|
||||||
|
|
||||||
|
# Watch auto-enrollment progress
|
||||||
|
journalctl -u linux-patch-api -f
|
||||||
|
```
|
||||||
|
|
||||||
|
The daemon will:
|
||||||
|
1. Validate certificates → find them missing
|
||||||
|
2. Read `enrollment.manager_url` → begin auto-enrollment
|
||||||
|
3. Register with manager, poll for approval
|
||||||
|
4. Provision certificates after admin approval
|
||||||
|
5. Continue to normal mTLS server startup
|
||||||
|
|
||||||
|
**No manual `--enroll` command needed.** The service self-heals on restart if certificates are missing or invalid.
|
||||||
|
|
||||||
|
#### Step 4: Admin Approval (Manager Side)
|
||||||
|
|
||||||
|
On the `linux_patch_manager` dashboard:
|
||||||
|
1. Navigate to Pending Enrollments
|
||||||
|
2. Review host details (machine-id, FQDN, IP, OS)
|
||||||
|
3. Approve the enrollment request
|
||||||
|
4. Manager provisions PKI bundle and signals approval
|
||||||
|
|
||||||
|
#### Step 5: Verify Successful Enrollment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check service is running
|
||||||
|
systemctl status linux-patch-api
|
||||||
|
|
||||||
|
# Verify certificates exist
|
||||||
|
ls -la /etc/linux_patch_api/certs/
|
||||||
|
|
||||||
|
# Test mTLS connection
|
||||||
|
curl --cacert /etc/linux_patch_api/certs/ca.pem \
|
||||||
|
--cert /path/to/client.pem \
|
||||||
|
--key /path/to/client.key.pem \
|
||||||
|
https://localhost:12443/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment Method 2: Manual Enrollment
|
||||||
|
|
||||||
|
For environments where auto-enrollment is not desired, or for initial setup before the service is enabled.
|
||||||
|
|
||||||
|
#### Step 1: Install Package
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian/Ubuntu
|
||||||
|
dpkg -i linux-patch-api_1.2.0-1_amd64.deb
|
||||||
|
|
||||||
|
# RHEL/CentOS/Fedora
|
||||||
|
rpm -ivh linux-patch-api-1.2.0-1.x86_64.rpm
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: Run Enrollment Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic enrollment with manager URL
|
||||||
|
sudo linux-patch-api --enroll https://linux-patch-manager-dev.moon-dragon.us
|
||||||
|
|
||||||
|
# With verbose logging for troubleshooting
|
||||||
|
sudo linux-patch-api --enroll https://linux-patch-manager-dev.moon-dragon.us --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** The `--enroll` command provisions certificates and **exits**. It does NOT start the server. This prevents port conflicts with the systemd service.
|
||||||
|
|
||||||
|
The enrollment process will:
|
||||||
|
- Extract machine identity from `/etc/machine-id` and system properties
|
||||||
|
- Submit registration request to manager
|
||||||
|
- Enter polling loop (logs progress every 60 seconds)
|
||||||
|
- Await admin approval on the manager side
|
||||||
|
- Download and install certificates automatically
|
||||||
|
- Update IP whitelist with manager address
|
||||||
|
- Print: "Enrollment complete. Start service: systemctl start linux-patch-api"
|
||||||
|
- Exit with code 0
|
||||||
|
|
||||||
|
#### Step 3: Start Service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl enable linux-patch-api
|
||||||
|
systemctl start linux-patch-api
|
||||||
|
systemctl status linux-patch-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Certificate Renewal
|
||||||
|
|
||||||
|
Certificates can be renewed manually or automatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Manual renewal
|
||||||
|
sudo linux-patch-api --renew-certs
|
||||||
|
|
||||||
|
# Auto-renewal: certs expiring within cert_renewal_threshold_days trigger re-enrollment on startup
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
Enrollment behavior can be tuned via the `enrollment` section in `/etc/linux_patch_api/config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Enrollment Configuration
|
||||||
|
enrollment:
|
||||||
|
manager_url: "https://linux-patch-manager-dev.moon-dragon.us"
|
||||||
|
polling_interval_seconds: 60 # Time between approval polls (default: 60)
|
||||||
|
max_poll_attempts: 1440 # Maximum poll attempts (default: 1440 = 24 hours)
|
||||||
|
polling_token: "" # Auto-populated during enrollment (do not edit)
|
||||||
|
cert_renewal_threshold_days: 7 # Days before expiry to trigger re-enrollment
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameter Reference:**
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
|-----------|---------|-------------|
|
||||||
|
| `manager_url` | (none) | Manager URL for auto-enrollment. Required for auto-enrollment on startup. |
|
||||||
|
| `polling_interval_seconds` | 60 | Seconds between approval status polls. Minimum: 10 |
|
||||||
|
| `max_poll_attempts` | 1440 | Maximum polling attempts before timeout. Effective timeout = interval × attempts |
|
||||||
|
| `polling_token` | (empty) | Auto-populated during enrollment for resume after restart. Do not edit manually. |
|
||||||
|
| `cert_renewal_threshold_days` | 7 | Days before cert expiry to trigger automatic re-enrollment |
|
||||||
|
|
||||||
|
**Effective Timeout Calculation:**
|
||||||
|
- Default: 60s × 1440 = 86,400 seconds (24 hours)
|
||||||
|
- Custom example: 30s × 720 = 21,600 seconds (6 hours)
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Resolution |
|
||||||
|
|---------|-------|------------|
|
||||||
|
| `Enrollment failed: connection refused` | Manager not reachable | Verify manager URL, check firewall rules |
|
||||||
|
| `Enrollment failed: DNS resolution error` | Hostname cannot resolve | Check `/etc/resolv.conf`, verify DNS |
|
||||||
|
| `HTTP 403 - Enrollment denied` | Admin rejected request | Contact manager admin to approve enrollment |
|
||||||
|
| `HTTP 404 - Token not found` | Token expired/purged | Re-run enrollment command with `--enroll` flag |
|
||||||
|
| `Polling timeout after N attempts` | Max attempts exceeded | Increase `max_poll_attempts` in config, re-enroll |
|
||||||
|
| `Rate limited: 429 Too Many Requests` | Polling too frequently | Ensure `polling_interval_seconds >= 10` |
|
||||||
|
| `Permission denied writing certificates` | Insufficient privileges | Run with `sudo` or as root user |
|
||||||
|
| `Whitelist update failed` | File permission issue | Verify `/etc/linux_patch_api/` is writable by service user |
|
||||||
|
|
||||||
|
**Diagnostic Commands:**
|
||||||
|
```bash
|
||||||
|
# Check enrollment logs
|
||||||
|
journalctl -u linux-patch-api --since "1 hour ago"
|
||||||
|
|
||||||
|
# Test manager connectivity
|
||||||
|
curl -v https://manager.example.com/api/v1/enroll
|
||||||
|
|
||||||
|
# Verify DNS resolution
|
||||||
|
dig manager.example.com
|
||||||
|
nslookup manager.example.com
|
||||||
|
|
||||||
|
# Check certificate paths are writable
|
||||||
|
ls -la /etc/linux_patch_api/certs/
|
||||||
|
sudo touch /etc/linux_patch_api/certs/test && sudo rm /etc/linux_patch_api/certs/test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Post-Enrollment Verification
|
||||||
|
|
||||||
|
After successful enrollment, verify the following:
|
||||||
|
|
||||||
|
1. **Certificate Files Exist:**
|
||||||
|
```bash
|
||||||
|
ls -la /etc/linux_patch_api/certs/
|
||||||
|
# Expected: ca.pem (644), server.pem (644), server.key (600)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Certificate Validity:**
|
||||||
|
```bash
|
||||||
|
openssl x509 -in /etc/linux_patch_api/certs/server.pem -text -noout | grep -A2 "Validity"
|
||||||
|
openssl x509 -in /etc/linux_patch_api/certs/ca.pem -text -noout | grep -A2 "Validity"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Whitelist Contains Manager IP:**
|
||||||
|
```bash
|
||||||
|
cat /etc/linux_patch_api/whitelist.yaml
|
||||||
|
# Should contain manager IP address in entries list
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **mTLS Connection Test:**
|
||||||
|
```bash
|
||||||
|
curl --cacert /etc/linux_patch_api/certs/ca.pem \
|
||||||
|
--cert /path/to/client.pem \
|
||||||
|
--key /path/to/client.key.pem \
|
||||||
|
https://localhost:12443/health
|
||||||
|
# Expected: {"status": "ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Service Status:**
|
||||||
|
```bash
|
||||||
|
systemctl status linux-patch-api
|
||||||
|
# Expected: active (running)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rollback and Re-Enrollment
|
||||||
|
|
||||||
|
#### Removing Enrolled Certificates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop the service
|
||||||
|
sudo systemctl stop linux-patch-api
|
||||||
|
|
||||||
|
# Remove provisioned certificates
|
||||||
|
sudo rm -f /etc/linux_patch_api/certs/ca.pem
|
||||||
|
sudo rm -f /etc/linux_patch_api/certs/server.pem
|
||||||
|
sudo rm -f /etc/linux_patch_api/certs/server.key
|
||||||
|
|
||||||
|
# Revert whitelist (remove manager IP entry)
|
||||||
|
sudo vi /etc/linux_patch_api/whitelist.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Re-Enrolling a Host
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run enrollment again with same or different manager
|
||||||
|
sudo linux-patch-api --enroll https://manager.example.com
|
||||||
|
|
||||||
|
# Or enroll with a different manager
|
||||||
|
sudo linux-patch-api --enroll https://new-manager.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- Re-enrollment overwrites existing certificates in the configured paths
|
||||||
|
- The previous polling token is discarded; a new registration request is submitted
|
||||||
|
- If re-enrolling with the same manager, ensure the old enrollment was purged or approved
|
||||||
|
|
||||||
|
### Enrollment vs Manual Certificate Deployment
|
||||||
|
|
||||||
|
| Aspect | Self-Enrollment | Manual PKI |
|
||||||
|
|--------|----------------|------------|
|
||||||
|
| Certificate distribution | Automatic from manager | Manual SCP/copy |
|
||||||
|
| Whitelist management | Auto-populated with manager IP | Manual configuration |
|
||||||
|
| Admin approval required | Yes (on manager side) | N/A |
|
||||||
|
| Network dependency | Requires outbound HTTPS to manager | None after cert distribution |
|
||||||
|
| Best for | Large-scale deployments, automation | Air-gapped environments, single hosts |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### Configuration File Locations
|
### Configuration File Locations
|
||||||
|
|||||||
@ -20,7 +20,7 @@ This report documents the implementation of 6 security hardening fixes deferred
|
|||||||
| VULN-003 | LOW | Input Validation | ✅ RESOLVED | src/api/handlers/packages.rs |
|
| VULN-003 | LOW | Input Validation | ✅ RESOLVED | src/api/handlers/packages.rs |
|
||||||
| VULN-004 | MEDIUM | Header Security | ✅ RESOLVED | src/main.rs |
|
| VULN-004 | MEDIUM | Header Security | ✅ RESOLVED | src/main.rs |
|
||||||
| VULN-005 | LOW | HTTP Protocol | ✅ RESOLVED | src/api/routes.rs |
|
| VULN-005 | LOW | HTTP Protocol | ✅ RESOLVED | src/api/routes.rs |
|
||||||
| VULN-006 | LOW | Header Security | ✅ RESOLVED | src/auth/mtls.rs |
|
| VULN-006 | LOW | Header Security | ✅ RESOLVED | src/auth/security_headers.rs |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -176,20 +176,19 @@ web::scope("/api/v1")
|
|||||||
**Finding:** Duplicate Content-Type headers were accepted.
|
**Finding:** Duplicate Content-Type headers were accepted.
|
||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
- Added `has_duplicate_critical_headers()` function to check for duplicate headers
|
- `has_duplicate_critical_headers()` function checks for duplicate headers on every request
|
||||||
- Monitors critical headers: `content-type`, `authorization`, `host`
|
- Monitors critical headers: `content-type`, `authorization`, `host`
|
||||||
- Integrated into mTLS middleware `call()` method
|
- Implemented as `SecurityHeadersMiddleware` — a dedicated Actix-web middleware
|
||||||
- Rejects requests with duplicate critical headers before further processing
|
- Wired into the middleware pipeline in `main.rs` between WhitelistMiddleware and Logger
|
||||||
|
- Rejects requests with duplicate critical headers with HTTP 400 Bad Request
|
||||||
|
|
||||||
**Code Location:** `src/auth/mtls.rs` (lines 26-49, 203-212)
|
**Code Location:** `src/auth/security_headers.rs`
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn has_duplicate_critical_headers(req: &ServiceRequest) -> bool {
|
pub fn has_duplicate_critical_headers(headers: &HeaderMap) -> bool {
|
||||||
let critical_headers = ["content-type", "authorization", "host"];
|
for header_name in CRITICAL_HEADERS.iter() {
|
||||||
|
|
||||||
for header_name in critical_headers.iter() {
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for (name, _) in req.headers().iter() {
|
for (name, _value) in headers.iter() {
|
||||||
if name.as_str().eq_ignore_ascii_case(header_name) {
|
if name.as_str().eq_ignore_ascii_case(header_name) {
|
||||||
count += 1;
|
count += 1;
|
||||||
if count > 1 {
|
if count > 1 {
|
||||||
@ -202,7 +201,29 @@ fn has_duplicate_critical_headers(req: &ServiceRequest) -> bool {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:** HTTP 400 Bad Request with message "Duplicate critical headers not allowed"
|
**Response:** HTTP 400 Bad Request with error message "Duplicate critical headers not allowed"
|
||||||
|
|
||||||
|
**Architecture Note:** The duplicate-header check was originally in `MtlsMiddleware`, which was dead code (never wired into the pipeline). It has been extracted into `SecurityHeadersMiddleware`, which IS wired into the pipeline and runs on every request. Client certificate authentication is handled at the TLS handshake level by rustls via `CrlAwareVerifier` — no application-layer certificate middleware is needed. See `src/auth/mtls.rs` for the ADR documenting this decision.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Decision Record: rustls as Authoritative Client-Auth Gate
|
||||||
|
|
||||||
|
**Decision:** Client certificate authentication is enforced at the TLS handshake level by rustls via `CrlAwareVerifier`, NOT by application-layer middleware.
|
||||||
|
|
||||||
|
**Context:** The original `MtlsMiddleware` was never wired into the Actix-web pipeline. It contained both a duplicate-header check (VULN-006) and a `validate_client_certificate()` stub that returned `Ok(())` unconditionally. Meanwhile, the actual client certificate verification was always performed by rustls at the TLS handshake level through `CrlAwareVerifier`, which wraps `WebPkiClientVerifier`.
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- rustls provides battle-tested X.509 verification at the TLS handshake level
|
||||||
|
- Enforcing auth at the TLS layer eliminates bypass vulnerabilities (middleware ordering bugs, route-specific skips)
|
||||||
|
- CRL revocation checking is integrated into the same handshake path via `CrlAwareVerifier`
|
||||||
|
- Application-layer certificate validation is redundant when the TLS layer already rejects untrusted connections
|
||||||
|
|
||||||
|
**Consequences:**
|
||||||
|
- `MtlsMiddleware` (Transform/Service) and `validate_client_certificate()` have been removed as dead code
|
||||||
|
- `build_rustls_config()` is now a free function (no longer a method on `MtlsMiddleware`)
|
||||||
|
- `SecurityHeadersMiddleware` handles VULN-006 (duplicate critical header rejection) as a dedicated, wired middleware
|
||||||
|
- `ClientCertInfo` struct is preserved for potential future use in extracting certificate details from TLS sessions
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
190
LICENSE
Normal file
190
LICENSE
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by the Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright 2025-2026 Draco Lunaris
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
216
README.md
216
README.md
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Version:** 1.0.0
|
**Version:** 1.0.0
|
||||||
**Status:** Production Ready
|
**Status:** Production Ready
|
||||||
**License:** Internal Use Only
|
**License:** [Apache 2.0](LICENSE)
|
||||||
|
|
||||||
Secure REST API for remote package and patch management on Linux systems.
|
Secure REST API for remote package and patch management on Linux systems.
|
||||||
|
|
||||||
@ -13,6 +13,7 @@ Secure REST API for remote package and patch management on Linux systems.
|
|||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Quick Start](#quick-start)
|
- [Quick Start](#quick-start)
|
||||||
|
- [Usage Examples](#usage-examples)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [API Usage](#api-usage)
|
- [API Usage](#api-usage)
|
||||||
@ -65,6 +66,7 @@ Linux Patch API provides a secure, production-ready interface for managing softw
|
|||||||
### Security Features
|
### Security Features
|
||||||
- mTLS certificate authentication (TLS 1.3 only)
|
- mTLS certificate authentication (TLS 1.3 only)
|
||||||
- IP whitelist enforcement (deny by default)
|
- IP whitelist enforcement (deny by default)
|
||||||
|
- Automated self-enrollment with linux_patch_manager (no manual PKI distribution)
|
||||||
- Comprehensive audit logging (systemd journal)
|
- Comprehensive audit logging (systemd journal)
|
||||||
- Systemd hardening and process isolation
|
- Systemd hardening and process isolation
|
||||||
- File permission enforcement
|
- File permission enforcement
|
||||||
@ -137,10 +139,59 @@ curl --cacert ca.pem \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Standard Startup (Existing Certificates)
|
||||||
|
|
||||||
|
When certificates are already provisioned, start with the configuration path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo linux-patch-api --config /etc/linux_patch_api/config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via systemd (recommended for production):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl enable linux-patch-api
|
||||||
|
systemctl start linux-patch-api
|
||||||
|
```
|
||||||
|
|
||||||
|
### Self-Enrollment with Manager
|
||||||
|
|
||||||
|
Bootstrap a new host by automatically requesting certificates from the manager:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo linux-patch-api --enroll https://manager.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
The enrollment workflow:
|
||||||
|
1. Extracts machine identity (`/etc/machine-id`, FQDN, OS details)
|
||||||
|
2. Registers with manager (`POST /api/v1/enroll`)
|
||||||
|
3. Polls for admin approval (default: every 60 seconds, up to 24 hours)
|
||||||
|
4. Downloads PKI bundle on approval
|
||||||
|
5. Writes certificates and updates whitelist automatically
|
||||||
|
6. Starts mTLS server without requiring a restart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enrollment with verbose logging
|
||||||
|
sudo linux-patch-api --enroll https://manager.example.com --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
For detailed enrollment procedures, see [DEPLOYMENT_GUIDE.md - Self-Enrollment Deployment](./DEPLOYMENT_GUIDE.md#self-enrollment-deployment).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Package Installation
|
### Package Installation
|
||||||
|
|
||||||
|
All platform packages produce identical installation results:
|
||||||
|
- Creates `/etc/linux_patch_api/`, `/etc/linux_patch_api/certs/`, `/var/lib/linux_patch_api/`, `/var/log/linux_patch_api/`
|
||||||
|
- Copies example configs to live configs if not already present
|
||||||
|
- Enables the service (does not start automatically)
|
||||||
|
- Sets correct permissions (750 on config dirs, 755 on data/log dirs)
|
||||||
|
- Ownership: root:root (service runs as root)
|
||||||
|
|
||||||
#### Debian/Ubuntu (.deb)
|
#### Debian/Ubuntu (.deb)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -153,52 +204,173 @@ apt-get install -f -y
|
|||||||
# Verify installation
|
# Verify installation
|
||||||
systemctl status linux-patch-api
|
systemctl status linux-patch-api
|
||||||
linux-patch-api --version
|
linux-patch-api --version
|
||||||
|
|
||||||
|
# Check installed files
|
||||||
|
dpkg -L linux-patch-api
|
||||||
|
|
||||||
|
# Remove package (keeping configs)
|
||||||
|
dpkg -r linux-patch-api
|
||||||
|
|
||||||
|
# Purge package (removing all configs)
|
||||||
|
dpkg -P linux-patch-api
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Prerequisites:** `systemd`, `libsystemd0`
|
||||||
|
|
||||||
|
**Post-install:** The package automatically copies example configs, enables the service, and prints next steps. Configure `/etc/linux_patch_api/config.yaml` and place TLS certificates in `/etc/linux_patch_api/certs/` before starting.
|
||||||
|
|
||||||
#### RHEL/CentOS/Fedora (.rpm)
|
#### RHEL/CentOS/Fedora (.rpm)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install the package
|
# Install the package (recommended - resolves dependencies automatically)
|
||||||
rpm -ivh linux-patch-api-1.0.0-1.x86_64.rpm
|
dnf install -y ./linux-patch-api-1.0.0-1.el9.x86_64.rpm
|
||||||
|
|
||||||
|
# Or with yum
|
||||||
|
yum install -y ./linux-patch-api-1.0.0-1.el9.x86_64.rpm
|
||||||
|
|
||||||
|
# Or with rpm (does NOT resolve dependencies)
|
||||||
|
rpm -ivh linux-patch-api-1.0.0-1.el9.x86_64.rpm
|
||||||
|
|
||||||
# Verify installation
|
# Verify installation
|
||||||
systemctl status linux-patch-api
|
systemctl status linux-patch-api
|
||||||
linux-patch-api --version
|
linux-patch-api --version
|
||||||
|
|
||||||
|
# Check installed files
|
||||||
|
rpm -ql linux-patch-api
|
||||||
|
|
||||||
|
# Remove package
|
||||||
|
rpm -e linux-patch-api
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Prerequisites (auto-resolved with dnf/yum):** `systemd`, `libsystemd`, `openssl-libs`, `ca-certificates`
|
||||||
|
|
||||||
|
**Post-install:** The package automatically creates directories, copies example configs, enables the service, and prints next steps. Configure `/etc/linux_patch_api/config.yaml` and place TLS certificates in `/etc/linux_patch_api/certs/` before starting.
|
||||||
|
|
||||||
|
**Note:** Use `dnf install` or `yum install` instead of `rpm -ivh` to automatically resolve dependencies. The `rpm -ivh` command will fail if required packages are not already installed.
|
||||||
|
|
||||||
|
#### Arch Linux (.pkg.tar.zst)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install the package
|
||||||
|
sudo pacman -U ./linux-patch-api-1.0.0-1-x86_64.pkg.tar.zst
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
systemctl status linux-patch-api
|
||||||
|
linux-patch-api --version
|
||||||
|
|
||||||
|
# Check installed files
|
||||||
|
pacman -Ql linux-patch-api
|
||||||
|
|
||||||
|
# Remove package
|
||||||
|
sudo pacman -R linux-patch-api
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prerequisites:** `systemd` (included by default on Arch)
|
||||||
|
|
||||||
|
**Post-install:** The package automatically creates directories, copies example configs, enables the service, and prints next steps. Configure `/etc/linux_patch_api/config.yaml` and place TLS certificates in `/etc/linux_patch_api/certs/` before starting.
|
||||||
|
|
||||||
|
**Note:** Arch uses systemd by default. The install hook runs `systemctl enable` but does not start the service. You must configure before starting.
|
||||||
|
|
||||||
|
#### Alpine Linux (.apk)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install the package
|
||||||
|
sudo apk add --allow-unstable ./linux-patch-api-1.0.0-r0.apk
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
rc-service linux-patch-api status
|
||||||
|
linux-patch-api --version
|
||||||
|
|
||||||
|
# Check installed files
|
||||||
|
apk info -L linux-patch-api
|
||||||
|
|
||||||
|
# Remove package
|
||||||
|
sudo apk del linux-patch-api
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prerequisites:** `openrc` (included by default on Alpine)
|
||||||
|
|
||||||
|
**Post-install:** The package automatically creates directories, copies example configs, adds the service to the default runlevel, and prints next steps. Configure `/etc/linux_patch_api/config.yaml` and place TLS certificates in `/etc/linux_patch_api/certs/` before starting.
|
||||||
|
|
||||||
|
**Important differences from systemd-based systems:**
|
||||||
|
- Alpine uses **OpenRC** instead of systemd. Use `rc-service` commands instead of `systemctl`
|
||||||
|
- Start service: `rc-service linux-patch-api start`
|
||||||
|
- Stop service: `rc-service linux-patch-api stop`
|
||||||
|
- Check status: `rc-service linux-patch-api status`
|
||||||
|
- The service is added to the `default` runlevel automatically on install
|
||||||
|
- Service init script: `/etc/init.d/linux-patch-api`
|
||||||
|
|
||||||
### Manual Installation
|
### Manual Installation
|
||||||
|
|
||||||
For systems without package manager support:
|
For systems without package manager support:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run interactive installer (requires root)
|
# Run interactive installer (requires root)
|
||||||
./install.sh
|
sudo ./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The installer will:
|
The installer will:
|
||||||
- Detect operating system
|
- Detect operating system
|
||||||
- Create system user and group
|
- Create directory structure (`/etc/linux_patch_api/`, `/var/lib/linux_patch_api/`, `/var/log/linux_patch_api/`)
|
||||||
- Set up directory structure
|
- Install binary to `/usr/bin/linux-patch-api`
|
||||||
- Install binary and configuration files
|
- Install example configs
|
||||||
- Configure systemd service
|
- Configure systemd service
|
||||||
|
- Set correct permissions
|
||||||
|
|
||||||
### Building from Source
|
### Building from Source
|
||||||
|
|
||||||
|
#### Prerequisites (all platforms)
|
||||||
|
|
||||||
|
- Rust toolchain (stable channel, 1.75+)
|
||||||
|
- OpenSSL development headers
|
||||||
|
- systemd development headers
|
||||||
|
- C compiler (gcc)
|
||||||
|
|
||||||
|
#### Build Debian Package (.deb)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone repository
|
# On Debian/Ubuntu
|
||||||
git clone https://gitea.internal/linux-patch-api.git
|
apt-get install -y build-essential debhelper pkg-config libsystemd-dev libssl-dev cargo rustc
|
||||||
cd linux-patch-api
|
cargo build --release
|
||||||
|
sudo dpkg-buildpackage -us -uc -b
|
||||||
# Build release binary
|
|
||||||
cargo build --release --target x86_64-unknown-linux-gnu
|
|
||||||
|
|
||||||
# Build Debian package
|
|
||||||
dpkg-buildpackage -us -uc -b
|
|
||||||
|
|
||||||
# Or build RPM package
|
|
||||||
rpmbuild -ba linux-patch-api.spec
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Build RPM Package (.rpm)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On Fedora/RHEL/CentOS
|
||||||
|
dnf install -y rpm-build cargo rust gcc openssl-devel systemd-devel pkgconfig
|
||||||
|
cargo build --release --target x86_64-unknown-linux-gnu
|
||||||
|
chmod +x build-rpm.sh
|
||||||
|
./build-rpm.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** The RPM spec includes `BuildRequires` for native RPM build environments. When building in CI containers (where deps are pre-installed via apt-get), these are informational only.
|
||||||
|
|
||||||
|
#### Build Arch Package (.pkg.tar.zst)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On Arch Linux/Manjaro
|
||||||
|
pacman -Syu --noconfirm rust cargo systemd git base-devel gcc
|
||||||
|
cargo build --release
|
||||||
|
chmod +x build-arch.sh
|
||||||
|
SKIP_CARGO_BUILD=1 ./build-arch.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** The build script creates a `builduser` for `makepkg` when running as root (typical in CI). The `.install` hook handles directory creation, config copying, and service enablement.
|
||||||
|
|
||||||
|
#### Build Alpine Package (.apk)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On Alpine Linux 3.18+
|
||||||
|
apk add --no-cache alpine-sdk rust cargo openssl openssl-dev elogind-dev musl-dev abuild gcc
|
||||||
|
cargo build --release --target x86_64-unknown-linux-musl
|
||||||
|
chmod +x build-alpine.sh
|
||||||
|
SKIP_CARGO_BUILD=1 ./build-alpine.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Alpine requires the `x86_64-unknown-linux-musl` target for static linking. The build script handles `abuild` key generation and runs as a `builduser` when executed as root.
|
||||||
|
|
||||||
See [BUILD_PACKAGES.md](./BUILD_PACKAGES.md) for detailed build instructions.
|
See [BUILD_PACKAGES.md](./BUILD_PACKAGES.md) for detailed build instructions.
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -223,7 +395,7 @@ tls:
|
|||||||
ca_cert: "/etc/linux_patch_api/certs/ca.pem"
|
ca_cert: "/etc/linux_patch_api/certs/ca.pem"
|
||||||
server_cert: "/etc/linux_patch_api/certs/server.pem"
|
server_cert: "/etc/linux_patch_api/certs/server.pem"
|
||||||
server_key: "/etc/linux_patch_api/certs/server.key"
|
server_key: "/etc/linux_patch_api/certs/server.key"
|
||||||
min_tls_version: "1.3"
|
# TLS 1.3 is the only supported version (hardcoded, not configurable)
|
||||||
|
|
||||||
# Job Configuration
|
# Job Configuration
|
||||||
jobs:
|
jobs:
|
||||||
@ -519,7 +691,9 @@ linux-patch-api --check-config
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Internal Use Only - Not for external distribution
|
This project is licensed under the [Apache License 2.0](LICENSE).
|
||||||
|
|
||||||
|
Copyright 2025-2026 Draco Lunaris
|
||||||
|
|
||||||
**Version:** 1.0.0
|
**Version:** 1.0.0
|
||||||
**Release Date:** 2026-07-17
|
**Release Date:** 2026-07-17
|
||||||
|
|||||||
@ -50,6 +50,16 @@
|
|||||||
- Log configuration changes (whitelist updates, cert renewals)
|
- Log configuration changes (whitelist updates, cert renewals)
|
||||||
- Log system changes made by the API
|
- Log system changes made by the API
|
||||||
|
|
||||||
|
### FR-007: Package Cache Refresh
|
||||||
|
|
||||||
|
- The agent MUST refresh the local package index before every patch_apply operation
|
||||||
|
- The agent MUST refresh the local package index when the health check detects stale cache (>4 hours)
|
||||||
|
- The agent SHOULD automatically retry patch_apply once after cache refresh on 404/fetch errors
|
||||||
|
- The agent SHOULD track and report last_cache_update timestamp in health check responses
|
||||||
|
- Cache state persists to /var/lib/linux_patch_api/state/cache.json across service restarts
|
||||||
|
- Cache refresh before apply is mandatory and not configurable
|
||||||
|
- Cache refresh timeout is 120 seconds
|
||||||
|
|
||||||
---
|
---
|
||||||
## Non-Functional Requirements
|
## Non-Functional Requirements
|
||||||
|
|
||||||
|
|||||||
37
ROADMAP.md
37
ROADMAP.md
@ -151,6 +151,32 @@
|
|||||||
**See:** [PERFORMANCE_BENCHMARK.md](./PERFORMANCE_BENCHMARK.md), [PROFILING_REPORT.md](./PROFILING_REPORT.md), [OPTIMIZATION_RECOMMENDATIONS.md](./OPTIMIZATION_RECOMMENDATIONS.md)
|
**See:** [PERFORMANCE_BENCHMARK.md](./PERFORMANCE_BENCHMARK.md), [PROFILING_REPORT.md](./PROFILING_REPORT.md), [OPTIMIZATION_RECOMMENDATIONS.md](./OPTIMIZATION_RECOMMENDATIONS.md)
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Phase 5: Enrollment & Self-Registration
|
||||||
|
**Duration:** 3 weeks
|
||||||
|
**Target Date:** 2026-07-17 to 2026-08-07
|
||||||
|
**Actual Completion:** 2026-08-07
|
||||||
|
**Status:** ✅ Complete (Enrollment Feature Released)
|
||||||
|
|
||||||
|
- [x] Self-enrollment workflow implementation ✅ **COMPLETE**
|
||||||
|
- [x] CLI flag: `--enroll <MANAGER_URL>` for enrollment mode
|
||||||
|
- [x] Three-phase enrollment: Registration → Polling (24h timeout) → PKI Provisioning
|
||||||
|
- [x] Automatic certificate provisioning to configured mTLS paths
|
||||||
|
- [x] Automatic manager IP whitelist append after successful enrollment
|
||||||
|
- [x] Configurable polling interval (default 60s) and max attempts (default 1440/24h)
|
||||||
|
- [x] Signal handling for graceful shutdown during enrollment
|
||||||
|
- [x] Enrollment configuration section in config.yaml (`enrollment.*`) ✅ **COMPLETE**
|
||||||
|
- [x] Identity extraction module (machine-id, FQDN, IP addresses, OS details) ✅ **COMPLETE**
|
||||||
|
- [x] PKI bundle validation with PEM format checking ✅ **COMPLETE**
|
||||||
|
- [x] Atomic certificate file writing with secure permissions (key=0600, certs=0644) ✅ **COMPLETE**
|
||||||
|
- [x] Whitelist auto-append with file locking and duplicate detection ✅ **COMPLETE**
|
||||||
|
- [x] Integration tests for enrollment workflow ✅ **COMPLETE**
|
||||||
|
- [x] E2E enrollment test suite ✅ **COMPLETE**
|
||||||
|
|
||||||
|
**Future Improvements (Medium Priority - from Security Review):**
|
||||||
|
- M-001: PKI certificate rollback mechanism (deferred to Phase 6)
|
||||||
|
- M-002: Kernel version redaction in identity payload (deferred to Phase 6)
|
||||||
|
---
|
||||||
|
|
||||||
## Milestones
|
## Milestones
|
||||||
|
|
||||||
| Milestone | Description | Target Date | Status |
|
| Milestone | Description | Target Date | Status |
|
||||||
@ -164,6 +190,7 @@
|
|||||||
| M6 | Security testing complete (Beta) | 2026-06-28 | ✅ Complete |
|
| M6 | Security testing complete (Beta) | 2026-06-28 | ✅ Complete |
|
||||||
| M7 | Performance benchmarking complete | 2026-04-09 | ✅ Complete |
|
| M7 | Performance benchmarking complete | 2026-04-09 | ✅ Complete |
|
||||||
| M8 | Production release (v1.0.0) | 2026-07-17 | ✅ Complete |
|
| M8 | Production release (v1.0.0) | 2026-07-17 | ✅ Complete |
|
||||||
|
| M9 | Self-enrollment feature complete | 2026-08-07 | ✅ Complete |
|
||||||
---
|
---
|
||||||
|
|
||||||
## Risk Register
|
## Risk Register
|
||||||
@ -241,6 +268,16 @@
|
|||||||
- [x] UAT sign-off received ✅
|
- [x] UAT sign-off received ✅
|
||||||
- [x] v1.0.0 released ✅
|
- [x] v1.0.0 released ✅
|
||||||
|
|
||||||
|
### Phase 5 Success
|
||||||
|
- [x] Self-enrollment workflow functional ✅
|
||||||
|
- [x] CLI enrollment flag (`--enroll`) operational ✅
|
||||||
|
- [x] Three-phase enrollment (Registration → Polling → PKI) working ✅
|
||||||
|
- [x] Automatic certificate provisioning to mTLS paths ✅
|
||||||
|
- [x] Whitelist auto-append with duplicate detection ✅
|
||||||
|
- [x] Enrollment integration tests passing ✅
|
||||||
|
- [x] E2E enrollment test suite passing ✅
|
||||||
|
- [x] Config example updated with enrollment section ✅
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Following kiro spec-driven development standards*
|
*Following kiro spec-driven development standards*
|
||||||
|
|||||||
356
SECURITY.md
356
SECURITY.md
@ -1,346 +1,46 @@
|
|||||||
# Linux_Patch_API - Security Specification Document
|
# Security Policy
|
||||||
|
|
||||||
## Security Overview
|
## Supported Versions
|
||||||
|
|
||||||
**Philosophy:** Defense in depth with zero-trust principles for internal network.
|
Only the **latest release** is currently supported with security updates.
|
||||||
|
|
||||||
**Approach:**
|
| Version | Supported |
|
||||||
- mTLS certificate-based authentication (required for all connections)
|
|---------|----------|
|
||||||
- IP whitelist enforcement (deny by default, allow only listed)
|
| Latest | ✅ |
|
||||||
- Comprehensive audit logging for all operations
|
| Older | ❌ |
|
||||||
- Systemd hardening and process isolation
|
|
||||||
- Minimal attack surface (internal network only)
|
|
||||||
|
|
||||||
---
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
## Threat Model
|
**Do not report security vulnerabilities through public GitHub Issues.**
|
||||||
|
|
||||||
### Threat Actor Profile
|
Instead, use GitHub's private vulnerability reporting:
|
||||||
|
|
||||||
| Attribute | Description |
|
👉 [Report a vulnerability for Linux-Patch-Api](https://github.com/Draco-Lunaris/Linux-Patch-Api/security/advisories/new)
|
||||||
|-----------|-------------|
|
|
||||||
| **Origin** | Internal network only |
|
|
||||||
| **Skill Level** | Moderate to High |
|
|
||||||
| **Resources** | Limited (not nation-state) |
|
|
||||||
| **Motivation** | Unauthorized system access, privilege escalation |
|
|
||||||
| **Access** | Must bypass mTLS + IP whitelist |
|
|
||||||
|
|
||||||
### STRIDE Threat Analysis
|
This allows us to coordinate a fix before public disclosure.
|
||||||
|
|
||||||
| Threat Category | Potential Threat | Mitigation | Status |
|
### Response Timeline
|
||||||
|-----------------|------------------|------------|--------|
|
|
||||||
| **Spoofing** | Attacker impersonates valid client | mTLS certificate validation, unique certs per client | ✅ Mitigated |
|
|
||||||
| **Spoofing** | Attacker uses expired/revoked cert | Certificate expiry validation (1-year max) | ✅ Mitigated |
|
|
||||||
| **Tampering** | API requests modified in transit | TLS 1.3 encryption | ✅ Mitigated |
|
|
||||||
| **Tampering** | Config files modified unauthorized | File permissions (600/644), config validation before reload | ✅ Mitigated |
|
|
||||||
| **Repudiation** | Client denies making request | Audit logging with request_id, client cert ID | ✅ Mitigated |
|
|
||||||
| **Repudiation** | Server denies response | Comprehensive audit trail (systemd journal) | ✅ Mitigated |
|
|
||||||
| **Information Disclosure** | Package/data info leaked to unauthorized | Silent drop for non-mTLS, IP whitelist | ✅ Mitigated |
|
|
||||||
| **Information Disclosure** | Error messages leak system info | Detailed errors only for authenticated clients | ✅ Mitigated |
|
|
||||||
| **Denial of Service** | Resource exhaustion via many requests | Internal network only, IP whitelist limits exposure | ⚠️ Partial |
|
|
||||||
| **Denial of Service** | Job queue flooding | Configurable concurrent job limit (default: 5) | ✅ Mitigated |
|
|
||||||
| **Denial of Service** | Long-running job starvation | 30-minute job timeout enforcement | ✅ Mitigated |
|
|
||||||
| **Elevation of Privilege** | Unauthorized package installation | Root required, but mTLS + IP whitelist required | ✅ Mitigated |
|
|
||||||
| **Elevation of Privilege** | Subprocess escape | SystemCallFilter, ProtectSystem=strict | ✅ Mitigated |
|
|
||||||
|
|
||||||
### Attack Vectors & Mitigations
|
- **Acknowledgment** within 48 hours
|
||||||
|
- **Initial assessment** within 7 days
|
||||||
|
- **Ongoing updates** on remediation progress
|
||||||
|
|
||||||
| Attack Vector | Likelihood | Impact | Mitigation |
|
## Disclosure Policy
|
||||||
|---------------|------------|--------|------------|
|
|
||||||
| Network interception | Low | Critical | TLS 1.3 only, mTLS required |
|
|
||||||
| Certificate theft | Medium | Critical | Cert permissions (600), internal CA only |
|
|
||||||
| IP spoofing | Low | High | IP whitelist + mTLS (both required) |
|
|
||||||
| Config file tampering | Medium | High | File permissions, validation before reload |
|
|
||||||
| Package manager injection | Low | Critical | Pluggable backend with input validation |
|
|
||||||
| Job manipulation | Low | High | Job storage isolation, exclusive rollback mode |
|
|
||||||
| Log tampering | Medium | High | systemd journal (immutable), optional remote syslog |
|
|
||||||
|
|
||||||
---
|
We follow **coordinated disclosure**:
|
||||||
|
|
||||||
## Authentication & Authorization
|
- We ask for **90 days** before public disclosure of a vulnerability
|
||||||
|
- Security advisories are published via [GitHub Security Advisories](https://github.com/Draco-Lunaris/Linux-Patch-Api/security/advisories)
|
||||||
|
- We will work with you to determine an appropriate disclosure timeline when a fix requires more time
|
||||||
|
|
||||||
### Authentication Requirements
|
## Security Best Practices
|
||||||
|
|
||||||
- **Method:** mTLS certificate-based authentication
|
This project is a security tool — we hold ourselves to a high standard:
|
||||||
- **Certificate Type:** Unique client certificate per client (1-year validity)
|
|
||||||
- **CA:** Internal self-hosted Certificate Authority
|
|
||||||
- **TLS Version:** TLS 1.3 only
|
|
||||||
- **Multi-factor:** Certificate + IP whitelist (dual requirement)
|
|
||||||
- **Session Management:** Stateless (no sessions)
|
|
||||||
|
|
||||||
### Authorization Model
|
- **Signed commits**: All commits must be signed (SSH signing)
|
||||||
|
- **CI enforcement**: All PRs require passing CI checks (fmt, clippy, test, audit, build)
|
||||||
|
- **Dependency auditing**: `cargo audit` runs in CI to catch known vulnerabilities
|
||||||
|
|
||||||
- **Model:** Binary authorization (all-or-nothing)
|
## Credit
|
||||||
- **Permission Levels:** Single level (full access if authenticated)
|
|
||||||
- **Requirements:**
|
|
||||||
- Valid mTLS certificate (not expired, signed by internal CA)
|
|
||||||
- Source IP in whitelist (YAML config, instant apply)
|
|
||||||
- **No RBAC:** All authenticated clients have full API access
|
|
||||||
|
|
||||||
---
|
Contributors who responsibly report vulnerabilities will be credited in the corresponding GitHub Security Advisory.
|
||||||
|
|
||||||
## Data Security
|
|
||||||
|
|
||||||
### Encryption at Rest
|
|
||||||
|
|
||||||
- **Certificates:** File permissions 600 for private keys
|
|
||||||
- **Job Storage:** `/var/lib/linux_patch_api/jobs/` (cleared on restart)
|
|
||||||
- **Config Files:** `/etc/linux_patch_api/` (644 for config, 600 for keys)
|
|
||||||
- **Audit Logs:** systemd journal (immutable by default)
|
|
||||||
|
|
||||||
### Encryption in Transit
|
|
||||||
|
|
||||||
- **Protocol:** TLS 1.3 only
|
|
||||||
- **Port:** 12443
|
|
||||||
- **Cipher Suites:** TLS 1.3 default (no legacy cipher support)
|
|
||||||
- **Certificate Validation:** Mutual TLS (server + client cert required)
|
|
||||||
|
|
||||||
### Key Management
|
|
||||||
|
|
||||||
- **CA Private Key:** Stored securely on CA host only
|
|
||||||
- **Server Certificates:** `/etc/linux_patch_api/certs/server.key` (600)
|
|
||||||
- **Client Certificates:** Distributed manually to authorized clients
|
|
||||||
- **Rotation:** 1-year certificate expiry, manual renewal process
|
|
||||||
- **Revocation:** Not implemented (rely on expiry + physical cert retrieval)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API Security
|
|
||||||
|
|
||||||
### Input Validation
|
|
||||||
|
|
||||||
- **Package Names:** Alphanumeric + standard package chars only
|
|
||||||
- **Versions:** Semantic versioning validation
|
|
||||||
- **IP Addresses:** IPv4 + CIDR validation for whitelist
|
|
||||||
- **JSON Schema:** Strict schema validation for all request bodies
|
|
||||||
- **Path Traversal:** Blocked (no `..` in paths)
|
|
||||||
|
|
||||||
### Rate Limiting
|
|
||||||
|
|
||||||
- **Not Required:** Internal network only with strict IP whitelist
|
|
||||||
- **Job Concurrency:** Configurable limit (default: 5 concurrent jobs)
|
|
||||||
- **Job Timeout:** 30-minute maximum per job
|
|
||||||
|
|
||||||
### CORS Policy
|
|
||||||
|
|
||||||
- **Not Applicable:** API is not browser-accessible
|
|
||||||
- **Origin:** mTLS clients only (no browser CORS concerns)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Audit & Logging
|
|
||||||
|
|
||||||
### Security Events to Log
|
|
||||||
|
|
||||||
- All API requests (endpoint, method, timestamp, client cert ID, source IP)
|
|
||||||
- Authentication events (success/failure, cert validation result)
|
|
||||||
- Authorization events (IP whitelist match/failure)
|
|
||||||
- Package operations (package name, version, action, result)
|
|
||||||
- Configuration changes (config reload, whitelist updates)
|
|
||||||
- Job lifecycle events (create, start, complete, fail, timeout, rollback)
|
|
||||||
- Service events (start, stop, restart, config validation failures)
|
|
||||||
|
|
||||||
### Log Protection
|
|
||||||
|
|
||||||
- **Primary Storage:** systemd journal (immutable, access-controlled)
|
|
||||||
- **Secondary Storage:** Optional remote syslog
|
|
||||||
- **Fallback:** Local file `/var/log/linux_patch_api/audit.log` (640)
|
|
||||||
- **Retention:** 30 days with daily rotation and compression
|
|
||||||
- **Access:** Root only, audit group read access
|
|
||||||
- **Integrity:** systemd journal provides tamper evidence
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Compliance Requirements
|
|
||||||
|
|
||||||
- **Internal Standards:** Follows organizational security policies
|
|
||||||
- **No External Compliance:** Not designed for PCI-DSS, HIPAA, SOC2 (can be extended)
|
|
||||||
- **Audit Trail:** Comprehensive logging supports internal audit requirements
|
|
||||||
- **Access Control:** mTLS + IP whitelist provides strong access control
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Testing
|
|
||||||
|
|
||||||
### Penetration Testing
|
|
||||||
|
|
||||||
- **Schedule:** Required before production deployment
|
|
||||||
- **Scope:**
|
|
||||||
- mTLS authentication bypass attempts
|
|
||||||
- IP whitelist enforcement testing
|
|
||||||
- API endpoint fuzzing
|
|
||||||
- Certificate validation testing
|
|
||||||
- Config file tampering attempts
|
|
||||||
- Privilege escalation attempts
|
|
||||||
- **Tester:** Internal security team or external contractor
|
|
||||||
- **Frequency:** Annual or after major changes
|
|
||||||
|
|
||||||
### Vulnerability Management
|
|
||||||
|
|
||||||
- **Dependency Scanning:** Rust crate security advisories monitored
|
|
||||||
- **System Patches:** Host system patched via API itself (dogfooding)
|
|
||||||
- **Certificate Updates:** Annual renewal process
|
|
||||||
- **Config Audits:** Quarterly review of whitelist and security settings
|
|
||||||
- **Incident Response:** Log analysis for security event investigation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3 Security Testing Results
|
|
||||||
|
|
||||||
**Test Date:** 2026-04-09
|
|
||||||
**Tester:** Agent Zero Fuzz Testing Agent
|
|
||||||
**Status:** ✅ ALL CRITICAL ISSUES RESOLVED - Minor improvements recommended
|
|
||||||
|
|
||||||
### Security Test Summary (16 Tests)
|
|
||||||
|
|
||||||
| Category | Passed | Failed | Status |
|
|
||||||
|----------|--------|--------|--------|
|
|
||||||
| mTLS Enforcement | 3 | 0 | ✅ Complete |
|
|
||||||
| IP Whitelist | 1 | 0 | ✅ Complete |
|
|
||||||
| API Endpoints | 5 | 0 | ✅ Complete |
|
|
||||||
| Input Validation | 3 | 0 | ✅ Complete |
|
|
||||||
| Certificate Security | 2 | 0 | ✅ Complete |
|
|
||||||
| Configuration Security | 2 | 0 | ✅ Complete |
|
|
||||||
| **TOTAL** | **16** | **0** | **✅ 100%** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3 Fuzz Testing Results
|
|
||||||
|
|
||||||
**Test Date:** 2026-04-09
|
|
||||||
**Tester:** Agent Zero Fuzz Testing Agent
|
|
||||||
**Test Type:** Comprehensive Fuzz Testing
|
|
||||||
**Overall Status:** ⚠️ GOOD - Minor improvements needed
|
|
||||||
|
|
||||||
### Fuzz Test Summary (21 Tests)
|
|
||||||
|
|
||||||
| Section | Tests | Passed | Failed | Pass Rate |
|
|
||||||
|---------|-------|--------|--------|-----------|
|
|
||||||
| API Input Fuzzing | 8 | 5 | 3 | 62.5% |
|
|
||||||
| Request Header Fuzzing | 5 | 2 | 3 | 40% |
|
|
||||||
| Certificate Fuzzing | 5 | 5 | 0 | 100% |
|
|
||||||
| Rate Limiting/DoS | 3 | 3 | 0 | 100% |
|
|
||||||
| **TOTAL** | **21** | **15** | **6** | **71.4%** |
|
|
||||||
|
|
||||||
### Vulnerabilities Identified
|
|
||||||
|
|
||||||
| ID | Severity | Category | Description | Status |
|
|
||||||
|----|----------|----------|-------------|--------|
|
|
||||||
| VULN-001 | MEDIUM | Input Validation | Missing input length validation | 📝 Recommended |
|
|
||||||
| VULN-002 | MEDIUM | Input Validation | Path traversal partial bypass | 📝 Recommended |
|
|
||||||
| VULN-003 | LOW | Input Validation | Empty string validation missing | 📝 Recommended |
|
|
||||||
| VULN-004 | MEDIUM | Header Security | Missing header size limits | 📝 Recommended |
|
|
||||||
| VULN-005 | LOW | HTTP Protocol | Invalid methods return 404 vs 405 | 📝 Recommended |
|
|
||||||
| VULN-006 | LOW | Header Security | Duplicate header handling | 📝 Recommended |
|
|
||||||
|
|
||||||
### Security Strengths Confirmed
|
|
||||||
|
|
||||||
✅ **mTLS Implementation: ROBUST**
|
|
||||||
- All invalid certificates properly rejected at TLS layer
|
|
||||||
- Silent drop behavior prevents information leakage
|
|
||||||
- Certificate chain validation working correctly
|
|
||||||
|
|
||||||
✅ **Injection Protection: EFFECTIVE**
|
|
||||||
- SQL injection patterns: 4/4 blocked
|
|
||||||
- Command injection patterns: 5/5 handled safely
|
|
||||||
|
|
||||||
✅ **DoS Protection: ADEQUATE**
|
|
||||||
- Large payloads (10MB) properly rejected with HTTP 413
|
|
||||||
- Concurrent connections (20) handled gracefully
|
|
||||||
- Rapid flooding (100 req) completed without service degradation
|
|
||||||
|
|
||||||
### Recommendations for Phase 4
|
|
||||||
|
|
||||||
**Medium Priority:**
|
|
||||||
1. Implement input length validation (package names: 256 chars max)
|
|
||||||
2. Enhance path traversal protection with strict normalization
|
|
||||||
3. Configure header size limits (8KB max)
|
|
||||||
|
|
||||||
**Low Priority:**
|
|
||||||
4. Return 405 Method Not Allowed for unsupported methods
|
|
||||||
5. Reject empty strings for required fields
|
|
||||||
6. Handle duplicate headers with rejection
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overall Security Assessment
|
|
||||||
|
|
||||||
| Category | Status | Notes |
|
|
||||||
|----------|--------|-------|
|
|
||||||
| Authentication (mTLS) | ✅ SECURE | All certificate attacks blocked |
|
|
||||||
| Authorization (IP Whitelist) | ✅ SECURE | Properly enforced |
|
|
||||||
| Input Validation | ⚠️ GOOD | Minor improvements recommended |
|
|
||||||
| Injection Protection | ✅ SECURE | SQL/Command/Path traversal blocked |
|
|
||||||
| DoS Protection | ✅ SECURE | Large payloads rejected |
|
|
||||||
| Certificate Security | ✅ SECURE | Robust mTLS implementation |
|
|
||||||
|
|
||||||
**Overall Security Posture: GOOD**
|
|
||||||
|
|
||||||
The API is suitable for internal network deployment. The 6 identified vulnerabilities are low-to-medium severity and represent hardening opportunities rather than critical security gaps. All critical and high severity issues from earlier testing have been resolved.
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3 Threat Model Validation
|
|
||||||
|
|
||||||
**Validation Date:** 2026-04-09
|
|
||||||
**Validator:** Threat Model Validation Agent (Agent Zero)
|
|
||||||
**Report:** THREAT_MODEL_VALIDATION.md
|
|
||||||
|
|
||||||
### STRIDE Validation Summary
|
|
||||||
|
|
||||||
| Category | Status | Confidence |
|
|
||||||
|----------|--------|------------|
|
|
||||||
| Spoofing | ✅ Fully Mitigated | High |
|
|
||||||
| Tampering | ⚠️ Partially Mitigated | Medium |
|
|
||||||
| Repudiation | ✅ Fully Mitigated | High |
|
|
||||||
| Information Disclosure | ✅ Fully Mitigated | High |
|
|
||||||
| Denial of Service | ⚠️ Partially Mitigated | Medium |
|
|
||||||
| Elevation of Privilege | ✅ Fully Mitigated | High |
|
|
||||||
|
|
||||||
### Key Findings
|
|
||||||
|
|
||||||
**Validated Strengths:**
|
|
||||||
- mTLS authentication robust (all certificate attacks blocked)
|
|
||||||
- TLS 1.3 enforcement verified (plain HTTP rejected)
|
|
||||||
- IP whitelist enforcement working correctly
|
|
||||||
- Audit logging provides strong non-repudiation
|
|
||||||
- Job-level DoS protection implemented
|
|
||||||
- Injection protection effective (SQL, command, path traversal)
|
|
||||||
- Systemd hardening in place
|
|
||||||
|
|
||||||
**Identified Gaps (Medium Priority):**
|
|
||||||
- Rate limiting not implemented (relies on network security)
|
|
||||||
- Header size limits not configured
|
|
||||||
- Input length validation missing
|
|
||||||
- Config file integrity relies on permissions only
|
|
||||||
- No certificate revocation mechanism
|
|
||||||
|
|
||||||
**Recommendation:** Proceed to Phase 4 with focus on medium-priority hardening items. API suitable for internal network deployment with current mitigations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test Artifacts
|
|
||||||
|
|
||||||
- Fuzz test script: `/a0/usr/projects/linux_patch_api/fuzz_tests.sh`
|
|
||||||
- Security test script: `/a0/usr/projects/linux_patch_api/security_tests.sh`
|
|
||||||
- Fuzz test report: `/a0/usr/projects/linux_patch_api/FUZZ_TEST_REPORT.md`
|
|
||||||
- Security findings report: `/a0/usr/projects/linux_patch_api/SECURITY_FINDINGS_REPORT.md`
|
|
||||||
- Threat model validation: `/a0/usr/projects/linux_patch_api/THREAT_MODEL_VALIDATION.md`
|
|
||||||
- API specification: `/a0/usr/projects/linux_patch_api/API_SPEC.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Security documentation updated following Phase 3 Security Hardening and Threat Model Validation - Agent Zero*
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test Artifacts
|
|
||||||
|
|
||||||
- Fuzz test script: `/a0/usr/projects/linux_patch_api/fuzz_tests.sh`
|
|
||||||
- Security test script: `/a0/usr/projects/linux_patch_api/security_tests.sh`
|
|
||||||
- Fuzz test report: `/a0/usr/projects/linux_patch_api/FUZZ_TEST_REPORT.md`
|
|
||||||
- API specification: `/a0/usr/projects/linux_patch_api/API_SPEC.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Security documentation updated following Phase 3 Security Hardening - Agent Zero Fuzz Testing Agent*
|
|
||||||
|
|||||||
@ -41,7 +41,7 @@
|
|||||||
| **SPEC.md Reference** | Lines 132-138 |
|
| **SPEC.md Reference** | Lines 132-138 |
|
||||||
| **Requirement** | Internal self-hosted CA for certificate issuance |
|
| **Requirement** | Internal self-hosted CA for certificate issuance |
|
||||||
| **Implementation** | OpenSSL CA infrastructure with 4096-bit RSA keys |
|
| **Implementation** | OpenSSL CA infrastructure with 4096-bit RSA keys |
|
||||||
| **Evidence** | `configs/CA_SETUP.md`, `configs/certs/ca.pem`, `configs/certs/ca.key.pem` |
|
| **Evidence** | `configs/CA_SETUP.md`, `scripts/generate-dev-certs.sh` (private keys generated at runtime, not committed) |
|
||||||
| **Test Result** | ✅ PASS - CA properly signs server and client certificates |
|
| **Test Result** | ✅ PASS - CA properly signs server and client certificates |
|
||||||
| **Compliance Status** | ✅ COMPLIANT |
|
| **Compliance Status** | ✅ COMPLIANT |
|
||||||
|
|
||||||
@ -52,7 +52,7 @@
|
|||||||
| **SPEC.md Reference** | Line 136 |
|
| **SPEC.md Reference** | Line 136 |
|
||||||
| **Requirement** | Unique certificate per client (no shared certs) |
|
| **Requirement** | Unique certificate per client (no shared certs) |
|
||||||
| **Implementation** | Per-client certificate generation with unique CN |
|
| **Implementation** | Per-client certificate generation with unique CN |
|
||||||
| **Evidence** | `configs/certs/client001.pem`, `SECURITY.md` line 65 |
|
| **Evidence** | `scripts/generate-dev-certs.sh` (certificates generated at runtime, not committed) |
|
||||||
| **Test Result** | ✅ PASS - Each client has distinct certificate |
|
| **Test Result** | ✅ PASS - Each client has distinct certificate |
|
||||||
| **Compliance Status** | ✅ COMPLIANT |
|
| **Compliance Status** | ✅ COMPLIANT |
|
||||||
|
|
||||||
@ -63,7 +63,7 @@
|
|||||||
| **SPEC.md Reference** | Line 135 |
|
| **SPEC.md Reference** | Line 135 |
|
||||||
| **Requirement** | 1 year standard certificate expiration |
|
| **Requirement** | 1 year standard certificate expiration |
|
||||||
| **Implementation** | Certificates generated with `-days 365` parameter |
|
| **Implementation** | Certificates generated with `-days 365` parameter |
|
||||||
| **Evidence** | `configs/certs/` certificate files, `openssl x509 -in cert.pem -noout -dates` |
|
| **Evidence** | `scripts/generate-dev-certs.sh` (certificates generated at runtime, not committed) |
|
||||||
| **Test Result** | ✅ PASS - Expired certificates properly rejected (FUZZ_TEST_REPORT.md Test 3.2) |
|
| **Test Result** | ✅ PASS - Expired certificates properly rejected (FUZZ_TEST_REPORT.md Test 3.2) |
|
||||||
| **Compliance Status** | ✅ COMPLIANT |
|
| **Compliance Status** | ✅ COMPLIANT |
|
||||||
|
|
||||||
@ -137,7 +137,7 @@
|
|||||||
| **SPEC.md Reference** | Lines 86-89 |
|
| **SPEC.md Reference** | Lines 86-89 |
|
||||||
| **Requirement** | Private key permissions 600 (owner read/write only) |
|
| **Requirement** | Private key permissions 600 (owner read/write only) |
|
||||||
| **Implementation** | File permissions set during certificate deployment |
|
| **Implementation** | File permissions set during certificate deployment |
|
||||||
| **Evidence** | `configs/certs/*.key.pem` (chmod 600), `DEPLOYMENT_SECURITY_GUIDE.md` Section 1 |
|
| **Evidence** | Private keys generated at runtime with `chmod 600` by `scripts/generate-dev-certs.sh`, not committed to repository |
|
||||||
| **Test Result** | ✅ PASS - Key files properly protected |
|
| **Test Result** | ✅ PASS - Key files properly protected |
|
||||||
| **Compliance Status** | ✅ COMPLIANT |
|
| **Compliance Status** | ✅ COMPLIANT |
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
| **Total Tests** | 16 |
|
| **Total Tests** | 16 |
|
||||||
| **Passed** | 16 |
|
| **Passed** | 16 |
|
||||||
| **Failed** | 0 |
|
| **Failed** | 0 |
|
||||||
| **Critical Findings** | 0 (Previously 1 - RESOLVED) |
|
| **Critical Findings** | 1 (Issue #12 - Committed Private Keys - RESOLVED) |
|
||||||
| **High Findings** | 0 (Previously 2 - RESOLVED) |
|
| **High Findings** | 0 (Previously 2 - RESOLVED) |
|
||||||
| **Medium Findings** | 3 (Unchanged) |
|
| **Medium Findings** | 3 (Unchanged) |
|
||||||
| **Low Findings** | 4 (Unchanged) |
|
| **Low Findings** | 4 (Unchanged) |
|
||||||
@ -150,6 +150,36 @@ Consider storing CA key on separate, more secure host.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 🔴 CRITICAL: Committed Private Key Material (Issue #12)
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
Private key files (`*.key`, `*.key.pem`) were committed to version control in:
|
||||||
|
- `configs/certs/ca.key.pem` — CA private key
|
||||||
|
- `configs/certs/server.key.pem` — Server private key
|
||||||
|
- `configs/certs/client001.key.pem` — Client private key
|
||||||
|
- `tests/e2e/certs/client.key` — E2E test client private key
|
||||||
|
|
||||||
|
Committed private keys are a critical security risk: anyone with repository access
|
||||||
|
(even read-only) can impersonate the server or clients, decrypt captured TLS traffic,
|
||||||
|
or forge certificates signed by the CA.
|
||||||
|
|
||||||
|
**Status:** ✅ RESOLVED
|
||||||
|
|
||||||
|
**Remediation Applied:**
|
||||||
|
1. Removed all private key files from git tracking (`git rm --cached`)
|
||||||
|
2. Added `*.key`, `*.key.pem`, `configs/certs/`, and `tests/e2e/certs/*.key` to `.gitignore`
|
||||||
|
3. Created `scripts/generate-dev-certs.sh` to generate test certificates at runtime
|
||||||
|
4. Updated e2e tests to generate certificates on demand instead of loading from disk
|
||||||
|
5. Added `gitleaks` secret scanning to CI pipeline
|
||||||
|
6. Git history will be purged with `git filter-repo` after PR merge
|
||||||
|
|
||||||
|
**Key Rotation:**
|
||||||
|
These keys were used for development/testing only. No production key rotation is needed.
|
||||||
|
All committed keys should be considered compromised and must not be used in any
|
||||||
|
production environment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 🟢 LOW: No Automated Security Scanning
|
### 🟢 LOW: No Automated Security Scanning
|
||||||
|
|
||||||
**Description:**
|
**Description:**
|
||||||
@ -235,5 +265,39 @@ The Linux_Patch_API Phase 3 is now **SECURE FOR DEPLOYMENT** in an internal netw
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Architecture Decision Record: rustls as Authoritative Client-Auth Gate
|
||||||
|
|
||||||
|
**Date:** 2026-06-06
|
||||||
|
**Status:** Accepted
|
||||||
|
**Context:** Issue #13
|
||||||
|
|
||||||
|
### Decision
|
||||||
|
|
||||||
|
Client certificate authentication is enforced at the TLS handshake level by rustls via `CrlAwareVerifier`, NOT by application-layer middleware.
|
||||||
|
|
||||||
|
### Context
|
||||||
|
|
||||||
|
The original `MtlsMiddleware` was never wired into the Actix-web pipeline (dead code). It contained:
|
||||||
|
1. A duplicate-header check (VULN-006) that never ran
|
||||||
|
2. A `validate_client_certificate()` stub that returned `Ok(())` unconditionally
|
||||||
|
|
||||||
|
Meanwhile, actual client certificate verification was always performed by rustls at the TLS handshake level through `CrlAwareVerifier` (which wraps `WebPkiClientVerifier`), with CRL revocation checking integrated into the same path.
|
||||||
|
|
||||||
|
### Changes Made
|
||||||
|
|
||||||
|
1. **Removed dead code:** `MtlsMiddleware`, `MtlsMiddlewareService`, `validate_client_certificate()`, and the Transform/Service impls
|
||||||
|
2. **Extracted VULN-006:** `has_duplicate_critical_headers()` moved to new `SecurityHeadersMiddleware` (wired into pipeline)
|
||||||
|
3. **Converted `build_rustls_config()`** from method on `MtlsMiddleware` to free function
|
||||||
|
4. **Preserved:** `CrlAwareVerifier`, `MtlsConfig`, `MtlsError`, `ClientCertInfo`, `build_rustls_config()`, and all CRL infrastructure
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
- rustls provides battle-tested X.509 verification at the TLS handshake level
|
||||||
|
- Enforcing auth at the TLS layer eliminates bypass vulnerabilities (middleware ordering bugs, route-specific skips)
|
||||||
|
- CRL revocation checking is integrated into the same handshake path
|
||||||
|
- Application-layer certificate validation is redundant when TLS already rejects untrusted connections
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**Report Generated:** 2026-04-09T22:57:00Z
|
**Report Generated:** 2026-04-09T22:57:00Z
|
||||||
**Verified By:** Security Verification Agent (Agent Zero)
|
**Verified By:** Security Verification Agent (Agent Zero)
|
||||||
|
|||||||
162
SPEC.md
162
SPEC.md
@ -3,7 +3,7 @@
|
|||||||
## Project Overview
|
## Project Overview
|
||||||
**Title:** Linux_Patch_API
|
**Title:** Linux_Patch_API
|
||||||
**Description:** API service for secure remote management of patching processes and software add/removal
|
**Description:** API service for secure remote management of patching processes and software add/removal
|
||||||
**Version:** 0.0.1
|
**Version:** 1.2.0
|
||||||
**Status:** Draft
|
**Status:** Draft
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
@ -105,6 +105,12 @@
|
|||||||
- Permission denied
|
- Permission denied
|
||||||
- System resource errors
|
- System resource errors
|
||||||
- Configuration errors
|
- Configuration errors
|
||||||
|
- Enrollment failures:
|
||||||
|
- `ENROLLMENT_DENIED`: Admin rejected enrollment request on linux_patch_manager
|
||||||
|
- `ENROLLMENT_EXPIRED`: Polling token expired or purged (HTTP 404 from manager)
|
||||||
|
- `ENROLLMENT_TIMEOUT`: 24-hour polling limit exceeded (1440 attempts exhausted)
|
||||||
|
- `ENROLLMENT_RATE_LIMITED`: Request rate limit exceeded (1/minute per IP, HTTP 429)
|
||||||
|
- `PKI_PROVISION_FAILED`: Certificate write or PEM validation failed during provisioning
|
||||||
|
|
||||||
- **Error Message Policy:**
|
- **Error Message Policy:**
|
||||||
- mTLS confirmed clients: Detailed error messages with debugging info
|
- mTLS confirmed clients: Detailed error messages with debugging info
|
||||||
@ -136,11 +142,120 @@
|
|||||||
## Certificate Management
|
## Certificate Management
|
||||||
|
|
||||||
- **CA Type:** Internal self-hosted Certificate Authority
|
- **CA Type:** Internal self-hosted Certificate Authority
|
||||||
- **Distribution:** Manual certificate distribution to clients
|
- **Distribution:** Automated Self-Enrollment (preferred) OR manual certificate distribution
|
||||||
|
- Auto-Enrollment: daemon automatically enrolls on startup when certs are missing/invalid and `enrollment.manager_url` is configured
|
||||||
|
- Manual Enrollment: `linux-patch-api --enroll <url>` for explicit enrollment (exits after completion, does not start server)
|
||||||
|
- Eliminates manual certificate copy/permission management for new hosts
|
||||||
- **Scope:** Limited distribution (small number of authorized clients)
|
- **Scope:** Limited distribution (small number of authorized clients)
|
||||||
- **Validity Period:** 1 year standard expiration
|
- **Validity Period:** 1 year standard expiration
|
||||||
- **Client Identity:** Unique certificate per client (no shared certs)
|
- **Client Identity:** Unique certificate per client (no shared certs)
|
||||||
- **Rotation:** Manual renewal process before expiration
|
- **Rotation:** Automatic re-enrollment when certs are expiring within threshold, or manual via `--renew-certs`
|
||||||
|
|
||||||
|
## Certificate Validation
|
||||||
|
|
||||||
|
On startup, the daemon validates all configured TLS certificates before attempting to bind the listening port. Validation checks (in order):
|
||||||
|
|
||||||
|
1. **Existence**: All three cert files (`ca_cert`, `server_cert`, `server_key`) must exist at configured paths
|
||||||
|
2. **Parse**: Each file must be valid PEM — CA and server cert must parse as X.509, server key must parse as PKCS#8 or PKCS#1
|
||||||
|
3. **Expiry**: CA cert and server cert must not be expired (`not_after > now`). Certs expiring within `cert_renewal_threshold_days` (default 7) trigger a warning and auto-re-enrollment
|
||||||
|
4. **Key match**: Server cert's public key must correspond to server key's private key
|
||||||
|
5. **CA trust**: Server cert must be signed by the CA cert (or chain validates to CA)
|
||||||
|
|
||||||
|
Validation results determine startup behavior:
|
||||||
|
|
||||||
|
| Result | Action |
|
||||||
|
|--------|--------|
|
||||||
|
| Valid | Start normally with mTLS |
|
||||||
|
| ExpiringSoon | Log warning, start normally, schedule background re-enrollment |
|
||||||
|
| Missing/Corrupt/Expired/KeyMismatch/Untrusted | Trigger auto-enrollment if `enrollment.manager_url` configured, otherwise exit with guidance |
|
||||||
|
|
||||||
|
## Self-Enrollment Workflow
|
||||||
|
|
||||||
|
The `linux_patch_api` daemon supports automated self-enrollment to securely request identity from the `linux_patch_manager` without manual PKI distribution. Enrollment can be triggered automatically on startup or manually via CLI.
|
||||||
|
|
||||||
|
### Auto-Enrollment on Startup
|
||||||
|
|
||||||
|
When cert validation fails AND `enrollment.manager_url` is configured in config.yaml, the daemon automatically enters enrollment mode:
|
||||||
|
|
||||||
|
1. Log: "Certs [status]. Auto-enrolling with <url>"
|
||||||
|
2. Skip cert validation (`skip_tls_validation=true`)
|
||||||
|
3. Register with manager (POST /api/v1/enroll)
|
||||||
|
- If host already exists: log warning, skip to step 5 (polling for re-provisioning)
|
||||||
|
- If new registration: receive polling token
|
||||||
|
4. Poll for approval (GET /api/v1/enroll/status/{token})
|
||||||
|
- Persist `polling_token` to config.yaml for resume after restart
|
||||||
|
- Retry with exponential backoff on network errors
|
||||||
|
5. When approved: provision certs (ca.pem, server.pem, server.key)
|
||||||
|
6. Re-validate certs (should now be Valid)
|
||||||
|
7. Continue to normal mTLS server startup
|
||||||
|
|
||||||
|
If enrollment fails (network error, manager unreachable):
|
||||||
|
- Log: "Auto-enrollment failed: [error]. Retrying on next restart."
|
||||||
|
- Exit code 1 (triggers systemd restart with backoff)
|
||||||
|
|
||||||
|
If no enrollment URL is configured and certs are invalid:
|
||||||
|
- Log clear error with guidance (add URL, run --enroll, or place certs manually)
|
||||||
|
- Exit code 0 (don't trigger restart loop)
|
||||||
|
|
||||||
|
### Polling Token Resume
|
||||||
|
|
||||||
|
If the service restarts during enrollment polling:
|
||||||
|
1. Read `polling_token` from config.yaml (persisted during enrollment)
|
||||||
|
2. If token exists and `enrollment.manager_url` is configured:
|
||||||
|
a. Resume polling from where left off
|
||||||
|
b. Don't re-register (host already has a pending request)
|
||||||
|
3. On successful provisioning:
|
||||||
|
a. Clear `polling_token` from config.yaml
|
||||||
|
b. Continue to normal server startup
|
||||||
|
|
||||||
|
### CLI Enrollment (`--enroll`)
|
||||||
|
|
||||||
|
```
|
||||||
|
linux-patch-api --enroll https://<manager_url>
|
||||||
|
```
|
||||||
|
|
||||||
|
The enrollment flow runs and **exits after completion** — it does NOT start the server. This prevents port conflicts with the systemd service.
|
||||||
|
|
||||||
|
- On success: prints "Enrollment complete. Start service: systemctl start linux-patch-api" and exits with code 0
|
||||||
|
- On failure: exits with code 1 (triggers systemd restart if configured)
|
||||||
|
|
||||||
|
### Security Model
|
||||||
|
- Initial connection uses TLS with verification disabled (`danger_accept_invalid_certs`)
|
||||||
|
- Manager approval workflow provides authorization; transport encryption is secondary during enrollment
|
||||||
|
- URL scheme validation prevents SSRF/path traversal (only `http` and `https` permitted)
|
||||||
|
- Host component required in manager URL
|
||||||
|
|
||||||
|
### Phase 1: Registration Request
|
||||||
|
- **Identity Extraction:**
|
||||||
|
- `/etc/machine-id` (fallback: `/var/lib/dbus/machine-id`)
|
||||||
|
- FQDN from `hostname -f` (validated contains `.`) → `hostname` + `hostname -d` → `/etc/hostname` → `hostname` → `localhost`
|
||||||
|
- Non-loopback IPv4 addresses via network interface enumeration
|
||||||
|
- OS details from `/etc/os-release` (distro, version, id_like, codename) + kernel version (`uname -r`)
|
||||||
|
- **Submission:** Unauthenticated `POST /api/v1/enroll` to manager with identity payload
|
||||||
|
- **Response:** HTTP 202 with temporary `polling_token` (bearer credential — never logged)
|
||||||
|
- **Rate Limiting:** Manager enforces 1 request/minute per IP (HTTP 429 on violation)
|
||||||
|
|
||||||
|
### Phase 2: Polling & Approval
|
||||||
|
- **Polling Loop:** `GET /api/v1/enroll/status/{token}` with configurable interval and max attempts
|
||||||
|
- **Default Interval:** 60 seconds (configurable via `enrollment.polling_interval_seconds`)
|
||||||
|
- **Hard Timeout:** 24 hours maximum (1440 attempts; values >1440 clamped to 1440)
|
||||||
|
- **Status States:**
|
||||||
|
- `pending`: Continue polling
|
||||||
|
- `approved`: Proceed to Phase 3 with PKI bundle
|
||||||
|
- `denied`: Abort enrollment (`ENROLLMENT_DENIED`)
|
||||||
|
- `not_found`: Token expired/purged — abort (`ENROLLMENT_EXPIRED`)
|
||||||
|
- **Signal Handling:** SIGINT (Ctrl+C) and SIGTERM interrupt polling gracefully
|
||||||
|
- **Transient Errors:** Network failures and HTTP 5xx retried with backoff; HTTP 404/429 terminate immediately
|
||||||
|
- **Log Throttling:** Status logged every 10 attempts or after 5 minutes elapsed
|
||||||
|
|
||||||
|
### Phase 3: PKI Provisioning
|
||||||
|
- **Certificate Validation:** PEM format verification for CA cert, server cert, and server key (supports PKCS#8, PKCS#1 RSA, EC keys)
|
||||||
|
- **Atomic Writes:** Temp file → set permissions → atomic rename pattern prevents partial writes
|
||||||
|
- **File Permissions:** Keys at `0600`, certificates at `0644`, directories at `0755`
|
||||||
|
- **Backup Strategy:** Existing certificate files renamed to `.bak` before overwrite
|
||||||
|
- **Target Paths:** Configured via TLS settings or defaults (`/etc/linux_patch_api/certs/{ca,server,server.key}.pem`)
|
||||||
|
- **Whitelist Auto-Append:** Manager IP resolved (hostname → DNS or direct IP) and appended to `/etc/linux_patch_api/whitelist.yaml`
|
||||||
|
- **Completion:** For auto-enrollment: daemon transitions to standard mTLS listening mode without requiring service restart. For `--enroll`: daemon exits with code 0.
|
||||||
|
|
||||||
## Audit Logging
|
## Audit Logging
|
||||||
|
|
||||||
@ -152,6 +267,16 @@
|
|||||||
- System changes made by the API
|
- System changes made by the API
|
||||||
- Configuration changes (whitelist updates, cert renewals)
|
- Configuration changes (whitelist updates, cert renewals)
|
||||||
|
|
||||||
|
- **Enrollment Events:**
|
||||||
|
- Registration request submitted (machine-id, FQDN, manager URL — polling token never logged)
|
||||||
|
- Polling status changes (`pending` → `approved`/`denied`/`not_found`)
|
||||||
|
- PKI bundle provisioning success/failure with target file paths
|
||||||
|
- Whitelist auto-append during enrollment (manager IP added)
|
||||||
|
- Enrollment timeout or denial with reason
|
||||||
|
- Signal interruption (SIGINT/SIGTERM) during polling
|
||||||
|
- Auto-enrollment triggered (cert status and reason)
|
||||||
|
- Certificate validation results on startup
|
||||||
|
|
||||||
- **Log Storage:**
|
- **Log Storage:**
|
||||||
- Primary: Distribution-appropriate logging
|
- Primary: Distribution-appropriate logging
|
||||||
- systemd journal (journalctl) on systemd systems
|
- systemd journal (journalctl) on systemd systems
|
||||||
@ -193,15 +318,12 @@
|
|||||||
- **mTLS:** CA cert path, server cert path, server key path
|
- **mTLS:** CA cert path, server cert path, server key path
|
||||||
- **Logging:** log level, log retention, remote syslog server (optional)
|
- **Logging:** log level, log retention, remote syslog server (optional)
|
||||||
- **Security:** job timeout, max concurrent jobs, rate limiting
|
- **Security:** job timeout, max concurrent jobs, rate limiting
|
||||||
|
- **Enrollment:** manager_url, polling_interval_seconds, max_poll_attempts, polling_token (auto-populated), cert_renewal_threshold_days
|
||||||
|
|
||||||
- **Hard-Coded Paths (not configurable):**
|
- **Hard-Coded Paths (not configurable):**
|
||||||
- Whitelist file: `/etc/linux_patch_api/whitelist.yaml`
|
- Whitelist file: `/etc/linux_patch_api/whitelist.yaml`
|
||||||
- Data directory: `/var/lib/linux_patch_api/`
|
- Data directory: `/var/lib/linux_patch_api/`
|
||||||
- Job storage: `/var/lib/linux_patch_api/jobs/`
|
- Job storage: `/var/lib/linux_patch_api/jobs/`
|
||||||
- Hard-Coded Paths (not configurable):
|
|
||||||
- Whitelist file: `/etc/linux_patch_api/whitelist.yaml`
|
|
||||||
- Data directory: `/var/lib/linux_patch_api/`
|
|
||||||
- Job storage: `/var/lib/linux_patch_api/jobs/`
|
|
||||||
- Log directory: `/var/log/linux_patch_api/`
|
- Log directory: `/var/log/linux_patch_api/`
|
||||||
|
|
||||||
## Testing Requirements
|
## Testing Requirements
|
||||||
@ -216,6 +338,32 @@
|
|||||||
- CI/CD Pipeline: Required for automated testing
|
- CI/CD Pipeline: Required for automated testing
|
||||||
- Penetration Testing: Required before release
|
- Penetration Testing: Required before release
|
||||||
|
|
||||||
|
## CLI Arguments
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--config <PATH>` or `-c` | Path to configuration file (default: `/etc/linux_patch_api/config.yaml`) |
|
||||||
|
| `--verbose` or `-v` | Enable verbose (DEBUG-level) logging |
|
||||||
|
| `--enroll <MANAGER_URL>` | Run self-enrollment flow with manager at URL, then EXIT (does not start server) |
|
||||||
|
| `--renew-certs` | Validate existing certs and re-enroll if expiring within threshold or invalid |
|
||||||
|
| `--version` or `-V` | Print version information and exit |
|
||||||
|
| `--help` or `-h` | Display help information and exit |
|
||||||
|
|
||||||
|
### Enrollment Mode Behavior
|
||||||
|
|
||||||
|
- **`--enroll <URL>`**: Executes enrollment flow, provisions certs, then **exits with code 0**. Does NOT start server or bind port. Print guidance message on completion.
|
||||||
|
- **Auto-enrollment (startup)**: Triggered when cert validation fails and `enrollment.manager_url` is configured. After provisioning, continues to normal server startup.
|
||||||
|
- **`--renew-certs`**: Validates existing certs. If expiring within threshold or invalid, re-enrolls using `enrollment.manager_url` from config. Exits with code 0 after completion.
|
||||||
|
- TLS verification is disabled on initial manager connection (manager approval workflow provides security)
|
||||||
|
|
||||||
|
### Exit Codes
|
||||||
|
|
||||||
|
| Code | Meaning | systemd Behavior |
|
||||||
|
|------|---------|------------------|
|
||||||
|
| 0 | Clean exit: no certs and no enrollment URL configured, or --enroll/--renew-certs success | No restart |
|
||||||
|
| 1 | Error: config error, enrollment network failure, cert validation error | Restart with backoff |
|
||||||
|
| 2 | Certs invalid, auto-enrollment in progress (will retry) | Restart with backoff |
|
||||||
|
|
||||||
- **Phase 1 Acceptance Criteria:**
|
- **Phase 1 Acceptance Criteria:**
|
||||||
- All endpoints functional with mTLS authentication
|
- All endpoints functional with mTLS authentication
|
||||||
- IP whitelist enforced correctly
|
- IP whitelist enforced correctly
|
||||||
|
|||||||
131
build-alpine.sh
131
build-alpine.sh
@ -22,10 +22,22 @@ fi
|
|||||||
# Generate abuild signing keys
|
# Generate abuild signing keys
|
||||||
echo "Generating abuild signing keys..."
|
echo "Generating abuild signing keys..."
|
||||||
apk add --no-cache abuild
|
apk add --no-cache abuild
|
||||||
|
|
||||||
|
# Force HOME to /root for consistent key generation location
|
||||||
|
export HOME=/root
|
||||||
|
mkdir -p "$HOME/.abuild"
|
||||||
abuild-keygen -a -n 2>&1 | tee /tmp/keygen.log
|
abuild-keygen -a -n 2>&1 | tee /tmp/keygen.log
|
||||||
KEYFILE=$(ls /root/.abuild/*.rsa 2>/dev/null | head -1)
|
|
||||||
|
# Find the generated key using find (ls fails on dash-prefixed filenames)
|
||||||
|
KEYFILE=$(find "$HOME/.abuild" -name "*.rsa" ! -name "*.pub" -type f 2>/dev/null | head -1)
|
||||||
if [ -z "$KEYFILE" ]; then
|
if [ -z "$KEYFILE" ]; then
|
||||||
KEYFILE=$(ls /root/.abuild/-*.rsa 2>/dev/null | head -1)
|
# Fallback: check other common locations where keys might end up
|
||||||
|
KEYFILE=$(find /github/home/.abuild -name "*.rsa" ! -name "*.pub" -type f 2>/dev/null | head -1)
|
||||||
|
fi
|
||||||
|
if [ -z "$KEYFILE" ]; then
|
||||||
|
echo "ERROR: No abuild signing key found!"
|
||||||
|
echo "Searched: $HOME/.abuild, /github/home/.abuild"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Found key: $KEYFILE"
|
echo "Found key: $KEYFILE"
|
||||||
echo "PACKAGER_PRIVKEY=\"$KEYFILE\"" > /etc/abuild.conf
|
echo "PACKAGER_PRIVKEY=\"$KEYFILE\"" > /etc/abuild.conf
|
||||||
@ -44,27 +56,51 @@ else
|
|||||||
echo "Skipping cargo build (SKIP_CARGO_BUILD is set)"
|
echo "Skipping cargo build (SKIP_CARGO_BUILD is set)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create package directory in /home/builduser (accessible by builduser)
|
# Get version from Cargo.toml
|
||||||
PKGDIR=/home/builduser/apk-package
|
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
||||||
mkdir -p "$PKGDIR"/usr/bin
|
|
||||||
mkdir -p "$PKGDIR"/etc/linux_patch_api
|
|
||||||
mkdir -p "$PKGDIR"/etc/init.d
|
|
||||||
|
|
||||||
# Copy files
|
# Create package directory structure
|
||||||
|
PKGDIR=$(pwd)/apk-package
|
||||||
|
rm -rf "$PKGDIR"
|
||||||
|
mkdir -p "$PKGDIR"/usr/bin
|
||||||
|
mkdir -p "$PKGDIR"/etc/linux_patch_api/certs
|
||||||
|
mkdir -p "$PKGDIR"/etc/init.d
|
||||||
|
mkdir -p "$PKGDIR"/var/lib/linux_patch_api
|
||||||
|
mkdir -p "$PKGDIR"/var/log/linux_patch_api
|
||||||
|
|
||||||
|
# Copy binary
|
||||||
|
chmod 755 target/x86_64-unknown-linux-musl/release/linux-patch-api
|
||||||
cp target/x86_64-unknown-linux-musl/release/linux-patch-api "$PKGDIR"/usr/bin/
|
cp target/x86_64-unknown-linux-musl/release/linux-patch-api "$PKGDIR"/usr/bin/
|
||||||
chmod 755 "$PKGDIR"/usr/bin/linux-patch-api
|
|
||||||
|
# Copy OpenRC init script
|
||||||
cp configs/linux-patch-api-openrc "$PKGDIR"/etc/init.d/linux-patch-api
|
cp configs/linux-patch-api-openrc "$PKGDIR"/etc/init.d/linux-patch-api
|
||||||
chmod 755 "$PKGDIR"/etc/init.d/linux-patch-api
|
chmod 755 "$PKGDIR"/etc/init.d/linux-patch-api
|
||||||
cp configs/whitelist.yaml.example "$PKGDIR"/etc/linux_patch_api/whitelist.yaml
|
|
||||||
|
|
||||||
# Use /home/builduser as workspace for APKBUILD
|
# Copy example configs (as .example files - install script creates live configs)
|
||||||
WORKSPACE_DIR=/home/builduser
|
cp configs/config.yaml.example "$PKGDIR"/etc/linux_patch_api/config.yaml.example
|
||||||
|
cp configs/whitelist.yaml.example "$PKGDIR"/etc/linux_patch_api/whitelist.yaml.example
|
||||||
|
|
||||||
# Create APKBUILD
|
# Prepare workspace for abuild
|
||||||
|
WORKSPACE_DIR=/home/builduser/repo
|
||||||
|
rm -rf "$WORKSPACE_DIR"
|
||||||
|
mkdir -p "$WORKSPACE_DIR"
|
||||||
|
|
||||||
|
# Copy package directory to workspace
|
||||||
|
cp -r "$PKGDIR" "$WORKSPACE_DIR"/apk-package
|
||||||
|
|
||||||
|
# Copy install scripts to workspace (must be co-located with APKBUILD)
|
||||||
|
# Alpine abuild requires SEPARATE files with valid suffixes:
|
||||||
|
# pkgname.pre-install, pkgname.post-install, pkgname.pre-deinstall, pkgname.post-deinstall
|
||||||
|
cp configs/linux-patch-api.pre-install "$WORKSPACE_DIR"/linux-patch-api.pre-install
|
||||||
|
cp configs/linux-patch-api.post-install "$WORKSPACE_DIR"/linux-patch-api.post-install
|
||||||
|
cp configs/linux-patch-api.pre-deinstall "$WORKSPACE_DIR"/linux-patch-api.pre-deinstall
|
||||||
|
cp configs/linux-patch-api.post-deinstall "$WORKSPACE_DIR"/linux-patch-api.post-deinstall
|
||||||
|
|
||||||
|
# Create APKBUILD in workspace directory (co-located with install scripts)
|
||||||
echo "Creating APKBUILD..."
|
echo "Creating APKBUILD..."
|
||||||
cat > APKBUILD << EOF
|
cat > "$WORKSPACE_DIR"/APKBUILD << EOF
|
||||||
pkgname=linux-patch-api
|
pkgname=linux-patch-api
|
||||||
pkgver=1.0.0
|
pkgver=${VERSION}
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Secure remote package management API for Linux systems"
|
pkgdesc="Secure remote package management API for Linux systems"
|
||||||
url="https://gitea.moon-dragon.us/echo/linux_patch_api"
|
url="https://gitea.moon-dragon.us/echo/linux_patch_api"
|
||||||
@ -72,78 +108,77 @@ arch="x86_64"
|
|||||||
license="MIT"
|
license="MIT"
|
||||||
makedepends=""
|
makedepends=""
|
||||||
depends="openrc"
|
depends="openrc"
|
||||||
|
install="linux-patch-api.pre-install linux-patch-api.post-install linux-patch-api.pre-deinstall linux-patch-api.post-deinstall"
|
||||||
|
subpackages=""
|
||||||
source=""
|
source=""
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
install -d "\$pkgdir"/usr/bin
|
install -d "\$pkgdir"/usr/bin
|
||||||
install -d "\$pkgdir"/etc/linux_patch_api
|
install -d "\$pkgdir"/etc/linux_patch_api/certs
|
||||||
install -d "\$pkgdir"/etc/init.d
|
install -d "\$pkgdir"/etc/init.d
|
||||||
cp -r ${WORKSPACE_DIR}/apk-package/usr/bin/* "\$pkgdir"/usr/bin/
|
install -d "\$pkgdir"/var/lib/linux_patch_api
|
||||||
cp -r ${WORKSPACE_DIR}/apk-package/etc/linux_patch_api/* "\$pkgdir"/etc/linux_patch_api/
|
install -d "\$pkgdir"/var/log/linux_patch_api
|
||||||
cp -r ${WORKSPACE_DIR}/apk-package/etc/init.d/* "\$pkgdir"/etc/init.d/
|
|
||||||
|
install -Dm755 "\$startdir"/apk-package/usr/bin/linux-patch-api "\$pkgdir"/usr/bin/linux-patch-api
|
||||||
|
install -Dm755 "\$startdir"/apk-package/etc/init.d/linux-patch-api "\$pkgdir"/etc/init.d/linux-patch-api
|
||||||
|
install -Dm644 "\$startdir"/apk-package/etc/linux_patch_api/config.yaml.example "\$pkgdir"/etc/linux_patch_api/config.yaml.example
|
||||||
|
install -Dm644 "\$startdir"/apk-package/etc/linux_patch_api/whitelist.yaml.example "\$pkgdir"/etc/linux_patch_api/whitelist.yaml.example
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Generate checksums for APKBUILD sources
|
|
||||||
echo "Generating checksums..."
|
|
||||||
|
|
||||||
# Build APK package
|
# Build APK package
|
||||||
echo "Building APK package..."
|
echo "Building APK package..."
|
||||||
|
|
||||||
|
# Determine the directory where abuild keys were generated
|
||||||
|
KEY_DIR=$(dirname "$KEYFILE" 2>/dev/null || echo "$HOME/.abuild")
|
||||||
|
echo "Key directory: $KEY_DIR"
|
||||||
|
|
||||||
# For CI environments where we may run as root or as a build user
|
# For CI environments where we may run as root or as a build user
|
||||||
if [ "$(id -u)" = "0" ]; then
|
if [ "$(id -u)" = "0" ]; then
|
||||||
echo "Running as root - creating build user for abuild..."
|
echo "Running as root - creating build user for abuild..."
|
||||||
adduser -D -s /bin/sh builduser 2>/dev/null || true
|
adduser -D -s /bin/sh builduser 2>/dev/null || true
|
||||||
addgroup builduser abuild 2>/dev/null || usermod -aG abuild builduser
|
addgroup builduser abuild 2>/dev/null || usermod -aG abuild builduser
|
||||||
|
|
||||||
# Copy repo contents to builduser home (accessible directory)
|
# Set ownership of workspace
|
||||||
cp -r . /home/builduser/repo/
|
chown -R builduser:builduser "$WORKSPACE_DIR"
|
||||||
chown -R builduser:builduser /home/builduser/repo/
|
|
||||||
chown -R builduser:builduser /home/builduser/apk-package/
|
|
||||||
|
|
||||||
# Set up builduser home directory for abuild
|
# Set up builduser home directory for abuild
|
||||||
|
# Copy keys from wherever abuild-keygen put them (KEY_DIR)
|
||||||
mkdir -p /home/builduser/.abuild
|
mkdir -p /home/builduser/.abuild
|
||||||
cp /root/.abuild/* /home/builduser/.abuild/ 2>/dev/null || true
|
cp "$KEY_DIR"/* /home/builduser/.abuild/ 2>/dev/null || true
|
||||||
chown -R builduser:builduser /home/builduser/.abuild
|
chown -R builduser:builduser /home/builduser/.abuild
|
||||||
|
|
||||||
KEYFILE=$(ls /home/builduser/.abuild/*.rsa 2>/dev/null | head -1)
|
BUILDUSER_KEYFILE=$(ls /home/builduser/.abuild/*.rsa 2>/dev/null | head -1)
|
||||||
if [ -z "$KEYFILE" ]; then
|
if [ -z "$BUILDUSER_KEYFILE" ]; then
|
||||||
KEYFILE=$(ls /home/builduser/.abuild/-*.rsa 2>/dev/null | head -1)
|
BUILDUSER_KEYFILE=$(ls /home/builduser/.abuild/-*.rsa 2>/dev/null | head -1)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Key file: $KEYFILE"
|
echo "Builduser key file: $BUILDUSER_KEYFILE"
|
||||||
echo "PACKAGER_PRIVKEY=\"$KEYFILE\"" > /home/builduser/.abuild/abuild.conf
|
echo "PACKAGER_PRIVKEY=\"$BUILDUSER_KEYFILE\"" > /home/builduser/.abuild/abuild.conf
|
||||||
chown builduser:builduser /home/builduser/.abuild/abuild.conf
|
chown builduser:builduser /home/builduser/.abuild/abuild.conf
|
||||||
|
|
||||||
# Copy APKBUILD and checksums to builduser home for abuild
|
|
||||||
cp APKBUILD /home/builduser/
|
|
||||||
cp .checksums /home/builduser/ 2>/dev/null || true
|
|
||||||
|
|
||||||
# Install public key BEFORE abuild (fixes UNTRUSTED signature)
|
# Install public key BEFORE abuild (fixes UNTRUSTED signature)
|
||||||
cp /home/builduser/.abuild/*.rsa.pub /etc/apk/keys/ 2>/dev/null || true
|
cp /home/builduser/.abuild/*.rsa.pub /etc/apk/keys/ 2>/dev/null || true
|
||||||
|
|
||||||
# Run abuild as builduser in /home/builduser where APKBUILD exists
|
# Run abuild as builduser in workspace directory
|
||||||
# Use || true because index update may fail but APK is still created
|
su - builduser -c "cd $WORKSPACE_DIR && abuild checksum && abuild -d"
|
||||||
su - builduser -c "cd /home/builduser && abuild checksum && abuild -d -F" || true
|
|
||||||
|
|
||||||
# Copy APK from builduser packages to releases
|
# Copy APK from builduser packages to releases
|
||||||
|
# Note: abuild outputs to /home/builduser/packages/builduser/x86_64/ not /home/builduser/packages/home/x86_64/
|
||||||
mkdir -p releases
|
mkdir -p releases
|
||||||
cp /home/builduser/packages/x86_64/*.apk releases/ 2>/dev/null || cp /home/builduser/packages/*.apk releases/ 2>/dev/null || find /home/builduser/packages -name "*.apk" -exec cp {} releases/ \; 2>/dev/null || true
|
cp /home/builduser/packages/builduser/x86_64/*.apk releases/ 2>/dev/null || find /home/builduser/packages -name "*.apk" -exec cp {} releases/ \; 2>/dev/null || true
|
||||||
else
|
else
|
||||||
|
cd "$WORKSPACE_DIR"
|
||||||
abuild checksum
|
abuild checksum
|
||||||
abuild -F -r
|
abuild -r
|
||||||
|
cd -
|
||||||
|
mkdir -p releases
|
||||||
cp ~/packages/x86_64/*.apk releases/ 2>/dev/null || cp ~/packages/*.apk releases/ 2>/dev/null || true
|
cp ~/packages/x86_64/*.apk releases/ 2>/dev/null || cp ~/packages/*.apk releases/ 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy to releases directory (fallback for non-root builds)
|
|
||||||
echo ""
|
|
||||||
echo "Copying package to releases/..."
|
|
||||||
mkdir -p releases
|
|
||||||
cp ~/packages/x86_64/*.apk releases/ 2>/dev/null || cp ~/packages/*.apk releases/ 2>/dev/null || find ~/packages -name "*.apk" -exec cp {} releases/ \; 2>/dev/null || true
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Build Complete ==="
|
echo "=== Build Complete ==="
|
||||||
echo "Package: releases/linux-patch-api-*.apk"
|
echo "Package: releases/linux-patch-api-*.apk"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Install with:"
|
echo "Install with:"
|
||||||
echo " sudo apk add --allow-unstable ./releases/linux-patch-api-*.apk"
|
echo " sudo apk add ./releases/linux-patch-api-*.apk"
|
||||||
|
|||||||
@ -14,6 +14,11 @@ if ! command -v makepkg &> /dev/null; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Clean stale packages from previous builds
|
||||||
|
rm -f releases/linux-patch-api-*.pkg.tar.zst 2>/dev/null || true
|
||||||
|
rm -f /home/builduser/repo/releases/linux-patch-api-*.pkg.tar.zst 2>/dev/null || true
|
||||||
|
rm -f /home/builduser/repo/*.pkg.tar.zst 2>/dev/null || true
|
||||||
|
|
||||||
# Build release binary
|
# Build release binary
|
||||||
if [ -z "$SKIP_CARGO_BUILD" ]; then
|
if [ -z "$SKIP_CARGO_BUILD" ]; then
|
||||||
echo "Building release binary..."
|
echo "Building release binary..."
|
||||||
@ -22,43 +27,68 @@ else
|
|||||||
echo "Skipping cargo build (SKIP_CARGO_BUILD is set)"
|
echo "Skipping cargo build (SKIP_CARGO_BUILD is set)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create package directory
|
# Create package directory structure
|
||||||
PKGDIR=$(pwd)/arch-package
|
PKGDIR=$(pwd)/arch-package
|
||||||
|
rm -rf "$PKGDIR"
|
||||||
mkdir -p "$PKGDIR"/usr/bin
|
mkdir -p "$PKGDIR"/usr/bin
|
||||||
mkdir -p "$PKGDIR"/etc/linux_patch_api
|
mkdir -p "$PKGDIR"/etc/linux_patch_api/certs
|
||||||
mkdir -p "$PKGDIR"/usr/lib/systemd/system
|
mkdir -p "$PKGDIR"/usr/lib/systemd/system
|
||||||
|
mkdir -p "$PKGDIR"/var/lib/linux_patch_api
|
||||||
|
mkdir -p "$PKGDIR"/var/log/linux_patch_api
|
||||||
|
|
||||||
# Copy files
|
# Copy binary
|
||||||
|
chmod 755 target/release/linux-patch-api
|
||||||
cp target/release/linux-patch-api "$PKGDIR"/usr/bin/
|
cp target/release/linux-patch-api "$PKGDIR"/usr/bin/
|
||||||
chmod 755 "$PKGDIR"/usr/bin/linux-patch-api
|
|
||||||
|
# Copy systemd service
|
||||||
cp configs/linux-patch-api.service "$PKGDIR"/usr/lib/systemd/system/
|
cp configs/linux-patch-api.service "$PKGDIR"/usr/lib/systemd/system/
|
||||||
cp configs/config.yaml.example "$PKGDIR"/etc/linux_patch_api/config.yaml
|
|
||||||
cp configs/whitelist.yaml.example "$PKGDIR"/etc/linux_patch_api/whitelist.yaml
|
# Copy example configs (as .example files - install script creates live configs)
|
||||||
|
cp configs/config.yaml.example "$PKGDIR"/etc/linux_patch_api/config.yaml.example
|
||||||
|
cp configs/whitelist.yaml.example "$PKGDIR"/etc/linux_patch_api/whitelist.yaml.example
|
||||||
|
|
||||||
|
# Copy install script to current directory (must be co-located with PKGBUILD)
|
||||||
|
cp configs/linux-patch-api.install linux-patch-api.install
|
||||||
|
|
||||||
|
# Get version from Cargo.toml
|
||||||
|
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
||||||
|
|
||||||
# Create PKGBUILD with quoted heredoc to prevent $pkgdir expansion
|
# Create PKGBUILD with quoted heredoc to prevent $pkgdir expansion
|
||||||
# $pkgdir must be literal for makepkg to expand at runtime
|
# $pkgdir must be literal for makepkg to expand at runtime
|
||||||
echo "Creating PKGBUILD..."
|
echo "Creating PKGBUILD..."
|
||||||
cat > PKGBUILD << 'EOF'
|
cat > PKGBUILD << 'EOF'
|
||||||
pkgname=linux-patch-api
|
pkgname=linux-patch-api
|
||||||
pkgver=1.0.0
|
pkgver=VERSION_PLACEHOLDER
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Secure remote package management API for Linux systems"
|
pkgdesc="Secure remote package management API for Linux systems"
|
||||||
url="https://gitea.moon-dragon.us/echo/linux_patch_api"
|
url="https://gitea.moon-dragon.us/echo/linux_patch_api"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=('systemd')
|
depends=('systemd')
|
||||||
|
install=linux-patch-api.install
|
||||||
|
source=()
|
||||||
|
backup=(
|
||||||
|
'etc/linux_patch_api/config.yaml'
|
||||||
|
'etc/linux_patch_api/whitelist.yaml'
|
||||||
|
)
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
cp -r /home/builduser/repo/arch-package/* "$pkgdir"/
|
# Use $startdir because arch-package is co-located with PKGBUILD, not in sources
|
||||||
|
cp -r "$startdir"/arch-package/* "$pkgdir"/
|
||||||
|
|
||||||
|
# Ensure directories exist with proper structure
|
||||||
|
mkdir -p "$pkgdir"/etc/linux_patch_api/certs
|
||||||
|
mkdir -p "$pkgdir"/var/lib/linux_patch_api
|
||||||
|
mkdir -p "$pkgdir"/var/log/linux_patch_api
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Create .SRCINFO
|
# Replace version placeholder with actual version
|
||||||
echo "Creating .SRCINFO..."
|
sed -i "s/VERSION_PLACEHOLDER/$VERSION/" PKGBUILD
|
||||||
|
|
||||||
|
echo "PKGBUILD version: $VERSION"
|
||||||
|
|
||||||
# Build package
|
# Build package
|
||||||
echo "Building Arch package..."
|
|
||||||
|
|
||||||
# For CI environments where we may run as root
|
# For CI environments where we may run as root
|
||||||
if [ "$(id -u)" = "0" ]; then
|
if [ "$(id -u)" = "0" ]; then
|
||||||
echo "Running as root - creating build user for makepkg..."
|
echo "Running as root - creating build user for makepkg..."
|
||||||
@ -69,12 +99,22 @@ if [ "$(id -u)" = "0" ]; then
|
|||||||
cp -r . /home/builduser/repo/
|
cp -r . /home/builduser/repo/
|
||||||
chown -R builduser:builduser /home/builduser/repo/
|
chown -R builduser:builduser /home/builduser/repo/
|
||||||
|
|
||||||
|
# Create source tarball for makepkg
|
||||||
|
# makepkg expects sources to be in $srcdir after extraction
|
||||||
|
# We create a tarball of arch-package so %autosetup or prepare can extract it
|
||||||
|
cd /home/builduser/repo
|
||||||
|
|
||||||
su - builduser -c "cd /home/builduser/repo && makepkg --printsrcinfo > .SRCINFO"
|
su - builduser -c "cd /home/builduser/repo && makepkg --printsrcinfo > .SRCINFO"
|
||||||
su - builduser -c "cd /home/builduser/repo && makepkg -f --noconfirm"
|
su - builduser -c "cd /home/builduser/repo && makepkg -f --noconfirm"
|
||||||
|
|
||||||
# Copy package to releases
|
# Copy package to releases
|
||||||
|
mkdir -p /home/builduser/repo/releases
|
||||||
|
cp /home/builduser/repo/*.pkg.tar.zst /home/builduser/repo/releases/ 2>/dev/null || true
|
||||||
|
cd -
|
||||||
|
|
||||||
|
# Copy releases back to original directory
|
||||||
mkdir -p releases
|
mkdir -p releases
|
||||||
cp /home/builduser/repo/*.pkg.tar.zst releases/
|
cp /home/builduser/repo/releases/*.pkg.tar.zst releases/ 2>/dev/null || true
|
||||||
else
|
else
|
||||||
makepkg --printsrcinfo > .SRCINFO
|
makepkg --printsrcinfo > .SRCINFO
|
||||||
makepkg -f --noconfirm
|
makepkg -f --noconfirm
|
||||||
|
|||||||
93
build-rpm.sh
Executable file → Normal file
93
build-rpm.sh
Executable file → Normal file
@ -2,51 +2,128 @@
|
|||||||
# Build RPM Package for RHEL/CentOS/Fedora
|
# Build RPM Package for RHEL/CentOS/Fedora
|
||||||
# Run on: RHEL 8/9, CentOS 8/9, Fedora 38+
|
# Run on: RHEL 8/9, CentOS 8/9, Fedora 38+
|
||||||
# Designed for native Gitea Actions runner execution
|
# Designed for native Gitea Actions runner execution
|
||||||
|
#
|
||||||
|
# Build pattern: Pre-build binary BEFORE creating tarball (like Alpine/Arch)
|
||||||
|
# The binary is included in the source tarball so rpmbuild's %build
|
||||||
|
# section is a no-op. This avoids PATH issues where rpmbuild can't find
|
||||||
|
# cargo installed via rustup.
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "=== Linux Patch API - RPM Build Script ==="
|
echo "=== Linux Patch API - RPM Build Script ==="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Source cargo environment (for rustup-installed toolchain in CI)
|
||||||
|
if [ -f "$HOME/.cargo/env" ]; then
|
||||||
|
. "$HOME/.cargo/env"
|
||||||
|
fi
|
||||||
|
|
||||||
# Check if running on RPM-based system
|
# Check if running on RPM-based system
|
||||||
if ! command -v rpmbuild &> /dev/null; then
|
if ! command -v rpmbuild &> /dev/null; then
|
||||||
echo "Installing RPM build tools..."
|
echo "Installing RPM build tools..."
|
||||||
if command -v dnf &> /dev/null; then
|
if command -v dnf &> /dev/null; then
|
||||||
dnf install -y rpm-build cargo rust gcc systemd-devel
|
dnf install -y rpm-build
|
||||||
elif command -v yum &> /dev/null; then
|
elif command -v yum &> /dev/null; then
|
||||||
yum install -y rpm-build cargo rust gcc systemd-devel
|
yum install -y rpm-build
|
||||||
else
|
else
|
||||||
echo "Error: Cannot install rpm-build. Please install manually."
|
echo "Error: Cannot install rpm-build. Please install manually."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Get version from Cargo.toml
|
||||||
|
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "Error: Could not determine version from Cargo.toml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Building version: $VERSION"
|
||||||
|
|
||||||
|
# Remove stale RPM artifacts to prevent uploading cached/old packages
|
||||||
|
echo "Cleaning stale RPM artifacts..."
|
||||||
|
rm -f ~/rpmbuild/RPMS/x86_64/linux-patch-api-*.rpm
|
||||||
|
rm -f releases/linux-patch-api-*.rpm
|
||||||
|
|
||||||
|
# Build release binary (skip if already built by CI)
|
||||||
|
if [ -z "$SKIP_CARGO_BUILD" ]; then
|
||||||
|
echo "Building release binary..."
|
||||||
|
cargo build --release
|
||||||
|
else
|
||||||
|
echo "Skipping cargo build (SKIP_CARGO_BUILD is set)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify binary exists
|
||||||
|
if [ ! -f "target/release/linux-patch-api" ]; then
|
||||||
|
echo "Error: Pre-built binary not found at target/release/linux-patch-api"
|
||||||
|
echo "Run 'cargo build --release' first or unset SKIP_CARGO_BUILD"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Setup RPM build directory structure
|
# Setup RPM build directory structure
|
||||||
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||||
|
|
||||||
# Create source tarball (required by %autosetup in spec file)
|
# Create source tarball with pre-built binary included
|
||||||
echo "Creating source tarball..."
|
# (required by %autosetup in spec file)
|
||||||
VERSION="1.0.0"
|
echo "Creating source tarball with pre-built binary..."
|
||||||
TMPDIR=$(mktemp -d)
|
TMPDIR=$(mktemp -d)
|
||||||
mkdir -p "$TMPDIR/linux-patch-api-${VERSION}"
|
mkdir -p "$TMPDIR/linux-patch-api-${VERSION}"
|
||||||
# Copy files excluding unwanted directories using find
|
|
||||||
|
# Copy files excluding unnecessary directories
|
||||||
cp -r . "$TMPDIR/linux-patch-api-${VERSION}/"
|
cp -r . "$TMPDIR/linux-patch-api-${VERSION}/"
|
||||||
|
|
||||||
|
# Remove unnecessary directories from tarball
|
||||||
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/target"
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/target"
|
||||||
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/.git"
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/.git"
|
||||||
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/releases"
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/releases"
|
||||||
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/.github"
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/.github"
|
||||||
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/debian"
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/debian"
|
||||||
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/arch-package"
|
||||||
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/.abuild"
|
||||||
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/apk-package"
|
||||||
|
rm -rf "$TMPDIR/linux-patch-api-${VERSION}/.a0proj"
|
||||||
|
|
||||||
|
# Re-create target/release with just the pre-built binary
|
||||||
|
# This is the key change: binary is in the tarball so %build is a no-op
|
||||||
|
mkdir -p "$TMPDIR/linux-patch-api-${VERSION}/target/release"
|
||||||
|
cp target/release/linux-patch-api "$TMPDIR/linux-patch-api-${VERSION}/target/release/"
|
||||||
|
chmod 755 "$TMPDIR/linux-patch-api-${VERSION}/target/release/linux-patch-api"
|
||||||
|
|
||||||
tar -czf ~/rpmbuild/SOURCES/linux-patch-api-${VERSION}.tar.gz -C "$TMPDIR" "linux-patch-api-${VERSION}"
|
tar -czf ~/rpmbuild/SOURCES/linux-patch-api-${VERSION}.tar.gz -C "$TMPDIR" "linux-patch-api-${VERSION}"
|
||||||
rm -rf "$TMPDIR"
|
rm -rf "$TMPDIR"
|
||||||
|
|
||||||
# Copy spec file
|
# Prepare spec file with dynamic version
|
||||||
echo "Preparing spec file..."
|
echo "Preparing spec file..."
|
||||||
cp linux-patch-api.spec ~/rpmbuild/SPECS/
|
sed "s/VERSION_PLACEHOLDER/$VERSION/" linux-patch-api.spec > ~/rpmbuild/SPECS/linux-patch-api.spec
|
||||||
|
|
||||||
|
# Verify VERSION replacement succeeded
|
||||||
|
if grep -q 'VERSION_PLACEHOLDER' ~/rpmbuild/SPECS/linux-patch-api.spec; then
|
||||||
|
echo "Error: VERSION_PLACEHOLDER not replaced in spec file!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Spec file version verified: $VERSION"
|
||||||
|
|
||||||
# Build RPM
|
# Build RPM
|
||||||
echo "Building RPM package..."
|
echo "Building RPM package..."
|
||||||
rpmbuild -ba ~/rpmbuild/SPECS/linux-patch-api.spec
|
rpmbuild -ba ~/rpmbuild/SPECS/linux-patch-api.spec
|
||||||
|
|
||||||
|
# Verify RPM was actually built
|
||||||
|
RPM_FILE=$(ls ~/rpmbuild/RPMS/x86_64/linux-patch-api-${VERSION}-*.rpm 2>/dev/null | head -1)
|
||||||
|
if [ -z "$RPM_FILE" ]; then
|
||||||
|
echo "Error: RPM package not found after build!"
|
||||||
|
echo "Looking for: ~/rpmbuild/RPMS/x86_64/linux-patch-api-${VERSION}-*.rpm"
|
||||||
|
ls -la ~/rpmbuild/RPMS/x86_64/ 2>/dev/null || echo "Directory empty or missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify RPM contains the correct version
|
||||||
|
RPM_VERSION=$(rpm -qp --queryformat '%{VERSION}' "$RPM_FILE" 2>/dev/null || true)
|
||||||
|
echo "RPM built: $RPM_FILE"
|
||||||
|
echo "RPM version: $RPM_VERSION"
|
||||||
|
if [ "$RPM_VERSION" != "$VERSION" ]; then
|
||||||
|
echo "Error: RPM version ($RPM_VERSION) does not match expected version ($VERSION)!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Copy to releases directory
|
# Copy to releases directory
|
||||||
echo ""
|
echo ""
|
||||||
echo "Copying package to releases/..."
|
echo "Copying package to releases/..."
|
||||||
|
|||||||
33
configs/certs/README.md
Normal file
33
configs/certs/README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Development Certificates
|
||||||
|
|
||||||
|
**⚠️ Private keys are NOT committed to version control.**
|
||||||
|
|
||||||
|
This directory is used for local development certificates only. Private key
|
||||||
|
files (`*.key`, `*.key.pem`) are excluded from git via `.gitignore`.
|
||||||
|
|
||||||
|
## Generating Development Certificates
|
||||||
|
|
||||||
|
Run the generation script from the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/generate-dev-certs.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates:
|
||||||
|
- `ca.pem` / `ca.key.pem` — Internal CA certificate and key
|
||||||
|
- `server.pem` / `server.key.pem` — Server certificate and key
|
||||||
|
- `client001.pem` / `client001.key.pem` — Client certificate and key
|
||||||
|
- `tests/e2e/certs/` — E2E test certificates
|
||||||
|
|
||||||
|
## Production Deployments
|
||||||
|
|
||||||
|
Production deployments should use certificates issued by the organisation's
|
||||||
|
internal CA. The `install.sh` script and systemd unit handle production
|
||||||
|
certificate paths at `/etc/linux_patch_api/certs/`.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **Never commit private keys** (`*.key`, `*.key.pem`) to version control
|
||||||
|
- Private keys must have `0600` permissions in production
|
||||||
|
- The `gitleaks` CI check scans for accidentally committed secrets
|
||||||
|
- See `SECURITY_FINDINGS_REPORT.md` and `SECURITY.md` for full details
|
||||||
@ -1,54 +0,0 @@
|
|||||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
|
||||||
MIIJtTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQOJY6BZQMTvXCEBl6
|
|
||||||
Rv+0fQICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEJwoQf7hSIurtiBM
|
|
||||||
nm+YEhwEgglQiyNTxNNkeZ8hikGe3m+2cfXtAituVJYgs4V+bgTJXnrJVaFyPoRw
|
|
||||||
Nde/m9vJU4EGaRwS7Sb89XsjFK+Qbc6+2mvBqhkoBIjXBjYsiqNlLStLUIf1IPdU
|
|
||||||
HHHxrOSnkNIXaiEojEIb4SLHYsmwSGbHPCmr+sIvzRDo/tuc0ugTDJFoS4lhDy6r
|
|
||||||
VsPuDsV3XTnyPTWlHgROr1DmwqhTa87PXkpiomFxw1/Jy+2D0tQ+PuhTlGh87q2r
|
|
||||||
T+ZHOLf8GLMKxKL7Elup/ugT+qfK0FekKJV+1Pw8EL+vWmJMLvhk+tlO9b4WxOlD
|
|
||||||
UiW98mf6ospzpbmVf/AkaI5mkeAaikp7XMim57bUDBbNT3YQWBgwjVR01n7nCKk/
|
|
||||||
2hYcaDEJGv6KuU3utlVhSIF1OuIJR42q7AJuOmM22yAHJ7KOkcoXFcNsJuVqAeDc
|
|
||||||
BcVMMEgrHuLdnlHTzUy+0ETFAiTTQE/8+RYHPi0t0LOgalJBVZN4kR8OvCXiTrfw
|
|
||||||
J6Ex7BvRM7MisS9lNfzCoMaN84/tEdwVTc0USEYvY4mQGV5xINN9ehNgfnw5leMW
|
|
||||||
n0+oFtNXeslV84xVVz/X0pSD5G4NiyOAVBD44jHowRHJqPMaWs38xFVNStjCyThX
|
|
||||||
L6lq8ZcFMOLzswvPjBKpb+XlSwETDMGEXbhiOop/CYT2YLwjWH7Vu7rZ1kL553pp
|
|
||||||
DUOmGRgSfeabKZeyePxh3Pz5uqmKtuX3WGmyeCX/bneDXVASJwCAe+HGFC0cy5JL
|
|
||||||
8c82b28AdijALlqcEi4S4xsSyL3eMGWlwedfMKN1JdkxrqqWDworfuK+vMUvBa55
|
|
||||||
GZKlZaYG7rs6nim3XgabnIS+bx08QteMHM9KnEGHggkUd4crSWU9vno+Fkkus1kG
|
|
||||||
RqO9hSya/s3CqXGyvK4VTB+ThkAHtMdcFg2fbUtgFW4JluysgosFNCBoI1DYlHqF
|
|
||||||
4O9H6zf90sjX02m4fxt/zl4sRffMbbpxk1gPlahxR/smPSu6LSpmgKMpHwQZKpHc
|
|
||||||
r4ZYSC2ITVC+wb3+LxGDcZvoWFP6CKpcqJ3u2OwGHrSroA9gz6BMOLhhJIqClaDe
|
|
||||||
qYLpCZ2GKUl6GZApEd9mQtrGZyG9qfn4i0+jysUCYq4WRVMJhIitXdmLYUjM+6mP
|
|
||||||
ZUl8P0KIdMjxLf/be2RofokCF8/PmmjfMdxXSwQ4v3wqx8/GvmuY/gM0nPnC4jMQ
|
|
||||||
CgiJNpnOMLSMEM2c3w32206zjSMPYfR7JsB4d+bp3UMsqGfvv6xOShky+XNU/2ft
|
|
||||||
AAeMmvvm39RNePoq9owFDFiID2QEmI61ZSOK2ndXbueX0pslYKXdjgtnygL2w19t
|
|
||||||
BxshEfXGxqu7ImztyO/TLhY5Szu9E+zwaJzGhSR4vPq3emO3M3dGRa/6RbbXB5BC
|
|
||||||
N9efMeIlEi/mVtdnu0jdgbrR7TbFCOrjhdrDgmEo4DKX4BEQZdeHpO/czF6gz4i8
|
|
||||||
bGtMGjYKL3xpYfk0yhycx5Q2iZQFbt3W90YPHz9SLrv4U3rJy7OQQmJ4upsaz0a8
|
|
||||||
lFIKzsGTczojwuYBVG7YNGqyxQtDLxQsjhK1j11pGBNKqGeFxOpvzw694XgLv/a8
|
|
||||||
785B7c66OJD1H04wndFeR/ruRZpMda8Cw6gwNkzFiWZ1SwwIeqg364wmvhB/VhVC
|
|
||||||
H6Pr9k4jgFYimM7DgGdrf1+RKOo7bDpyVPAOXNzmPINikxvZLT+ps++usXH4xOdi
|
|
||||||
YiCq2DR7zjF301ojyAuP4K10c8p3FAu8SerJ+lS9HRLJQ0z4cXnkbtAvIMs3C88Z
|
|
||||||
9bNWCs8bRH54HJiBgHVKkx00A0rAftMKzn3mKBcKnXvbfL/Qb+sKun0Z6hzWkld5
|
|
||||||
yEsDnI4B6gxUk+R78kmc2xIzorHHYjdmq07rITKk23QPHgDrrr26NppNMRGVds/9
|
|
||||||
DpV5yethMOGVNu8njiqU98uK4rQv1r7YSOvNVGkpvDKHjSDqe+N4bal5tQEnLEXw
|
|
||||||
GzdNB/ECJm0ij/98W8I+AzyGOVmoa0XqJKNfQQGXDigSM3HeYZhPu2JotleddkCT
|
|
||||||
/l0qpMDlxeTsf6Uhe/47I5iirJCUO6G7RSV9bOh2Pmnbp7PQPkFW79WOf8MPCnCL
|
|
||||||
XyR4GkyCQ4FjTMLIiDkeV9ReBykuNWohLN6NTwSrJZ5s/032oF+I0WZ5vbePL2zP
|
|
||||||
z/0X6fKTpVeyT1FMIFE+XH0v06awsq0gG1FlrMMQEO3xLPfF/NNqdJN49lD+AnPh
|
|
||||||
m/0b/pJ4+NwlEWLsQUdkGAyYD/ZgMHDZQryFxCwrBAFLRtj4NzaaDT5QOgxZUIbQ
|
|
||||||
VIpPZtAahy1463Pb3Oc7zIiuf7v1RvWipN05QtgepREXNJkOOVXjP0Nyrq98fS2T
|
|
||||||
oZNZMqr47YeyErztUudKMZ1MCT1jb20y4+y2OSG5lDbKS2gQWo0EIRveFT82QSQa
|
|
||||||
12gmQMVhAdoRUYBqdQoo98nLix4JftgKYc691pf9gQJIJ8P48uOQEIW6nNc8eXF6
|
|
||||||
L7QyYidqrqnSzpwRwTv9+LmiXm52lg4Ft3aq7GKq237Mz8Thx3YXamaFBdMYSu3p
|
|
||||||
5/nNorChQSnnCEmAMdNYej94OUwun5HSTGwh1/JloHCZUMsOqJ20xn3YRQS3E0Vo
|
|
||||||
uF6aqbZbKbZbJrY+NBrQae0onUNQLbFUX56rMXT9fJJmt/KeFKtI6kKWBs41vp0N
|
|
||||||
TqOORrtkwyu/AU3qWg4iUINRqFjI76MzzH1XZ9A/2qokAZduHgDoFGcOKkpRFT/9
|
|
||||||
F6P1SXfoeE8DtUpBhu5XlJyIwcWANkstATrXxyZLA7IdLLgSPZXSwAWxLwCN0ypM
|
|
||||||
Jnscfvkr2SW8OwpJ8/mA/SX88ZC28Uvp1egsgnM1k9Z7Oinxgk9a7LNUv0qxBc7k
|
|
||||||
SuooMBJvuiqHOzTr3IJvpCkZykvbnYbDgtypxVOeWO257yxer/ora9NVX84pfprU
|
|
||||||
7JbOpBGMY6FUAcONmBYikGyGeNsF9zsPcdSOdUKP7tlrncYughORsb/FkNq7LSbo
|
|
||||||
ll+tRCu/7Xb+VEctDQhk1fJ+ojFzC0P9duRWcskIVWFbSj6r21hzdhKOMX6bXLhA
|
|
||||||
NcNpSk3EHqDij4rMbaAqSs5W2Q4JTUJ6L0/OOy9aeckbXw/j31OdYnR5wM7nvXrH
|
|
||||||
tv89sXX/ObNJFD5uPRMPvoACv2oTsgWtm4sNkapAeiOcovPCWSroyzc=
|
|
||||||
-----END ENCRYPTED PRIVATE KEY-----
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFVzCCAz+gAwIBAgIUO/u3nWWJUG+i/cwM8o/1fkLzfbAwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwOzEZMBcGA1UEAwwQTGludXhQYXRjaEFQSSBDQTERMA8GA1UECgwISW50ZXJu
|
|
||||||
YWwxCzAJBgNVBAYTAlVTMB4XDTI2MDQwOTIwMTM1MloXDTM2MDQwNjIwMTM1Mlow
|
|
||||||
OzEZMBcGA1UEAwwQTGludXhQYXRjaEFQSSBDQTERMA8GA1UECgwISW50ZXJuYWwx
|
|
||||||
CzAJBgNVBAYTAlVTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAt+Li
|
|
||||||
R5RFcfgnm7fHHPg+csakg/C7+Pkb99mCTb+sBGodxNdlryFz3k/c6hFwUJWwfbPL
|
|
||||||
hsZo8JSxPIrXMhu8n6pDygUSqOx43dkqXURI40FfOaEkSHwYIF73eOV+qUBPTqQZ
|
|
||||||
udMc0BGYndpaLk+Lb6rKtEA4r0HkP2fLdO8wOqr68kYiMhhVP3Dw0k1JmtUY3k/k
|
|
||||||
RcBPQ7C/n+Pr4a0xdIr2TwzNyH+JOp/3oCZW5mZdfaZWXMZhObtT3a8hW0qfc/P5
|
|
||||||
3PM1C5jxTBRJiQTQlHsM6EpDS1vZLLU0R5PNRw2U7HgOPhY6iItZDN9NUNo5uGpT
|
|
||||||
5jBR3CumpkCxoGnLuV8+VBngjaovpzp245ERYYU7rox5CrHj1yybw6HuaXXqQncO
|
|
||||||
zDYJwEUINcGiSTlnWyy9iFqA3PInOtAE4YCyscKHH60CxY+/6WvE8yVgTE2SM/At
|
|
||||||
l2UmZhSDIZBMx2GUmRob9FmQCsyb9AnIytkXXBbJtX6wVi0S7TGKixYObnudb6k+
|
|
||||||
DEP/HA7BLRChR/XyDjeHNnsE/cqQeNcGOqP6UHS3rf4L3lIDCLvvKhid73C6/N8r
|
|
||||||
Mz4FvwbwMdw4MHn/WNQBe5+1xkgLLoNRHPXUFwpKcA2ev4JEchb9w9IWiuftJ9BM
|
|
||||||
zzGKlwT9rCw7A0rQMzsaaEMdCF1VPecoTyIxb2kCAwEAAaNTMFEwHQYDVR0OBBYE
|
|
||||||
FGfp3Y5/keT0TeQin1GfU3LalfuWMB8GA1UdIwQYMBaAFGfp3Y5/keT0TeQin1Gf
|
|
||||||
U3LalfuWMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAG7Tkj2S
|
|
||||||
SisI7ggjFXmferutk25v62dLeXJ9eBjnXHQkk6oMJo4TFWgLbo66gb+0mq9c7/rC
|
|
||||||
XsQY3mEnQ3lcujMXoEYGcOM7TOHENj0UX0GiQLexCSZF14IOsf0KGXvB649AhscC
|
|
||||||
N83mdk6GSP2gB8I3wGngbgCtZf/9sq4z6pXVNva+xNskWA7YidY/3pGIMkvRb21v
|
|
||||||
iTXsTGUC4U2/wjohYVLcyu36Dk2YbdAl0gY7JsNGXbT0a/zpo2aY4ogDMXe/828Q
|
|
||||||
gW2ZJWGXeJJKHOgBQw+zmBO+Zgm2vdWWBYCsJVLeVE2SE+LngJGfwgJT7tNb20e6
|
|
||||||
7UBJzhJHIcu73ODNF1TPCNIREVELC4iBXIMvoi3h8Yp7Wo6S8CGk9DY68fXSAkfb
|
|
||||||
oagvxe/rKRgljX70pRl6YOhpMVpl4yUc4BuRlfopRAIDS3AQdNyp9hvMUyj64Gan
|
|
||||||
UIkVXLoDA+7KGw/RPCtC18HWw19nmooh73cWSmGrOjtKu2L5ZsSuD3G6vnmFZaSv
|
|
||||||
HqK08pX+zv2NpYVhiE39zRQ37u9xVjNQsJ/1gnLQ3zOXyidpB8eH+1r5pR7dVjMf
|
|
||||||
wnhnLlm8nty7O0sOy2kiYp1YqosCitOgnLR1U/cgzGX6j0mHUuciY/fRyK1Yifa5
|
|
||||||
UM+xJs/yTc33DYhd4oYQHxqRkletlx2XDW9d
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@ -1 +0,0 @@
|
|||||||
790CDB9FA2002BF59B3EE88AF326CB060353D111
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE REQUEST-----
|
|
||||||
MIICeTCCAWECAQAwNDESMBAGA1UEAwwJY2xpZW50MDAxMREwDwYDVQQKDAhJbnRl
|
|
||||||
cm5hbDELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
|
|
||||||
AQC9Tida4so5qerRjEQXQQJb/W4jCsRwZg6lSvvd9qEtqWuwxX+SFfNbcpDOOZTh
|
|
||||||
+CWmmCq9v5ZO+XyO/s9xKLnyudnkT/nymB6KFN3XywfDE2iiGshNVNd5d0B4nF7e
|
|
||||||
nXeoF938GF5/ny4dkGgg+HoXXrQJ1WGjODXJsXtiiMZPI08kZL4vuYW64VojHvUf
|
|
||||||
AQdEjGqlIZzNW909g0uaQEizpZwJvH7YGvWuQDx9ywbWrs1t3hHu2ahA+myVXL3Y
|
|
||||||
C7+jAmROQ61FHW2F4swS+uRQDTEr9qQs256JCryzHCTma8IWmYqphM9wn18f5Tm7
|
|
||||||
EXw4EHCz5FVL7HPL1vZnQdsrAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAH46n
|
|
||||||
SxI9O21jO1q2cmFlZano5JWAo3eb424+3jCYgAJDBUtlzTLfdkADvttaTtAjI8sh
|
|
||||||
GqbUFsCtO4UlPD5SWFnPdUQqJ+zv1lXDyef0D694mUjgrjtdB27l9wmTnHZVwgcL
|
|
||||||
GEr8nfuhqNNjARmWUJUv629slt0RDZxGm0IXGJBrx39t31oh0q1ll4rPvd9TEiLZ
|
|
||||||
sP8r5WdC2PdFLh13J6erLkoMOOLmM/mXj1egz+ivgqo2uXDX1crBlNH0H1KM05ot
|
|
||||||
c5wJo3mzbRC/3PWLLJHKwQ6ObI88AviGEMevIw54jdz2UHXPv0aK2SSoIDr6GhUi
|
|
||||||
0OBKrqsjBII7l+w+Rw==
|
|
||||||
-----END CERTIFICATE REQUEST-----
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9Tida4so5qerR
|
|
||||||
jEQXQQJb/W4jCsRwZg6lSvvd9qEtqWuwxX+SFfNbcpDOOZTh+CWmmCq9v5ZO+XyO
|
|
||||||
/s9xKLnyudnkT/nymB6KFN3XywfDE2iiGshNVNd5d0B4nF7enXeoF938GF5/ny4d
|
|
||||||
kGgg+HoXXrQJ1WGjODXJsXtiiMZPI08kZL4vuYW64VojHvUfAQdEjGqlIZzNW909
|
|
||||||
g0uaQEizpZwJvH7YGvWuQDx9ywbWrs1t3hHu2ahA+myVXL3YC7+jAmROQ61FHW2F
|
|
||||||
4swS+uRQDTEr9qQs256JCryzHCTma8IWmYqphM9wn18f5Tm7EXw4EHCz5FVL7HPL
|
|
||||||
1vZnQdsrAgMBAAECggEAAL6K9Cq4oA4Pv/kbRskIdNNct38SLiOZnn8UVWfbvj9A
|
|
||||||
BpC+KZllhwoAyxsrf3ZnV8B45WNBxwERy2bpdwzznrsl4uGZfXg9+Au6HmiB89JF
|
|
||||||
x27vp1LUZYphluZDZiGT+x7kIO9swT3Eh78pvDqMU/S+VeTaThHa5VFHx23aPeKR
|
|
||||||
utc/dW1+1rT2rGZXTEF86xQkHQaKSYa4MPpxAhZ/Azc28sYtcGeJ6NEjqQyDEYHn
|
|
||||||
hlFLBs9RpvyYkmMx+s0xkdtEE9+v2cTnw04MseE/MMBzSS4Y3EBFmVSJvvpKmyox
|
|
||||||
DibwJtxhMa8atT5LOroBpPwYbmelAKbF85yxHtRE4QKBgQDjk/rkTOud3mUTiKt4
|
|
||||||
+26YgtpcEjTJ7Rgiq8F0McveRUnGRGwI2ML+nQZ0mBsroCdQjqBbyIGVYY9EZJfB
|
|
||||||
pRYLGHEUHcS94mkwpXyGZXzNwjKPo6bmOh3dLOO4J1fgIyBx1UIKS8HLaNR+gg5Q
|
|
||||||
N6iAvkiDj5Ucqy5+iCNjJhQbRwKBgQDU8ojlhW4Cc9ITQP2Xjlg4eymxoRT9XrAC
|
|
||||||
6ebWoDK2q9uLPPPzXkKQzRM7ydOBZR9EgNknwQyfQpXFVrB+gn27o2A3iKtvacXQ
|
|
||||||
/He04/fVPdWYF8t4su4rMYVCbl+aOwCdeFfGwFOP45oo0/eEH/ys/64I6UQEKNk9
|
|
||||||
oXnNSezq/QKBgBv3tZ+U7GfMSvOpmhkWHTNU8WzbN+2Q26R3IyEadYltTnG1Oumj
|
|
||||||
aeNMfNybTMuBtRMrU/2zmGk5QhgPnK7JkPnwGQV12xXS20aFL9Z8ZmgK85e/buVg
|
|
||||||
QwdJWvroqt36syQKJ0GIqdpLmcGqTgQBsw2PVO4GGTcaum4GYQLwTQxFAoGAZpED
|
|
||||||
GvnviMLcdmWhP3RSTbIU3PenMnp+8IhUpR+4DYAtWJ1dKuVFzpTYJL4LX5GjQ82D
|
|
||||||
ysATIkph9RDSJb0Ybl48o8LyP9GEdCqGRdxfrJgB3yXm3RXh3XAWrW6YIaM1oqMq
|
|
||||||
NBLCrNWFlRCzcTIu8+yamLQyDIbYS/UQw65NrMkCgYEAjM5Z6XJ3FuRjnEZaV6V4
|
|
||||||
evz0TyHTpHnNAKx1NRzut4wN684X81l1IgUAVp2xkbiYK/V/F2qXWAQjuA3ucyN3
|
|
||||||
svnXIsQqhnBilkcDbQg5TZtaIk58IDzENXF8TAtPQiAD478AyBcfzMrtqLhKgaBu
|
|
||||||
P7wqdvyaMVPLek9tuUINQ4o=
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIEPzCCAiegAwIBAgIUeQzbn6IAK/WbPuiK8ybLBgNT0REwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwOzEZMBcGA1UEAwwQTGludXhQYXRjaEFQSSBDQTERMA8GA1UECgwISW50ZXJu
|
|
||||||
YWwxCzAJBgNVBAYTAlVTMB4XDTI2MDQwOTIwMTQwM1oXDTI3MDQwOTIwMTQwM1ow
|
|
||||||
NDESMBAGA1UEAwwJY2xpZW50MDAxMREwDwYDVQQKDAhJbnRlcm5hbDELMAkGA1UE
|
|
||||||
BhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9Tida4so5qerR
|
|
||||||
jEQXQQJb/W4jCsRwZg6lSvvd9qEtqWuwxX+SFfNbcpDOOZTh+CWmmCq9v5ZO+XyO
|
|
||||||
/s9xKLnyudnkT/nymB6KFN3XywfDE2iiGshNVNd5d0B4nF7enXeoF938GF5/ny4d
|
|
||||||
kGgg+HoXXrQJ1WGjODXJsXtiiMZPI08kZL4vuYW64VojHvUfAQdEjGqlIZzNW909
|
|
||||||
g0uaQEizpZwJvH7YGvWuQDx9ywbWrs1t3hHu2ahA+myVXL3YC7+jAmROQ61FHW2F
|
|
||||||
4swS+uRQDTEr9qQs256JCryzHCTma8IWmYqphM9wn18f5Tm7EXw4EHCz5FVL7HPL
|
|
||||||
1vZnQdsrAgMBAAGjQjBAMB0GA1UdDgQWBBStUuU9Si2VnMMQ4VkYyf2pttMhezAf
|
|
||||||
BgNVHSMEGDAWgBRn6d2Of5Hk9E3kIp9Rn1Ny2pX7ljANBgkqhkiG9w0BAQsFAAOC
|
|
||||||
AgEAqWQEAwpW45LWprkr4zpz66azUVkc2I/kuNWLiDEw9Ex4i/5e+ND6Ia7Ayk+T
|
|
||||||
j3rodJA1rn64gJZOzABTb3mpWwNH/DxjF/XGohixl/kn81sNCydimc3qOKL5joUb
|
|
||||||
PDtK9QLTCJmGsYk5lV9K89pR7kBR2rXD70d1GM6KjyBeknEH4oA9/BqMYL5DkeHu
|
|
||||||
v2QWYoECno43eI+Ve4oow5MN/83+VhFLeayCd/JWBYjYi55tqI8QDBn7AY4UAO2C
|
|
||||||
77msurPEqaZn5OtzEW9El/M3/+bDeYfpERgYn2X7bw0oOUZw8g5L9dfc1UxjGY8J
|
|
||||||
NPJAXUKtsDBKzN8nlvrCVmHVrR19vquH7qfh/aKu58MGu3Ovzjz57T/gooi2wmnY
|
|
||||||
4+NJDXZ7ncc7T+40svi7tbLA7MgExuGM+pq/Bxn/PHLPbhyyp7p8EPUFf5KIiqr5
|
|
||||||
GiWL1re8gfe8CAxKDKs5ERtexgoldY1TsMbQ6wjP59rRN3ZbUBGtPsi8bKTEZBpo
|
|
||||||
cM+9bg44ndODpoB8B9NKYCU/n3Uvs+mZMYtAAkLiCUrYplIiCSvUrcDOQWVn1CD1
|
|
||||||
WbuvTTtlIi55NMUi1pvgaFi0PW6Gfin1wRHjt3iLU/i+3Q/b0V+pEL7wgf5bd3U5
|
|
||||||
F6xxNMRjNh1kbZVR0WywzPignBiK9cW+z3d9rPW2FgVoXcI=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE REQUEST-----
|
|
||||||
MIICfzCCAWcCAQAwOjEYMBYGA1UEAwwPbGludXgtcGF0Y2gtYXBpMREwDwYDVQQK
|
|
||||||
DAhJbnRlcm5hbDELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
|
||||||
ggEKAoIBAQCu+RZd6OHxdGJdI+C9rS8rowzsYF/qr3p6+Yvp9ySvBzJ20TVWQSrK
|
|
||||||
Fo1VcDgg7rkTYuRxzxehO0R7pTkxnWqLY6VAA39w5nyVzTwFPv7UovdAZx/U7hnJ
|
|
||||||
Zj7ndtxuqYk1tkx94NLjYXUCWqy48/tKQXobQh3qXeQ6HfwFSI5xpy61quQBZkJz
|
|
||||||
VAatcOv/fhn3K+TrgBaKKkXFNQ3jjKhzrH3Z0son1+GNyhHvQlvCJ+jdWDpzDvSP
|
|
||||||
XpqEDmxCQvdBdzGVAPrv3fmBMyOQFnHOTHmKtJ806jBFsYEUwnXKA4/xtaihl1OL
|
|
||||||
bz85Z6MicH1PwTo4v0Z7ngIcyoxlgX/RAgMBAAGgADANBgkqhkiG9w0BAQsFAAOC
|
|
||||||
AQEAPaBJ7ryKBUuKoUGsgb+fc9GIbGomCbWZnPFx3ZJcUZfeb4/Qi1glhe1GiiUt
|
|
||||||
np5x0cjgw5he6zd13lgylglsYrSHEJDV2MqVoHqCwFH+m+ODnZvnQkrgxW4t+JEK
|
|
||||||
wEwp0dRGLXsshDPWg5Xe/SaFBfvuCWEkWkcQ4NYwg5SOVn0TCAVy2VKmdDW1KHtf
|
|
||||||
GkqHdUiIs5FX6kXIMryQpIG6OXyJCQ3pGv+kSlfaeobnqUUASWwBAaubZBxnqTIl
|
|
||||||
Daj2End8iYQ9Fiv7z0YFxJrULSt5qhtRivmUHjSOyv0tlPs+aG9mP9j14ND2/ZIA
|
|
||||||
ihOZrIUTTxaaVL9IxIVnTt7tFw==
|
|
||||||
-----END CERTIFICATE REQUEST-----
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCu+RZd6OHxdGJd
|
|
||||||
I+C9rS8rowzsYF/qr3p6+Yvp9ySvBzJ20TVWQSrKFo1VcDgg7rkTYuRxzxehO0R7
|
|
||||||
pTkxnWqLY6VAA39w5nyVzTwFPv7UovdAZx/U7hnJZj7ndtxuqYk1tkx94NLjYXUC
|
|
||||||
Wqy48/tKQXobQh3qXeQ6HfwFSI5xpy61quQBZkJzVAatcOv/fhn3K+TrgBaKKkXF
|
|
||||||
NQ3jjKhzrH3Z0son1+GNyhHvQlvCJ+jdWDpzDvSPXpqEDmxCQvdBdzGVAPrv3fmB
|
|
||||||
MyOQFnHOTHmKtJ806jBFsYEUwnXKA4/xtaihl1OLbz85Z6MicH1PwTo4v0Z7ngIc
|
|
||||||
yoxlgX/RAgMBAAECggEAEkGfGdFQsdbI5Jr3uhK110G9+XPczindB7O9632D8Fc5
|
|
||||||
5rfRbtyB0HAl8wIweQ8vdFxfJZjMCGCctqH4o7qfAUg2V8WFqIwD98VgO9Pk1t7i
|
|
||||||
GXApHBhzzFXEvnXibhF2ZYpN1Nx+ZIcopEQ9vVaHo6nNScbOREPjqkSypQJ7ClSQ
|
|
||||||
vCzezzIhjeRTlttQvQv11oU/qolqVxL/GqGWtcI9I7onY5FP7qGNhnLrVJtDltcX
|
|
||||||
71mbpjKS0NquLQimcDBwgVdhH1Ie+1hYJBLYgR8vE3J4d5a21NhtCeqTIHJo5SO/
|
|
||||||
sKkZBVhD7OzP2qmQU4Hh99FK6648U6YdiBbKuumPgQKBgQDw1c5rf4jUlJaTk7wG
|
|
||||||
/p6hSaMKVsM/JcrZgKZCCLS7fJ6M9DslCPQoOWTqh5Xq8Yh0gZ+FB/mo6nexMkgJ
|
|
||||||
cpQhdBWgX6GXJhTK2M8A7FvA7IT3ZS7G4lzOFg9qsDjbKPI6F9JjqkOmeqLulJ/z
|
|
||||||
Sr9stH2lN/+hGxwrqUs26c49uQKBgQC5/ZoE5ZBty1oIu43TGDU+7kLptsP/Ifub
|
|
||||||
YOjlfJ1DrCFd7SDpL059p1c8PPjhphFi5UkIFp102OJ0higxgvo1gGnJQ6CYXvam
|
|
||||||
qvmQyG4V8MV9bVv5SMV4QvcunTxbYEawz00BfI60lWAXKKhtPOpwdWeR0lt2SNR0
|
|
||||||
zjwQm8+e2QKBgQC59o5eqWrRoy6mI8RjrkZ1CjQv7pDy+M6qplE62hgcUXzoIEpv
|
|
||||||
LXvCd5b6FdnoQbr5I4I2qdLY4LutgsLnMKc7MbTlUhKncMtLWqB0+Q1cagW+Nk4p
|
|
||||||
Wm8I3zXmTs6IRBTOUMivFrEIItge23qq1UP8v13prtTf5Nwaxq2CaIVNWQKBgC/B
|
|
||||||
ypaPS7KlkIzFe/lEMgfirhPM9i7AzxZqn+KtSMRjon23sceuef0RxviUv2NRfQ1j
|
|
||||||
yojlJbEnL560BAYSl6S9QGyJjOcTG0pYhJSEop/Hny5BsmgkI3Bp4YZ6oVDlO8GS
|
|
||||||
uTc0gIAmCvJnYjgKeDhALUPoO8v3j3YerpWlLH6hAoGAazgCnV9WSWo3WmgVo7xw
|
|
||||||
km2tt2mp7QgAAs8t3OSMvN7jC5uyRBJ+asH3ih9rDvtu4ZPwIYNEpoMkh9IKNoK+
|
|
||||||
vtbJPqs6rrzBqQMJrfnTXm6o8gxHuSMQWXe/8tSlnbvuZhH7iFRyHH2Zv3SWoOaO
|
|
||||||
pLYlvvPbeUK7Ue1jXJ8i4yE=
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIERTCCAi2gAwIBAgIUeQzbn6IAK/WbPuiK8ybLBgNT0RAwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwOzEZMBcGA1UEAwwQTGludXhQYXRjaEFQSSBDQTERMA8GA1UECgwISW50ZXJu
|
|
||||||
YWwxCzAJBgNVBAYTAlVTMB4XDTI2MDQwOTIwMTQwM1oXDTI3MDQwOTIwMTQwM1ow
|
|
||||||
OjEYMBYGA1UEAwwPbGludXgtcGF0Y2gtYXBpMREwDwYDVQQKDAhJbnRlcm5hbDEL
|
|
||||||
MAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCu+RZd
|
|
||||||
6OHxdGJdI+C9rS8rowzsYF/qr3p6+Yvp9ySvBzJ20TVWQSrKFo1VcDgg7rkTYuRx
|
|
||||||
zxehO0R7pTkxnWqLY6VAA39w5nyVzTwFPv7UovdAZx/U7hnJZj7ndtxuqYk1tkx9
|
|
||||||
4NLjYXUCWqy48/tKQXobQh3qXeQ6HfwFSI5xpy61quQBZkJzVAatcOv/fhn3K+Tr
|
|
||||||
gBaKKkXFNQ3jjKhzrH3Z0son1+GNyhHvQlvCJ+jdWDpzDvSPXpqEDmxCQvdBdzGV
|
|
||||||
APrv3fmBMyOQFnHOTHmKtJ806jBFsYEUwnXKA4/xtaihl1OLbz85Z6MicH1PwTo4
|
|
||||||
v0Z7ngIcyoxlgX/RAgMBAAGjQjBAMB0GA1UdDgQWBBTgNxkszZsl/UI2Kri5QJb8
|
|
||||||
VrHASzAfBgNVHSMEGDAWgBRn6d2Of5Hk9E3kIp9Rn1Ny2pX7ljANBgkqhkiG9w0B
|
|
||||||
AQsFAAOCAgEALx4MmEyFsmmpFS9JvKnkRi3AMn7ePRdg0nONEd735z1grnKNTjmH
|
|
||||||
PJLErX3aD4lCxqyBhyqJaCCZRF1CRkE3wWTGyXSlab9RgXHTU9AiSvopEdgSiISt
|
|
||||||
CI3X7uGqss3cERZcKLuM7JDTVdhtOouNbfwvG40hz6lm+OcQo7F3/z/boqKkFd+o
|
|
||||||
yXLDJFCVaXgslCp1+fts7aFXpqAwj7tedzB2a7M1ncTOwvP//bnYjm/FygOhj0No
|
|
||||||
4tNX2liUnfjbMqNFszxYl+ZtYYjrt23YwNPdVhF0oY2ludh16lluJHZECji2DzH0
|
|
||||||
275M5DsgQcQpZmA77px0i+piNuCoS4wFJQDeQmtp2loGHa123zJra/kAINayf0WF
|
|
||||||
S0dPAqXwBGj2WGP1uBNOLghV4MZaYuav0xWSMuTv2TW3ZsOYzYXQk0hMe7W7oIuZ
|
|
||||||
VAcaw9ZT8wAFwo+unvzGIWtxSZ3sykK6thBEo8lqRkmqDCkDE86mb6BviQj1NBSP
|
|
||||||
+KrmZJ8vuvqfr1Oav/7Vk5qYoNprqZand6A1hnLxS9q/JZcr0Fj+Z7OS1G3hLrjd
|
|
||||||
3oN6SdNWAVkznIBe0J+Ry29My/GniBbytJgXVi+4ROO5GGmtuDCMkFqOZcm6f8BW
|
|
||||||
faPQWiWB5EY6ZuqgLgydGQ3qf1a5b8z1EzmiDZf5qRUdfddOCsljgiw=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@ -14,7 +14,7 @@ tls:
|
|||||||
ca_cert: "/etc/linux_patch_api/certs/ca.pem"
|
ca_cert: "/etc/linux_patch_api/certs/ca.pem"
|
||||||
server_cert: "/etc/linux_patch_api/certs/server.pem"
|
server_cert: "/etc/linux_patch_api/certs/server.pem"
|
||||||
server_key: "/etc/linux_patch_api/certs/server.key"
|
server_key: "/etc/linux_patch_api/certs/server.key"
|
||||||
min_tls_version: "1.3"
|
# TLS 1.3 is the only supported version (hardcoded, not configurable)
|
||||||
|
|
||||||
# Job Configuration
|
# Job Configuration
|
||||||
jobs:
|
jobs:
|
||||||
@ -44,3 +44,30 @@ package_manager:
|
|||||||
# Primary backend (auto-detected if not specified)
|
# Primary backend (auto-detected if not specified)
|
||||||
# Options: apt, dnf, yum, apk, pacman
|
# Options: apt, dnf, yum, apk, pacman
|
||||||
backend: "auto"
|
backend: "auto"
|
||||||
|
|
||||||
|
# Enrollment Configuration (optional)
|
||||||
|
# Uncomment and configure for self-enrollment with linux_patch_manager
|
||||||
|
# enrollment:
|
||||||
|
# # URL of the enrollment manager for polling status updates
|
||||||
|
# manager_url: "https://manager.example.com/enroll"
|
||||||
|
# # Authentication token for enrollment polling requests
|
||||||
|
# polling_token: "your-enrollment-token-here"
|
||||||
|
# # How often to poll the manager in seconds (default: 60)
|
||||||
|
# polling_interval_seconds: 60
|
||||||
|
# # Maximum number of polling attempts before giving up
|
||||||
|
# # Default: 1440 (24 hours at 60s intervals = 86400 seconds total)
|
||||||
|
# max_poll_attempts: 1440
|
||||||
|
# # Network interface whose IPv4 address is reported to the manager.
|
||||||
|
# # Overrides auto-detection when the wrong IP is selected (e.g., Docker bridge).
|
||||||
|
# # Example: "eth0", "ens192", "enp0s3"
|
||||||
|
# report_interface: "eth0"
|
||||||
|
# # Explicit IPv4 address reported to the manager.
|
||||||
|
# # Highest priority — overrides both report_interface and route-based selection.
|
||||||
|
# # Useful when the host has multiple IPs or runs inside a container.
|
||||||
|
# report_ip: "192.168.3.36"
|
||||||
|
# # Route-based IP selection is enabled by default when manager_url is set.
|
||||||
|
# The agent resolves the manager hostname to an IP, then uses `ip route get <manager_ip>`
|
||||||
|
# to determine which local source IP the kernel would use to reach the manager.
|
||||||
|
# This is the most accurate method for multi-homed hosts because it queries
|
||||||
|
# the kernel routing table directly.
|
||||||
|
# Priority order: report_ip > report_interface > route-based > auto-detect
|
||||||
|
|||||||
@ -17,10 +17,10 @@ depend() {
|
|||||||
|
|
||||||
# Create required directories before starting
|
# Create required directories before starting
|
||||||
start_pre() {
|
start_pre() {
|
||||||
checkpath --directory --owner linux-patch-api:linux-patch-api --mode 0755 \
|
checkpath --directory --owner root:root --mode 0755 \
|
||||||
/run/linux-patch-api \
|
/run/linux-patch-api \
|
||||||
/var/log/linux-patch-api \
|
/var/log/linux_patch_api \
|
||||||
/var/lib/linux-patch-api \
|
/var/lib/linux_patch_api \
|
||||||
/etc/linux_patch_api/certs
|
/etc/linux_patch_api/certs
|
||||||
|
|
||||||
# Ensure config files exist
|
# Ensure config files exist
|
||||||
|
|||||||
81
configs/linux-patch-api.install
Normal file
81
configs/linux-patch-api.install
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# Arch Linux install hooks for linux-patch-api
|
||||||
|
# Matches Debian preinst/postinst behavior: no system user, root:root ownership
|
||||||
|
|
||||||
|
post_install() {
|
||||||
|
# Create required directories
|
||||||
|
mkdir -p /etc/linux_patch_api/certs
|
||||||
|
mkdir -p /var/lib/linux_patch_api
|
||||||
|
mkdir -p /var/log/linux_patch_api
|
||||||
|
|
||||||
|
# Set proper ownership (service runs as root)
|
||||||
|
chown -R root:root /var/lib/linux_patch_api
|
||||||
|
chown -R root:root /var/log/linux_patch_api
|
||||||
|
|
||||||
|
# Set secure permissions
|
||||||
|
chmod 750 /etc/linux_patch_api
|
||||||
|
chmod 750 /etc/linux_patch_api/certs
|
||||||
|
chmod 755 /var/lib/linux_patch_api
|
||||||
|
chmod 755 /var/log/linux_patch_api
|
||||||
|
|
||||||
|
# Copy example configs if they don't exist
|
||||||
|
if [ ! -f "/etc/linux_patch_api/config.yaml" ]; then
|
||||||
|
cp /etc/linux_patch_api/config.yaml.example /etc/linux_patch_api/config.yaml
|
||||||
|
chmod 640 /etc/linux_patch_api/config.yaml
|
||||||
|
chown root:root /etc/linux_patch_api/config.yaml
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then
|
||||||
|
cp /etc/linux_patch_api/whitelist.yaml.example /etc/linux_patch_api/whitelist.yaml
|
||||||
|
chmod 640 /etc/linux_patch_api/whitelist.yaml
|
||||||
|
chown root:root /etc/linux_patch_api/whitelist.yaml
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reload systemd daemon
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Enable the service (but don't start automatically - admin should configure first)
|
||||||
|
systemctl enable linux-patch-api.service
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "linux-patch-api installed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Configure /etc/linux_patch_api/config.yaml with your settings"
|
||||||
|
echo " 2. Place TLS certificates in /etc/linux_patch_api/certs/"
|
||||||
|
echo " 3. Configure IP whitelist in /etc/linux_patch_api/whitelist.yaml"
|
||||||
|
echo " 4. Start the service: systemctl start linux-patch-api"
|
||||||
|
echo " 5. Check status: systemctl status linux-patch-api"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
post_upgrade() {
|
||||||
|
# Reload systemd daemon on upgrade
|
||||||
|
systemctl daemon-reload
|
||||||
|
}
|
||||||
|
|
||||||
|
pre_remove() {
|
||||||
|
# Stop the service before removal
|
||||||
|
if systemctl is-active --quiet linux-patch-api.service; then
|
||||||
|
systemctl stop linux-patch-api.service
|
||||||
|
echo "Service stopped successfully"
|
||||||
|
else
|
||||||
|
echo "Service was not running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Disable the service
|
||||||
|
if systemctl is-enabled --quiet linux-patch-api.service 2>/dev/null; then
|
||||||
|
systemctl disable linux-patch-api.service
|
||||||
|
echo "Service disabled"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
post_remove() {
|
||||||
|
# Reload systemd to remove service file
|
||||||
|
systemctl daemon-reload 2>/dev/null || true
|
||||||
|
|
||||||
|
# Remove directories only if empty (preserve user data on upgrade/reinstall)
|
||||||
|
rmdir --ignore-fail-on-non-empty /var/lib/linux_patch_api 2>/dev/null || true
|
||||||
|
rmdir --ignore-fail-on-non-empty /var/log/linux_patch_api 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "linux-patch-api removed"
|
||||||
|
}
|
||||||
10
configs/linux-patch-api.post-deinstall
Normal file
10
configs/linux-patch-api.post-deinstall
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Alpine Linux post-deinstall script for linux-patch-api
|
||||||
|
# Runs after package files are removed
|
||||||
|
# Matches Debian postrm behavior: clean up empty directories
|
||||||
|
|
||||||
|
# Remove directories only if empty (preserve user data on reinstall)
|
||||||
|
rmdir /var/lib/linux_patch_api 2>/dev/null || true
|
||||||
|
rmdir /var/log/linux_patch_api 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "linux-patch-api removed"
|
||||||
35
configs/linux-patch-api.post-install
Normal file
35
configs/linux-patch-api.post-install
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Alpine Linux post-install script for linux-patch-api
|
||||||
|
# Runs after package files are laid down
|
||||||
|
# Matches Debian postinst behavior: copy example configs, enable service
|
||||||
|
|
||||||
|
# Copy example configs if they don't exist
|
||||||
|
if [ ! -f "/etc/linux_patch_api/config.yaml" ]; then
|
||||||
|
if [ -f "/etc/linux_patch_api/config.yaml.example" ]; then
|
||||||
|
cp /etc/linux_patch_api/config.yaml.example /etc/linux_patch_api/config.yaml
|
||||||
|
chmod 640 /etc/linux_patch_api/config.yaml
|
||||||
|
chown root:root /etc/linux_patch_api/config.yaml
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then
|
||||||
|
if [ -f "/etc/linux_patch_api/whitelist.yaml.example" ]; then
|
||||||
|
cp /etc/linux_patch_api/whitelist.yaml.example /etc/linux_patch_api/whitelist.yaml
|
||||||
|
chmod 640 /etc/linux_patch_api/whitelist.yaml
|
||||||
|
chown root:root /etc/linux_patch_api/whitelist.yaml
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enable the service (but don't start automatically - admin should configure first)
|
||||||
|
rc-update add linux-patch-api default
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "linux-patch-api installed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Configure /etc/linux_patch_api/config.yaml with your settings"
|
||||||
|
echo " 2. Place TLS certificates in /etc/linux_patch_api/certs/"
|
||||||
|
echo " 3. Configure IP whitelist in /etc/linux_patch_api/whitelist.yaml"
|
||||||
|
echo " 4. Start the service: rc-service linux-patch-api start"
|
||||||
|
echo " 5. Check status: rc-service linux-patch-api status"
|
||||||
|
echo ""
|
||||||
15
configs/linux-patch-api.pre-deinstall
Normal file
15
configs/linux-patch-api.pre-deinstall
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Alpine Linux pre-deinstall script for linux-patch-api
|
||||||
|
# Runs before package files are removed
|
||||||
|
# Matches Debian prerm behavior: stop and disable service
|
||||||
|
|
||||||
|
# Stop the service if running
|
||||||
|
if rc-service linux-patch-api status >/dev/null 2>&1; then
|
||||||
|
rc-service linux-patch-api stop
|
||||||
|
echo "Service stopped"
|
||||||
|
else
|
||||||
|
echo "Service was not running"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Disable the service
|
||||||
|
rc-update del linux-patch-api default 2>/dev/null || true
|
||||||
33
configs/linux-patch-api.pre-install
Normal file
33
configs/linux-patch-api.pre-install
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Alpine Linux pre-install script for linux-patch-api
|
||||||
|
# Runs before package files are laid down
|
||||||
|
# Matches Debian preinst behavior: create directories, set permissions
|
||||||
|
|
||||||
|
# Create required directories
|
||||||
|
mkdir -p /etc/linux_patch_api/certs
|
||||||
|
mkdir -p /var/lib/linux_patch_api
|
||||||
|
mkdir -p /var/log/linux_patch_api
|
||||||
|
|
||||||
|
# Generate machine-id if not present (required for enrollment)
|
||||||
|
# Alpine Linux does not include /etc/machine-id by default
|
||||||
|
if [ ! -f /etc/machine-id ] || [ ! -s /etc/machine-id ]; then
|
||||||
|
if command -v uuidgen > /dev/null 2>&1; then
|
||||||
|
uuidgen | tr -d '-' > /etc/machine-id
|
||||||
|
elif [ -f /proc/sys/kernel/random/uuid ]; then
|
||||||
|
cat /proc/sys/kernel/random/uuid | tr -d '-' > /etc/machine-id
|
||||||
|
else
|
||||||
|
# Fallback: generate from /dev/urandom
|
||||||
|
od -x -N4 /dev/urandom | head -1 | awk '{print $2$3}' > /etc/machine-id
|
||||||
|
fi
|
||||||
|
chmod 444 /etc/machine-id
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set proper ownership (service runs as root)
|
||||||
|
chown -R root:root /var/lib/linux_patch_api
|
||||||
|
chown -R root:root /var/log/linux_patch_api
|
||||||
|
|
||||||
|
# Set secure permissions
|
||||||
|
chmod 750 /etc/linux_patch_api
|
||||||
|
chmod 750 /etc/linux_patch_api/certs
|
||||||
|
chmod 755 /var/lib/linux_patch_api
|
||||||
|
chmod 755 /var/log/linux_patch_api
|
||||||
@ -9,7 +9,9 @@ Type=simple
|
|||||||
NotifyAccess=all
|
NotifyAccess=all
|
||||||
ExecStart=/usr/bin/linux-patch-api --config /etc/linux_patch_api/config.yaml
|
ExecStart=/usr/bin/linux-patch-api --config /etc/linux_patch_api/config.yaml
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=10s
|
||||||
|
StartLimitBurst=5
|
||||||
|
StartLimitIntervalSec=300
|
||||||
TimeoutStopSec=30s
|
TimeoutStopSec=30s
|
||||||
|
|
||||||
# Process management
|
# Process management
|
||||||
@ -17,16 +19,17 @@ RuntimeDirectory=linux-patch-api
|
|||||||
RuntimeDirectoryMode=0755
|
RuntimeDirectoryMode=0755
|
||||||
|
|
||||||
# Security hardening
|
# Security hardening
|
||||||
# Allow reboot capability for scheduled reboots
|
# NOTE: Package management requires extensive system access. The following
|
||||||
CapabilityBoundingSet=CAP_SYS_BOOT
|
# restrictions have been removed because they block core functionality:
|
||||||
AmbientCapabilities=CAP_SYS_BOOT
|
# - ProtectSystem=strict: Blocks writes to /usr, /etc, /lib where packages install
|
||||||
# ProtectSystem removed - package management requires write access to /usr, /etc, /lib
|
# - NoNewPrivileges: Blocks sudo/setuid which apt needs for _apt sandbox
|
||||||
# Network security provided by mTLS + IP whitelist
|
# - RestrictSUIDSGID: Blocks setuid/setgid which apt needs for _apt sandbox
|
||||||
|
# - CapabilityBoundingSet: Drops capabilities that apt needs (SETUID, SETGID, CHOWN, etc.)
|
||||||
|
# - AmbientCapabilities: Same issue as CapabilityBoundingSet
|
||||||
|
# Network security is provided by mTLS + IP whitelist. The service runs as root
|
||||||
|
# and MUST be able to install/remove/update packages system-wide.
|
||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
# ReadWritePaths kept as documentation reference for apt/dpkg paths
|
|
||||||
ReadWritePaths=/var/lib/linux_patch_api /var/log/linux_patch_api /var/cache/apt /var/lib/apt /var/lib/dpkg /var/log/apt
|
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
PrivateDevices=true
|
|
||||||
ProtectHostname=true
|
ProtectHostname=true
|
||||||
ProtectClock=true
|
ProtectClock=true
|
||||||
ProtectKernelTunables=true
|
ProtectKernelTunables=true
|
||||||
@ -36,8 +39,6 @@ RestrictNamespaces=true
|
|||||||
LockPersonality=true
|
LockPersonality=true
|
||||||
MemoryDenyWriteExecute=false
|
MemoryDenyWriteExecute=false
|
||||||
RestrictRealtime=true
|
RestrictRealtime=true
|
||||||
# RestrictSUIDSGID removed - package management requires setuid/setgid for apt/dpkg
|
|
||||||
RemoveIPC=true
|
|
||||||
|
|
||||||
# System call filtering (whitelist approach)
|
# System call filtering (whitelist approach)
|
||||||
SystemCallFilter=@system-service
|
SystemCallFilter=@system-service
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
linux-patch-api (1.0.0-1) stable; urgency=medium
|
|
||||||
|
|
||||||
* Initial production release
|
|
||||||
* Secure mTLS-authenticated REST API for remote package management
|
|
||||||
* 15 API endpoints for package install/remove, patch application, system management
|
|
||||||
* Asynchronous job processing with WebSocket status streaming
|
|
||||||
* IP whitelist enforcement and comprehensive audit logging
|
|
||||||
* Systemd integration with security hardening
|
|
||||||
* Supports Debian 11/12, Ubuntu 20.04/22.04/24.04
|
|
||||||
|
|
||||||
-- Echo <echo@moon-dragon.us> Thu, 09 Apr 2026 18:57:12 -0500
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
debian/tmp/usr/bin/linux-patch-api
|
|
||||||
debian/tmp/lib/systemd/system/linux-patch-api.service
|
|
||||||
debian/tmp/etc/linux_patch_api/config.yaml
|
|
||||||
debian/tmp/etc/linux_patch_api/whitelist.yaml
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
# Automatically added by dh_installsystemd/13.31
|
|
||||||
if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then
|
|
||||||
# The following line should be removed in trixie or trixie+1
|
|
||||||
deb-systemd-helper unmask 'linux-patch-api.service' >/dev/null || true
|
|
||||||
|
|
||||||
# was-enabled defaults to true, so new installations run enable.
|
|
||||||
if deb-systemd-helper --quiet was-enabled 'linux-patch-api.service'; then
|
|
||||||
# Enables the unit on first installation, creates new
|
|
||||||
# symlinks on upgrades if the unit file has changed.
|
|
||||||
deb-systemd-helper enable 'linux-patch-api.service' >/dev/null || true
|
|
||||||
else
|
|
||||||
# Update the statefile to add new symlinks (if any), which need to be
|
|
||||||
# cleaned up on purge. Also remove old symlinks.
|
|
||||||
deb-systemd-helper update-state 'linux-patch-api.service' >/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# End automatically added section
|
|
||||||
# Automatically added by dh_installsystemd/13.31
|
|
||||||
if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then
|
|
||||||
if [ -d /run/systemd/system ]; then
|
|
||||||
systemctl --system daemon-reload >/dev/null || true
|
|
||||||
if [ -n "$2" ]; then
|
|
||||||
_dh_action=restart
|
|
||||||
else
|
|
||||||
_dh_action=start
|
|
||||||
fi
|
|
||||||
deb-systemd-invoke $_dh_action 'linux-patch-api.service' >/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# End automatically added section
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
# Automatically added by dh_installsystemd/13.31
|
|
||||||
if [ -z "$DPKG_ROOT" ] && [ "$1" = remove ] && [ -d /run/systemd/system ] ; then
|
|
||||||
deb-systemd-invoke stop 'linux-patch-api.service' >/dev/null || true
|
|
||||||
fi
|
|
||||||
# End automatically added section
|
|
||||||
61
debian/changelog
vendored
61
debian/changelog
vendored
@ -1,49 +1,22 @@
|
|||||||
linux-patch-api (0.3.3-1) unstable; urgency=low
|
linux-patch-api (1.2.0) unstable; urgency=medium
|
||||||
|
|
||||||
* Fix dpkg packaging: Remove linux-patch-api user creation, fix directory ownership
|
* Add auto-enrollment on startup when certs are missing/invalid
|
||||||
* Fix package install: Remove sudo from apt commands (service runs as root)
|
* Add cert validation (existence, parse, expiry, key match, CA trust)
|
||||||
* Remove NoNewPrivileges and RestrictSUIDSGID from systemd service
|
* Add --renew-certs CLI flag for manual cert renewal
|
||||||
|
* Fix --enroll to exit after completion (no port conflict)
|
||||||
|
* Add SO_REUSEADDR to prevent Address already in use errors
|
||||||
|
* Add polling token persistence for enrollment resume after restart
|
||||||
|
* Add exit code strategy (0=clean, 1=error, 2=enrollment in progress)
|
||||||
|
* Increase RestartSec to 10s and add StartLimitBurst=5
|
||||||
|
* Add cert and enrollment URL check in postinst
|
||||||
|
* Fix misleading "Listening on" log before actual bind
|
||||||
|
|
||||||
-- Echo <echo@moon-dragon.us> Sat, 03 May 2026 02:30:00 -0500
|
-- Echo <echo@moon-dragon.us> Thu, 29 May 2026 10:20:00 -0500
|
||||||
linux-patch-api (0.3.2-1) unstable; urgency=low
|
|
||||||
|
|
||||||
* Fix package install: Remove sudo from apt commands (service runs as root)
|
linux-patch-api (1.1.17) unstable; urgency=medium
|
||||||
* Fix reboot endpoint: Implement actual system reboot via shutdown/systemctl
|
|
||||||
* Fix patches handler: Call reboot_system() instead of just logging
|
|
||||||
* Remove NoNewPrivileges and RestrictSUIDSGID from systemd service
|
|
||||||
* Add CAP_SYS_BOOT capability to systemd service for LXC reboot support
|
|
||||||
* Fix dpkg packaging: Remove linux-patch-api user creation, fix directory ownership
|
|
||||||
|
|
||||||
-- Echo <echo@moon-dragon.us> Sat, 02 May 2026 21:25:00 -0500
|
* Add mandatory package cache refresh before patch_apply
|
||||||
linux-patch-api (0.3.1-1) unstable; urgency=low
|
* Add health check cache refresh when stale (>4h)
|
||||||
|
* Add cache status fields to health response
|
||||||
|
|
||||||
* Fix reboot endpoint: Implement actual system reboot via shutdown/systemctl
|
-- Echo <echo@moon-dragon.us> Thu, 22 May 2026 12:00:00 -0500
|
||||||
* Fix patches handler: Call reboot_system() instead of just logging
|
|
||||||
* Add CAP_SYS_BOOT capability to systemd service for LXC reboot support
|
|
||||||
* Remove unused warn import
|
|
||||||
|
|
||||||
-- Echo <echo@moon-dragon.us> Sat, 02 May 2026 20:37:00 -0500
|
|
||||||
linux-patch-api (0.3.0-1) unstable; urgency=low
|
|
||||||
|
|
||||||
* v0.3.0 beta release
|
|
||||||
* Fix List Jobs connection reset: Add client_disconnect_timeout (5s)
|
|
||||||
* Enforce TLS 1.3 only with builder_with_provider()
|
|
||||||
* Fix RwLock contention: Release read lock before sorting in list_jobs()
|
|
||||||
* Fix systemd service: Remove ProtectSystem=strict
|
|
||||||
* Fix systemd service: Change Type=notify to Type=simple
|
|
||||||
* Fix systemd service: Add DEBIAN_FRONTEND=noninteractive
|
|
||||||
* Add Ubuntu 22.04 CI build job
|
|
||||||
* Add apt-get -f install for broken runner deps
|
|
||||||
|
|
||||||
-- Echo <echo@moon-dragon.us> Sat, 02 May 2026 19:55:00 -0500
|
|
||||||
linux-patch-api (1.0.0-1) stable; urgency=medium
|
|
||||||
|
|
||||||
* Initial production release
|
|
||||||
* Secure mTLS-authenticated REST API for remote package management
|
|
||||||
* 15 API endpoints for package install/remove, patch application, system management
|
|
||||||
* Asynchronous job processing with WebSocket status streaming
|
|
||||||
* IP whitelist enforcement and comprehensive audit logging
|
|
||||||
* Systemd integration with security hardening
|
|
||||||
* Supports Debian 11/12, Ubuntu 20.04/22.04/24.04
|
|
||||||
|
|
||||||
-- Echo <echo@moon-dragon.us> Thu, 09 Apr 2026 18:57:12 -0500
|
|
||||||
|
|||||||
2
debian/control
vendored
2
debian/control
vendored
@ -14,6 +14,8 @@ Vcs-Browser: https://gitea.moon-dragon.us/echo/linux_patch_api
|
|||||||
|
|
||||||
Package: linux-patch-api
|
Package: linux-patch-api
|
||||||
Architecture: amd64
|
Architecture: amd64
|
||||||
|
Version: 1.2.0-1
|
||||||
|
Installed-Size: 0
|
||||||
Depends: systemd,
|
Depends: systemd,
|
||||||
libsystemd0,
|
libsystemd0,
|
||||||
${shlibs:Depends},
|
${shlibs:Depends},
|
||||||
|
|||||||
1
debian/debhelper-build-stamp
vendored
1
debian/debhelper-build-stamp
vendored
@ -1 +0,0 @@
|
|||||||
linux-patch-api
|
|
||||||
2
debian/files
vendored
2
debian/files
vendored
@ -1,2 +0,0 @@
|
|||||||
linux-patch-api_1.0.0-1_amd64.buildinfo admin optional
|
|
||||||
linux-patch-api_1.0.0-1_amd64.deb admin optional
|
|
||||||
1
debian/linux-patch-api.debhelper.log
vendored
1
debian/linux-patch-api.debhelper.log
vendored
@ -1 +0,0 @@
|
|||||||
dh_auto_install
|
|
||||||
12
debian/linux-patch-api.postrm.debhelper
vendored
12
debian/linux-patch-api.postrm.debhelper
vendored
@ -1,12 +0,0 @@
|
|||||||
# Automatically added by dh_installsystemd/13.31
|
|
||||||
if [ "$1" = remove ] && [ -d /run/systemd/system ] ; then
|
|
||||||
systemctl --system daemon-reload >/dev/null || true
|
|
||||||
fi
|
|
||||||
# End automatically added section
|
|
||||||
# Automatically added by dh_installsystemd/13.31
|
|
||||||
if [ "$1" = "purge" ]; then
|
|
||||||
if [ -x "/usr/bin/deb-systemd-helper" ]; then
|
|
||||||
deb-systemd-helper purge 'linux-patch-api.service' >/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
# End automatically added section
|
|
||||||
3
debian/linux-patch-api.substvars
vendored
3
debian/linux-patch-api.substvars
vendored
@ -1,3 +0,0 @@
|
|||||||
shlibs:Depends=libc6 (>= 2.39), libgcc-s1 (>= 4.2)
|
|
||||||
misc:Depends=
|
|
||||||
misc:Pre-Depends=
|
|
||||||
4
debian/linux-patch-api/DEBIAN/conffiles
vendored
4
debian/linux-patch-api/DEBIAN/conffiles
vendored
@ -1,4 +0,0 @@
|
|||||||
/etc/linux_patch_api/config.yaml
|
|
||||||
/etc/linux_patch_api/whitelist.yaml
|
|
||||||
/etc/linux_patch_api/config.yaml
|
|
||||||
/etc/linux_patch_api/whitelist.yaml
|
|
||||||
23
debian/linux-patch-api/DEBIAN/control
vendored
23
debian/linux-patch-api/DEBIAN/control
vendored
@ -1,23 +0,0 @@
|
|||||||
Package: linux-patch-api
|
|
||||||
Version: 1.0.0-1
|
|
||||||
Architecture: amd64
|
|
||||||
Maintainer: Echo <echo@moon-dragon.us>
|
|
||||||
Installed-Size: 8897
|
|
||||||
Depends: systemd, libsystemd0, libc6 (>= 2.39), libgcc-s1 (>= 4.2)
|
|
||||||
Section: admin
|
|
||||||
Priority: optional
|
|
||||||
Homepage: https://gitea.moon-dragon.us/echo/linux_patch_api
|
|
||||||
Description: Secure remote package management API for Linux systems
|
|
||||||
Linux Patch API provides a secure, mTLS-authenticated REST API for
|
|
||||||
remote package management operations including:
|
|
||||||
- Package installation and removal
|
|
||||||
- Security patch application
|
|
||||||
- System health monitoring
|
|
||||||
- Job queue management with WebSocket status streaming
|
|
||||||
.
|
|
||||||
Features:
|
|
||||||
- Mutual TLS (mTLS) authentication
|
|
||||||
- IP whitelist enforcement
|
|
||||||
- Asynchronous job processing
|
|
||||||
- Comprehensive audit logging
|
|
||||||
- Systemd integration with security hardening
|
|
||||||
5
debian/linux-patch-api/DEBIAN/md5sums
vendored
5
debian/linux-patch-api/DEBIAN/md5sums
vendored
@ -1,5 +0,0 @@
|
|||||||
23b89eecc51f46c6813658dd615d13a9 lib/systemd/system/linux-patch-api.service
|
|
||||||
d64a80e2a796561c39c6941c6b9e268c usr/bin/linux-patch-api
|
|
||||||
154c7ae7e01ae22cdc8ceea1fd0956e2 usr/share/doc/linux-patch-api/changelog.Debian.gz
|
|
||||||
978478c6c7f1e9dcb38eb1f2454535c0 usr/share/doc/linux-patch-api/changelog.gz
|
|
||||||
c2fab316c94aa61adb70d79365cfe08f usr/share/doc/linux-patch-api/copyright
|
|
||||||
49
debian/linux-patch-api/DEBIAN/postinst
vendored
49
debian/linux-patch-api/DEBIAN/postinst
vendored
@ -1,49 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# postinst script for linux-patch-api
|
|
||||||
# Created by package build system
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Configure with debhelper
|
|
||||||
if [ "$1" = "configure" ]; then
|
|
||||||
echo "Configuring linux-patch-api..."
|
|
||||||
|
|
||||||
# Copy example configs if they don't exist
|
|
||||||
if [ ! -f "/etc/linux_patch_api/config.yaml" ]; then
|
|
||||||
echo "Creating default config.yaml..."
|
|
||||||
cp /etc/linux_patch_api/config.yaml.example /etc/linux_patch_api/config.yaml
|
|
||||||
chmod 640 /etc/linux_patch_api/config.yaml
|
|
||||||
chown root:root /etc/linux_patch_api/config.yaml
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then
|
|
||||||
echo "Creating default whitelist.yaml..."
|
|
||||||
cp /etc/linux_patch_api/whitelist.yaml.example /etc/linux_patch_api/whitelist.yaml
|
|
||||||
chmod 640 /etc/linux_patch_api/whitelist.yaml
|
|
||||||
chown root:root /etc/linux_patch_api/whitelist.yaml
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Reload systemd daemon to pick up new service file
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
# Enable the service (but don't start automatically - admin should configure first)
|
|
||||||
systemctl enable linux-patch-api.service
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "linux-patch-api installed successfully!"
|
|
||||||
echo ""
|
|
||||||
echo "Next steps:"
|
|
||||||
echo " 1. Configure /etc/linux_patch_api/config.yaml with your settings"
|
|
||||||
echo " 2. Place TLS certificates in /etc/linux_patch_api/certs/"
|
|
||||||
echo " 3. Configure IP whitelist in /etc/linux_patch_api/whitelist.yaml"
|
|
||||||
echo " 4. Start the service: systemctl start linux-patch-api"
|
|
||||||
echo " 5. Check status: systemctl status linux-patch-api"
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Handle upgrade
|
|
||||||
if [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-remove" ] || [ "$1" = "abort-deconfigure" ]; then
|
|
||||||
echo "Installation aborted - service remains in previous state"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
52
debian/linux-patch-api/DEBIAN/postrm
vendored
52
debian/linux-patch-api/DEBIAN/postrm
vendored
@ -1,52 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# postrm script for linux-patch-api
|
|
||||||
# Created by package build system
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Handle purge - remove all configuration and data
|
|
||||||
if [ "$1" = "purge" ]; then
|
|
||||||
echo "Purging linux-patch-api configuration and data..."
|
|
||||||
|
|
||||||
# Stop service if still running
|
|
||||||
if systemctl is-active --quiet linux-patch-api.service 2>/dev/null; then
|
|
||||||
systemctl stop linux-patch-api.service
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Disable service
|
|
||||||
if systemctl is-enabled --quiet linux-patch-api.service 2>/dev/null; then
|
|
||||||
systemctl disable linux-patch-api.service
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Reload systemd to remove service file
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
# Remove configuration directory (preserved by conffiles during normal remove)
|
|
||||||
if [ -d "/etc/linux_patch_api" ]; then
|
|
||||||
echo "Removing /etc/linux_patch_api..."
|
|
||||||
rm -rf /etc/linux_patch_api
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove data directory
|
|
||||||
if [ -d "/var/lib/linux_patch_api" ]; then
|
|
||||||
echo "Removing /var/lib/linux_patch_api..."
|
|
||||||
rm -rf /var/lib/linux_patch_api
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove log directory
|
|
||||||
if [ -d "/var/log/linux_patch_api" ]; then
|
|
||||||
echo "Removing /var/log/linux_patch_api..."
|
|
||||||
rm -rf /var/log/linux_patch_api
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "linux-patch-api purged successfully"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Handle upgrade/remove - just ensure service is disabled
|
|
||||||
if [ "$1" = "remove" ] || [ "$1" = "upgrade" ]; then
|
|
||||||
# Service should already be stopped by prerm
|
|
||||||
# Just reload systemd to remove the service file
|
|
||||||
systemctl daemon-reload 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
29
debian/linux-patch-api/DEBIAN/preinst
vendored
29
debian/linux-patch-api/DEBIAN/preinst
vendored
@ -1,29 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# preinst script for linux-patch-api
|
|
||||||
# Created by package build system
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Check if this is an upgrade
|
|
||||||
if [ -d "/etc/linux_patch_api" ]; then
|
|
||||||
echo "Detected existing installation - performing upgrade"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create required directories
|
|
||||||
mkdir -p /etc/linux_patch_api/certs
|
|
||||||
mkdir -p /var/lib/linux_patch_api
|
|
||||||
mkdir -p /var/log/linux_patch_api
|
|
||||||
|
|
||||||
# Set proper ownership (service runs as root)
|
|
||||||
chown -R root:root /var/lib/linux_patch_api
|
|
||||||
chown -R root:root /var/log/linux_patch_api
|
|
||||||
|
|
||||||
# Set secure permissions
|
|
||||||
chmod 750 /etc/linux_patch_api
|
|
||||||
chmod 750 /etc/linux_patch_api/certs
|
|
||||||
chmod 755 /var/lib/linux_patch_api
|
|
||||||
chmod 755 /var/log/linux_patch_api
|
|
||||||
|
|
||||||
echo "Pre-installation checks completed successfully"
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
33
debian/linux-patch-api/DEBIAN/prerm
vendored
33
debian/linux-patch-api/DEBIAN/prerm
vendored
@ -1,33 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# prerm script for linux-patch-api
|
|
||||||
# Created by package build system
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Stop the service before removal/upgrade
|
|
||||||
if [ "$1" = "remove" ] || [ "$1" = "upgrade" ]; then
|
|
||||||
echo "Stopping linux-patch-api service..."
|
|
||||||
|
|
||||||
if systemctl is-active --quiet linux-patch-api.service; then
|
|
||||||
systemctl stop linux-patch-api.service
|
|
||||||
echo "Service stopped successfully"
|
|
||||||
else
|
|
||||||
echo "Service was not running"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Disable the service
|
|
||||||
if systemctl is-enabled --quiet linux-patch-api.service 2>/dev/null; then
|
|
||||||
systemctl disable linux-patch-api.service
|
|
||||||
echo "Service disabled"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Handle failed upgrade
|
|
||||||
if [ "$1" = "failed-upgrade" ]; then
|
|
||||||
echo "Upgrade failed - attempting to restore previous state"
|
|
||||||
# Previous version should handle restoration
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Pre-removal script completed"
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
# Linux Patch API Configuration
|
|
||||||
# Example configuration file - copy to /etc/linux_patch_api/config.yaml
|
|
||||||
|
|
||||||
# Server Configuration
|
|
||||||
server:
|
|
||||||
port: 12443
|
|
||||||
bind: "0.0.0.0"
|
|
||||||
timeout_seconds: 30
|
|
||||||
|
|
||||||
# TLS/mTLS Configuration
|
|
||||||
tls:
|
|
||||||
enabled: true
|
|
||||||
port: 12443
|
|
||||||
ca_cert: "/etc/linux_patch_api/certs/ca.pem"
|
|
||||||
server_cert: "/etc/linux_patch_api/certs/server.pem"
|
|
||||||
server_key: "/etc/linux_patch_api/certs/server.key"
|
|
||||||
min_tls_version: "1.3"
|
|
||||||
|
|
||||||
# Job Configuration
|
|
||||||
jobs:
|
|
||||||
max_concurrent: 5
|
|
||||||
timeout_minutes: 30
|
|
||||||
storage_path: "/var/lib/linux_patch_api/jobs"
|
|
||||||
|
|
||||||
# Logging Configuration
|
|
||||||
logging:
|
|
||||||
level: "info"
|
|
||||||
journal_enabled: true
|
|
||||||
syslog_enabled: false
|
|
||||||
# syslog_server: "udp://localhost:514"
|
|
||||||
file_path: "/var/log/linux_patch_api/audit.log"
|
|
||||||
retention_days: 30
|
|
||||||
|
|
||||||
# IP Whitelist Configuration
|
|
||||||
whitelist:
|
|
||||||
path: "/etc/linux_patch_api/whitelist.yaml"
|
|
||||||
# Entries can be:
|
|
||||||
# - Individual IPs: "192.168.1.100"
|
|
||||||
# - CIDR subnets: "192.168.1.0/24"
|
|
||||||
# - Hostnames: "admin-server.internal"
|
|
||||||
|
|
||||||
# Package Manager Backend
|
|
||||||
package_manager:
|
|
||||||
# Primary backend (auto-detected if not specified)
|
|
||||||
# Options: apt, dnf, yum, apk, pacman
|
|
||||||
backend: "auto"
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
# Linux Patch API - IP Whitelist Configuration
|
|
||||||
# Copy to /etc/linux_patch_api/whitelist.yaml
|
|
||||||
# Block all by default - only listed IPs can access the API
|
|
||||||
|
|
||||||
# Supported entry types:
|
|
||||||
# - Individual IPs: "192.168.1.100"
|
|
||||||
# - CIDR subnets: "192.168.1.0/24"
|
|
||||||
# - Hostnames: "admin-server.internal" (resolved at startup)
|
|
||||||
|
|
||||||
# Example entries:
|
|
||||||
entries:
|
|
||||||
- "192.168.1.0/24" # Management network
|
|
||||||
- "10.0.0.50" # Specific admin workstation
|
|
||||||
# - "admin-server.internal" # Hostname example (uncomment to use)
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Linux Patch API - Secure Remote Package Management
|
|
||||||
Documentation=man:linux-patch-api(8)
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=notify
|
|
||||||
ExecStart=/usr/bin/linux-patch-api --config /etc/linux_patch_api/config.yaml
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5s
|
|
||||||
TimeoutStopSec=30s
|
|
||||||
|
|
||||||
# Process management
|
|
||||||
RuntimeDirectory=linux-patch-api
|
|
||||||
RuntimeDirectoryMode=0755
|
|
||||||
|
|
||||||
# Security hardening
|
|
||||||
NoNewPrivileges=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
ReadWritePaths=/var/lib/linux_patch_api /var/log/linux_patch_api
|
|
||||||
PrivateTmp=true
|
|
||||||
PrivateDevices=true
|
|
||||||
ProtectHostname=true
|
|
||||||
ProtectClock=true
|
|
||||||
ProtectKernelTunables=true
|
|
||||||
ProtectKernelModules=true
|
|
||||||
ProtectKernelLogs=true
|
|
||||||
RestrictNamespaces=true
|
|
||||||
LockPersonality=true
|
|
||||||
MemoryDenyWriteExecute=false
|
|
||||||
RestrictRealtime=true
|
|
||||||
RestrictSUIDSGID=true
|
|
||||||
RemoveIPC=true
|
|
||||||
|
|
||||||
# System call filtering (whitelist approach)
|
|
||||||
SystemCallFilter=@system-service
|
|
||||||
SystemCallErrorNumber=EPERM
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
Environment="RUST_BACKTRACE=1"
|
|
||||||
Environment="RUST_LOG=info"
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
SyslogIdentifier=linux-patch-api
|
|
||||||
SyslogFacility=daemon
|
|
||||||
SyslogLevel=info
|
|
||||||
|
|
||||||
# Resource limits
|
|
||||||
LimitNOFILE=65536
|
|
||||||
LimitNPROC=4096
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
BIN
debian/linux-patch-api/usr/bin/linux-patch-api
vendored
BIN
debian/linux-patch-api/usr/bin/linux-patch-api
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,31 +0,0 @@
|
|||||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
||||||
Upstream-Name: linux-patch-api
|
|
||||||
Upstream-Contact: Echo <echo@moon-dragon.us>
|
|
||||||
Source: https://gitea.moon-dragon.us/echo/linux_patch_api
|
|
||||||
|
|
||||||
Files: *
|
|
||||||
Copyright: 2024-2026 Echo <echo@moon-dragon.us>
|
|
||||||
License: MIT
|
|
||||||
|
|
||||||
License: MIT
|
|
||||||
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.
|
|
||||||
|
|
||||||
Files: debian/*
|
|
||||||
Copyright: 2024-2026 Echo <echo@moon-dragon.us>
|
|
||||||
License: MIT
|
|
||||||
58
debian/postinst
vendored
58
debian/postinst
vendored
@ -29,16 +29,60 @@ if [ "$1" = "configure" ]; then
|
|||||||
# Enable the service (but don't start automatically - admin should configure first)
|
# Enable the service (but don't start automatically - admin should configure first)
|
||||||
systemctl enable linux-patch-api.service
|
systemctl enable linux-patch-api.service
|
||||||
|
|
||||||
|
# Check for TLS certificates and enrollment URL
|
||||||
|
CERT_DIR="/etc/linux_patch_api/certs"
|
||||||
|
CA_CERT="$CERT_DIR/ca.pem"
|
||||||
|
SERVER_CERT="$CERT_DIR/server.pem"
|
||||||
|
SERVER_KEY="$CERT_DIR/server.key.pem"
|
||||||
|
CONFIG_FILE="/etc/linux_patch_api/config.yaml"
|
||||||
|
|
||||||
|
CERTS_MISSING=false
|
||||||
|
if [ ! -f "$CA_CERT" ] || [ ! -f "$SERVER_CERT" ] || [ ! -f "$SERVER_KEY" ]; then
|
||||||
|
CERTS_MISSING=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$CERTS_MISSING" = true ]; then
|
||||||
|
echo ""
|
||||||
|
echo "⚠ TLS certificates are missing. The service will not start without them."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if enrollment.manager_url is configured
|
||||||
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
|
# Check for manager_url in config (handles both old String format and new Option format)
|
||||||
|
MANAGER_URL=$(grep -E '^\s*manager_url:' "$CONFIG_FILE" 2>/dev/null | sed 's/^\s*manager_url:\s*//' | tr -d '"' | tr -d "'" | xargs)
|
||||||
|
if [ -n "$MANAGER_URL" ] && [ "$MANAGER_URL" != "" ]; then
|
||||||
|
echo "✓ Auto-enrollment is configured (manager_url: $MANAGER_URL)"
|
||||||
|
echo " Auto-enrollment will run on first service start."
|
||||||
|
echo " The service will automatically request and provision certificates."
|
||||||
|
else
|
||||||
|
echo "⚠ No enrollment.manager_url found in config.yaml."
|
||||||
|
echo ""
|
||||||
|
echo "To enable automatic certificate enrollment, add the manager URL:"
|
||||||
|
echo " 1. Edit /etc/linux_patch_api/config.yaml"
|
||||||
|
echo " 2. Add enrollment.manager_url: https://<your-manager-url>"
|
||||||
|
echo " 3. Start the service: systemctl start linux-patch-api"
|
||||||
|
echo ""
|
||||||
|
echo "Or enroll manually:"
|
||||||
|
echo " linux-patch-api --enroll https://<your-manager-url>"
|
||||||
|
echo ""
|
||||||
|
echo "Or place certificates manually:"
|
||||||
|
echo " - CA certificate: $CA_CERT"
|
||||||
|
echo " - Server certificate: $SERVER_CERT"
|
||||||
|
echo " - Server key: $SERVER_KEY"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠ Config file not found at $CONFIG_FILE"
|
||||||
|
echo " Please configure the service before starting."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "✓ TLS certificates found. The service is ready to start."
|
||||||
|
echo " Start the service: systemctl start linux-patch-api"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "linux-patch-api installed successfully!"
|
echo "linux-patch-api installed successfully!"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Next steps:"
|
|
||||||
echo " 1. Configure /etc/linux_patch_api/config.yaml with your settings"
|
|
||||||
echo " 2. Place TLS certificates in /etc/linux_patch_api/certs/"
|
|
||||||
echo " 3. Configure IP whitelist in /etc/linux_patch_api/whitelist.yaml"
|
|
||||||
echo " 4. Start the service: systemctl start linux-patch-api"
|
|
||||||
echo " 5. Check status: systemctl status linux-patch-api"
|
|
||||||
echo ""
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Handle upgrade
|
# Handle upgrade
|
||||||
|
|||||||
2
debian/rules
vendored
2
debian/rules
vendored
@ -8,7 +8,7 @@ export DEB_CARGO_BUILD_FLAGS=--release
|
|||||||
dh $@
|
dh $@
|
||||||
|
|
||||||
override_dh_auto_build:
|
override_dh_auto_build:
|
||||||
. "$$HOME/.cargo/env" && cargo build --release --target x86_64-unknown-linux-gnu
|
cargo build --release --target x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
override_dh_auto_install:
|
override_dh_auto_install:
|
||||||
dh_auto_install
|
dh_auto_install
|
||||||
|
|||||||
46
debian/tmp/etc/linux_patch_api/config.yaml
vendored
46
debian/tmp/etc/linux_patch_api/config.yaml
vendored
@ -1,46 +0,0 @@
|
|||||||
# Linux Patch API Configuration
|
|
||||||
# Example configuration file - copy to /etc/linux_patch_api/config.yaml
|
|
||||||
|
|
||||||
# Server Configuration
|
|
||||||
server:
|
|
||||||
port: 12443
|
|
||||||
bind: "0.0.0.0"
|
|
||||||
timeout_seconds: 30
|
|
||||||
|
|
||||||
# TLS/mTLS Configuration
|
|
||||||
tls:
|
|
||||||
enabled: true
|
|
||||||
port: 12443
|
|
||||||
ca_cert: "/etc/linux_patch_api/certs/ca.pem"
|
|
||||||
server_cert: "/etc/linux_patch_api/certs/server.pem"
|
|
||||||
server_key: "/etc/linux_patch_api/certs/server.key"
|
|
||||||
min_tls_version: "1.3"
|
|
||||||
|
|
||||||
# Job Configuration
|
|
||||||
jobs:
|
|
||||||
max_concurrent: 5
|
|
||||||
timeout_minutes: 30
|
|
||||||
storage_path: "/var/lib/linux_patch_api/jobs"
|
|
||||||
|
|
||||||
# Logging Configuration
|
|
||||||
logging:
|
|
||||||
level: "info"
|
|
||||||
journal_enabled: true
|
|
||||||
syslog_enabled: false
|
|
||||||
# syslog_server: "udp://localhost:514"
|
|
||||||
file_path: "/var/log/linux_patch_api/audit.log"
|
|
||||||
retention_days: 30
|
|
||||||
|
|
||||||
# IP Whitelist Configuration
|
|
||||||
whitelist:
|
|
||||||
path: "/etc/linux_patch_api/whitelist.yaml"
|
|
||||||
# Entries can be:
|
|
||||||
# - Individual IPs: "192.168.1.100"
|
|
||||||
# - CIDR subnets: "192.168.1.0/24"
|
|
||||||
# - Hostnames: "admin-server.internal"
|
|
||||||
|
|
||||||
# Package Manager Backend
|
|
||||||
package_manager:
|
|
||||||
# Primary backend (auto-detected if not specified)
|
|
||||||
# Options: apt, dnf, yum, apk, pacman
|
|
||||||
backend: "auto"
|
|
||||||
14
debian/tmp/etc/linux_patch_api/whitelist.yaml
vendored
14
debian/tmp/etc/linux_patch_api/whitelist.yaml
vendored
@ -1,14 +0,0 @@
|
|||||||
# Linux Patch API - IP Whitelist Configuration
|
|
||||||
# Copy to /etc/linux_patch_api/whitelist.yaml
|
|
||||||
# Block all by default - only listed IPs can access the API
|
|
||||||
|
|
||||||
# Supported entry types:
|
|
||||||
# - Individual IPs: "192.168.1.100"
|
|
||||||
# - CIDR subnets: "192.168.1.0/24"
|
|
||||||
# - Hostnames: "admin-server.internal" (resolved at startup)
|
|
||||||
|
|
||||||
# Example entries:
|
|
||||||
entries:
|
|
||||||
- "192.168.1.0/24" # Management network
|
|
||||||
- "10.0.0.50" # Specific admin workstation
|
|
||||||
# - "admin-server.internal" # Hostname example (uncomment to use)
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Linux Patch API - Secure Remote Package Management
|
|
||||||
Documentation=man:linux-patch-api(8)
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=notify
|
|
||||||
ExecStart=/usr/bin/linux-patch-api --config /etc/linux_patch_api/config.yaml
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5s
|
|
||||||
TimeoutStopSec=30s
|
|
||||||
|
|
||||||
# Process management
|
|
||||||
RuntimeDirectory=linux-patch-api
|
|
||||||
RuntimeDirectoryMode=0755
|
|
||||||
|
|
||||||
# Security hardening
|
|
||||||
NoNewPrivileges=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
ReadWritePaths=/var/lib/linux_patch_api /var/log/linux_patch_api
|
|
||||||
PrivateTmp=true
|
|
||||||
PrivateDevices=true
|
|
||||||
ProtectHostname=true
|
|
||||||
ProtectClock=true
|
|
||||||
ProtectKernelTunables=true
|
|
||||||
ProtectKernelModules=true
|
|
||||||
ProtectKernelLogs=true
|
|
||||||
RestrictNamespaces=true
|
|
||||||
LockPersonality=true
|
|
||||||
MemoryDenyWriteExecute=false
|
|
||||||
RestrictRealtime=true
|
|
||||||
RestrictSUIDSGID=true
|
|
||||||
RemoveIPC=true
|
|
||||||
|
|
||||||
# System call filtering (whitelist approach)
|
|
||||||
SystemCallFilter=@system-service
|
|
||||||
SystemCallErrorNumber=EPERM
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
Environment="RUST_BACKTRACE=1"
|
|
||||||
Environment="RUST_LOG=info"
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
SyslogIdentifier=linux-patch-api
|
|
||||||
SyslogFacility=daemon
|
|
||||||
SyslogLevel=info
|
|
||||||
|
|
||||||
# Resource limits
|
|
||||||
LimitNOFILE=65536
|
|
||||||
LimitNPROC=4096
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
BIN
debian/tmp/usr/bin/linux-patch-api
vendored
BIN
debian/tmp/usr/bin/linux-patch-api
vendored
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
%global debug_package %{nil}
|
%global debug_package %{nil}
|
||||||
|
|
||||||
Name: linux-patch-api
|
Name: linux-patch-api
|
||||||
Version: 1.0.0
|
Version: VERSION_PLACEHOLDER
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: Secure remote package management API for Linux systems
|
Summary: Secure remote package management API for Linux systems
|
||||||
License: MIT
|
License: MIT
|
||||||
@ -10,19 +10,21 @@ Source0: linux-patch-api-%{version}.tar.gz
|
|||||||
BuildArch: x86_64
|
BuildArch: x86_64
|
||||||
|
|
||||||
# Build requirements
|
# Build requirements
|
||||||
# NOTE: Building in Debian container (node:18) - apt packages don't register in RPM db
|
# NOTE: CI uses rustup to install cargo/rust, so they are NOT available as RPM packages.
|
||||||
# Build tools ARE available (installed via apt-get in ci.yml), just won't validate
|
# Only uncomment BuildRequires for native RPM build environments where cargo/rust
|
||||||
|
# are installed via dnf/yum package manager.
|
||||||
# BuildRequires: cargo >= 1.75
|
# BuildRequires: cargo >= 1.75
|
||||||
# BuildRequires: rust >= 1.75
|
# BuildRequires: rust >= 1.75
|
||||||
# BuildRequires: systemd-rpm-macros # Handling systemd manually
|
|
||||||
# BuildRequires: pkgconfig(systemd)
|
|
||||||
# BuildRequires: gcc
|
# BuildRequires: gcc
|
||||||
|
# BuildRequires: openssl-devel
|
||||||
|
# BuildRequires: systemd-devel
|
||||||
|
# BuildRequires: pkgconfig(systemd)
|
||||||
|
|
||||||
# Runtime requirements
|
# Runtime requirements
|
||||||
Requires: systemd
|
Requires: systemd-libs
|
||||||
Requires: libsystemd
|
Requires: openssl-libs
|
||||||
|
Requires: ca-certificates
|
||||||
|
|
||||||
# Description
|
|
||||||
%description
|
%description
|
||||||
Linux Patch API provides a secure, mTLS-authenticated REST API for
|
Linux Patch API provides a secure, mTLS-authenticated REST API for
|
||||||
remote package management operations including:
|
remote package management operations including:
|
||||||
@ -42,10 +44,11 @@ Features:
|
|||||||
%prep
|
%prep
|
||||||
%autosetup -n linux-patch-api-%{version}
|
%autosetup -n linux-patch-api-%{version}
|
||||||
|
|
||||||
# Build
|
# Build - no-op, binary is pre-built and included in source tarball
|
||||||
|
# The binary is built by build-rpm.sh BEFORE creating the tarball,
|
||||||
|
# so cargo does not need to be in rpmbuild's PATH.
|
||||||
%build
|
%build
|
||||||
export RUSTFLAGS="-C target-cpu=native"
|
# Binary already built - nothing to do
|
||||||
cargo build --release --target x86_64-unknown-linux-gnu
|
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
%install
|
%install
|
||||||
@ -56,8 +59,8 @@ mkdir -p %{buildroot}/lib/systemd/system
|
|||||||
mkdir -p %{buildroot}/var/log/linux_patch_api
|
mkdir -p %{buildroot}/var/log/linux_patch_api
|
||||||
mkdir -p %{buildroot}/var/lib/linux_patch_api
|
mkdir -p %{buildroot}/var/lib/linux_patch_api
|
||||||
|
|
||||||
# Install binary
|
# Install binary (pre-built, included in tarball at target/release/)
|
||||||
cp target/x86_64-unknown-linux-gnu/release/linux-patch-api %{buildroot}/usr/bin/
|
cp target/release/linux-patch-api %{buildroot}/usr/bin/
|
||||||
chmod 755 %{buildroot}/usr/bin/linux-patch-api
|
chmod 755 %{buildroot}/usr/bin/linux-patch-api
|
||||||
|
|
||||||
# Install systemd service
|
# Install systemd service
|
||||||
@ -69,28 +72,16 @@ cp configs/config.yaml.example %{buildroot}/etc/linux_patch_api/config.yaml.exam
|
|||||||
cp configs/whitelist.yaml.example %{buildroot}/etc/linux_patch_api/whitelist.yaml.example
|
cp configs/whitelist.yaml.example %{buildroot}/etc/linux_patch_api/whitelist.yaml.example
|
||||||
chmod 644 %{buildroot}/etc/linux_patch_api/*.example
|
chmod 644 %{buildroot}/etc/linux_patch_api/*.example
|
||||||
|
|
||||||
# Pre-installation script
|
# Pre-installation script - create directories (matches Debian preinst)
|
||||||
%pre
|
%pre
|
||||||
# Create system group
|
|
||||||
getent group linux-patch-api > /dev/null || groupadd --system linux-patch-api
|
|
||||||
|
|
||||||
# Create system user
|
|
||||||
getent passwd linux-patch-api > /dev/null || useradd --system \
|
|
||||||
--gid linux-patch-api \
|
|
||||||
--home-dir /var/lib/linux_patch_api \
|
|
||||||
--no-create-home \
|
|
||||||
--shell /usr/sbin/nologin \
|
|
||||||
--comment "Linux Patch API Service" \
|
|
||||||
linux-patch-api
|
|
||||||
|
|
||||||
# Create required directories
|
# Create required directories
|
||||||
mkdir -p /etc/linux_patch_api/certs
|
mkdir -p /etc/linux_patch_api/certs
|
||||||
mkdir -p /var/lib/linux_patch_api
|
mkdir -p /var/lib/linux_patch_api
|
||||||
mkdir -p /var/log/linux_patch_api
|
mkdir -p /var/log/linux_patch_api
|
||||||
|
|
||||||
# Set proper ownership
|
# Set proper ownership (service runs as root)
|
||||||
chown -R linux-patch-api:linux-patch-api /var/lib/linux_patch_api
|
chown -R root:root /var/lib/linux_patch_api
|
||||||
chown -R linux-patch-api:linux-patch-api /var/log/linux_patch_api
|
chown -R root:root /var/log/linux_patch_api
|
||||||
|
|
||||||
# Set secure permissions
|
# Set secure permissions
|
||||||
chmod 750 /etc/linux_patch_api
|
chmod 750 /etc/linux_patch_api
|
||||||
@ -98,19 +89,19 @@ chmod 750 /etc/linux_patch_api/certs
|
|||||||
chmod 755 /var/lib/linux_patch_api
|
chmod 755 /var/lib/linux_patch_api
|
||||||
chmod 755 /var/log/linux_patch_api
|
chmod 755 /var/log/linux_patch_api
|
||||||
|
|
||||||
# Post-installation script
|
# Post-installation script - copy configs, enable service (matches Debian postinst)
|
||||||
%post
|
%post
|
||||||
# Copy example configs if they don't exist
|
# Copy example configs if they don't exist
|
||||||
if [ ! -f "/etc/linux_patch_api/config.yaml" ]; then
|
if [ ! -f "/etc/linux_patch_api/config.yaml" ]; then
|
||||||
cp /etc/linux_patch_api/config.yaml.example /etc/linux_patch_api/config.yaml
|
cp /etc/linux_patch_api/config.yaml.example /etc/linux_patch_api/config.yaml
|
||||||
chmod 640 /etc/linux_patch_api/config.yaml
|
chmod 640 /etc/linux_patch_api/config.yaml
|
||||||
chown linux-patch-api:linux-patch-api /etc/linux_patch_api/config.yaml
|
chown root:root /etc/linux_patch_api/config.yaml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then
|
if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then
|
||||||
cp /etc/linux_patch_api/whitelist.yaml.example /etc/linux_patch_api/whitelist.yaml
|
cp /etc/linux_patch_api/whitelist.yaml.example /etc/linux_patch_api/whitelist.yaml
|
||||||
chmod 640 /etc/linux_patch_api/whitelist.yaml
|
chmod 640 /etc/linux_patch_api/whitelist.yaml
|
||||||
chown linux-patch-api:linux-patch-api /etc/linux_patch_api/whitelist.yaml
|
chown root:root /etc/linux_patch_api/whitelist.yaml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Reload systemd daemon
|
# Reload systemd daemon
|
||||||
@ -158,10 +149,13 @@ fi
|
|||||||
|
|
||||||
# Files
|
# Files
|
||||||
%files
|
%files
|
||||||
|
%defattr(-,root,root,-)
|
||||||
/usr/bin/linux-patch-api
|
/usr/bin/linux-patch-api
|
||||||
/lib/systemd/system/linux-patch-api.service
|
/lib/systemd/system/linux-patch-api.service
|
||||||
%config(noreplace) /etc/linux_patch_api/config.yaml.example
|
%config(noreplace) /etc/linux_patch_api/config.yaml.example
|
||||||
%config(noreplace) /etc/linux_patch_api/whitelist.yaml.example
|
%config(noreplace) /etc/linux_patch_api/whitelist.yaml.example
|
||||||
|
%ghost %config(noreplace) /etc/linux_patch_api/config.yaml
|
||||||
|
%ghost %config(noreplace) /etc/linux_patch_api/whitelist.yaml
|
||||||
%dir /etc/linux_patch_api
|
%dir /etc/linux_patch_api
|
||||||
%dir /etc/linux_patch_api/certs
|
%dir /etc/linux_patch_api/certs
|
||||||
%dir /var/lib/linux_patch_api
|
%dir /var/lib/linux_patch_api
|
||||||
@ -169,11 +163,71 @@ fi
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
%changelog
|
%changelog
|
||||||
* Thu Apr 09 2026 Echo <echo@moon-dragon.us> - 1.0.0-1
|
* Tue May 27 2026 Echo <echo@moon-dragon.us> - 1.1.17-1
|
||||||
|
- Add mandatory package cache refresh before patch_apply
|
||||||
|
- Add health check cache refresh when stale (>4h)
|
||||||
|
- Add cache status fields to health response
|
||||||
|
- Add 404/fetch error retry with cache refresh
|
||||||
|
- Add degraded health status on cache failure
|
||||||
|
- New src/packages/cache.rs module
|
||||||
|
|
||||||
|
* Tue May 20 2026 Echo <echo@moon-dragon.us> - 1.1.16-1
|
||||||
|
- Add Pacman package manager backend for Arch Linux
|
||||||
|
- Fix: Pacman backend not yet implemented error on Arch systems
|
||||||
|
- Support pacman -Q for package listing, pacman -Qi for package details
|
||||||
|
- Support pacman -Qu for patch/update detection
|
||||||
|
- Fix Arch CI: add stale package cleanup and version verification
|
||||||
|
|
||||||
|
* Tue May 20 2026 Echo <echo@moon-dragon.us> - 1.1.15-1
|
||||||
|
- Add DNF package manager backend for Fedora/RHEL/CentOS 8+
|
||||||
|
- Add YUM package manager backend for RHEL/CentOS 7
|
||||||
|
- Fix: DNF backend not yet implemented error on Fedora systems
|
||||||
|
- Support rpm -qa for package listing, rpm -qi for package details
|
||||||
|
- Support dnf check-update (exit code 100) for patch detection
|
||||||
|
- Support yum check-update (exit code 100) for patch detection
|
||||||
|
|
||||||
|
* Tue May 20 2026 Echo <echo@moon-dragon.us> - 1.1.14-1
|
||||||
|
- Fix RPM packaging: pre-build binary before tarball (like Alpine/Arch pattern)
|
||||||
|
- Fix rpmbuild can't find cargo in PATH - binary now included in source tarball
|
||||||
|
- Fix config file ownership: add %defattr(-,root,root,-) in %files section
|
||||||
|
- Fix Requires: libsystemd -> systemd-libs for Fedora compatibility
|
||||||
|
- Remove Requires: systemd (not needed, may not exist in containers)
|
||||||
|
- Add stale RPM cleanup and version verification to build-rpm.sh
|
||||||
|
- Support SKIP_CARGO_BUILD=1 like Alpine/Arch builds
|
||||||
|
|
||||||
|
* Wed May 20 2026 Echo <echo@moon-dragon.us> - 1.1.12-1
|
||||||
|
- Add APK (Alpine Linux) package manager backend
|
||||||
|
- Add machine-id generation to Alpine pre-install script
|
||||||
|
- Fix OpenRC init script ownership (root:root)
|
||||||
|
|
||||||
|
|
||||||
|
* Wed May 20 2026 Echo <echo@moon-dragon.us> - 1.1.10-1
|
||||||
|
- Fix Alpine install scripts: use separate files with valid abuild suffixes
|
||||||
|
- Root cause: .apk-install is not a valid abuild suffix (abuild silently fails)
|
||||||
|
- Correct format: pkgname.pre-install, .post-install, .pre-deinstall, .post-deinstall
|
||||||
|
- Verified on actual Alpine runner: install script suffixes now pass abuild validation
|
||||||
|
|
||||||
|
* Tue May 19 2026 Echo <echo@moon-dragon.us> - 1.1.9-1
|
||||||
|
- Fix non-Ubuntu packages: align Arch, RPM, Alpine with Debian baseline
|
||||||
|
- Remove system user creation (service runs as root)
|
||||||
|
- Fix ownership to root:root across all platforms
|
||||||
|
- Fix Alpine: co-locate install script with APKBUILD
|
||||||
|
- Fix Arch: correct $startdir path in PKGBUILD
|
||||||
|
- Fix RPM: add runtime deps, comment BuildRequires for CI
|
||||||
|
- Add comprehensive installation docs for all platforms
|
||||||
|
|
||||||
|
* Tue May 19 2026 Echo <echo@moon-dragon.us> - 1.1.8-1
|
||||||
|
- Fix RPM packaging: runtime deps, match Debian install behavior, comment BuildRequires for CI
|
||||||
|
- Remove system user creation (service runs as root per systemd unit)
|
||||||
|
- Fix ownership to root:root matching Debian package
|
||||||
|
- Add openssl-libs and ca-certificates runtime dependencies
|
||||||
|
|
||||||
|
* Mon May 18 2026 Echo <echo@moon-dragon.us> - 1.1.8-1
|
||||||
|
- Fix FQDN resolution: prioritize hostname -f over /etc/hostname
|
||||||
|
- Fix display_name blank: add hostname field to enrollment request
|
||||||
|
- Fix Arch/Alpine/RPM packaging: install scripts, user creation, directory creation
|
||||||
|
|
||||||
|
* Thu Apr 09 2026 Echo <echo@moon-dragon.us> - 1.1.7-1
|
||||||
- Initial production release
|
- Initial production release
|
||||||
- Secure mTLS-authenticated REST API for remote package management
|
- Secure mTLS-authenticated REST API for remote package management
|
||||||
- 15 API endpoints for package install/remove, patch application, system management
|
- 15 API endpoints for package install/remove, patch application, system management
|
||||||
- Asynchronous job processing with WebSocket status streaming
|
|
||||||
- IP whitelist enforcement and comprehensive audit logging
|
|
||||||
- Systemd integration with security hardening
|
|
||||||
- Supports RHEL 8/9, CentOS 8/9, Fedora 38+
|
|
||||||
|
|||||||
@ -1,209 +0,0 @@
|
|||||||
Format: 1.0
|
|
||||||
Source: linux-patch-api
|
|
||||||
Binary: linux-patch-api
|
|
||||||
Architecture: amd64
|
|
||||||
Version: 1.0.0-1
|
|
||||||
Checksums-Md5:
|
|
||||||
a64eb068fd021dd3a559bf1429960165 2624992 linux-patch-api_1.0.0-1_amd64.deb
|
|
||||||
Checksums-Sha1:
|
|
||||||
29bfe7427b42f05b4c0fd886d02b2550289df356 2624992 linux-patch-api_1.0.0-1_amd64.deb
|
|
||||||
Checksums-Sha256:
|
|
||||||
353b49ef3f83c0bf2c556bcfc1b3e8bb46b8e629a34659d4d5b63ac25c5a80c0 2624992 linux-patch-api_1.0.0-1_amd64.deb
|
|
||||||
Build-Origin: Kali
|
|
||||||
Build-Architecture: amd64
|
|
||||||
Build-Date: Fri, 10 Apr 2026 01:50:29 +0000
|
|
||||||
Build-Tainted-By:
|
|
||||||
usr-local-has-programs
|
|
||||||
Installed-Build-Depends:
|
|
||||||
autoconf (= 2.72-6),
|
|
||||||
automake (= 1:1.18.1-4),
|
|
||||||
autopoint (= 0.23.2-2),
|
|
||||||
autotools-dev (= 20240727.1),
|
|
||||||
base-files (= 1:2026.1.0),
|
|
||||||
base-passwd (= 3.6.8),
|
|
||||||
bash (= 5.3-1),
|
|
||||||
binutils (= 2.45.50.20251209-1+b1),
|
|
||||||
binutils-common (= 2.45.50.20251209-1+b1),
|
|
||||||
binutils-x86-64-linux-gnu (= 2.45.50.20251209-1+b1),
|
|
||||||
bsdextrautils (= 2.41.3-4),
|
|
||||||
build-essential (= 12.12),
|
|
||||||
bzip2 (= 1.0.8-6+b1),
|
|
||||||
cargo (= 1.92.0+dfsg1-2),
|
|
||||||
coreutils (= 9.7-3),
|
|
||||||
cpp (= 4:15.2.0-4),
|
|
||||||
cpp-15 (= 15.2.0-12),
|
|
||||||
cpp-15-x86-64-linux-gnu (= 15.2.0-12),
|
|
||||||
cpp-x86-64-linux-gnu (= 4:15.2.0-4),
|
|
||||||
dash (= 0.5.12-12),
|
|
||||||
debconf (= 1.5.91),
|
|
||||||
debhelper (= 13.31),
|
|
||||||
debianutils (= 5.23.2),
|
|
||||||
dh-autoreconf (= 22),
|
|
||||||
dh-strip-nondeterminism (= 1.15.0-1),
|
|
||||||
diffutils (= 1:3.12-1),
|
|
||||||
dpkg (= 1.23.3+kali1),
|
|
||||||
dpkg-dev (= 1.23.3+kali1),
|
|
||||||
dwz (= 0.16-4),
|
|
||||||
file (= 1:5.46-5+b1),
|
|
||||||
findutils (= 4.10.0-3),
|
|
||||||
g++ (= 4:15.2.0-4),
|
|
||||||
g++-15 (= 15.2.0-12),
|
|
||||||
g++-15-x86-64-linux-gnu (= 15.2.0-12),
|
|
||||||
g++-x86-64-linux-gnu (= 4:15.2.0-4),
|
|
||||||
gcc (= 4:15.2.0-4),
|
|
||||||
gcc-15 (= 15.2.0-12),
|
|
||||||
gcc-15-base (= 15.2.0-12),
|
|
||||||
gcc-15-x86-64-linux-gnu (= 15.2.0-12),
|
|
||||||
gcc-x86-64-linux-gnu (= 4:15.2.0-4),
|
|
||||||
gettext (= 0.23.2-2),
|
|
||||||
gettext-base (= 0.23.2-2),
|
|
||||||
grep (= 3.12-1),
|
|
||||||
groff-base (= 1.23.0-10),
|
|
||||||
gzip (= 1.13-1),
|
|
||||||
hostname (= 3.25),
|
|
||||||
init-system-helpers (= 1.69+kali1),
|
|
||||||
intltool-debian (= 0.35.0+20060710.6),
|
|
||||||
libacl1 (= 2.3.2-2+b2),
|
|
||||||
libarchive-zip-perl (= 1.68-1),
|
|
||||||
libasan8 (= 15.2.0-12),
|
|
||||||
libatomic1 (= 15.2.0-12),
|
|
||||||
libattr1 (= 1:2.5.2-3+b1),
|
|
||||||
libaudit-common (= 1:4.1.2-1),
|
|
||||||
libaudit1 (= 1:4.1.2-1+b1),
|
|
||||||
libbinutils (= 2.45.50.20251209-1+b1),
|
|
||||||
libblkid1 (= 2.41.3-4),
|
|
||||||
libbrotli1 (= 1.1.0-2+b9),
|
|
||||||
libbsd0 (= 0.12.2-2+b1),
|
|
||||||
libbz2-1.0 (= 1.0.8-6+b1),
|
|
||||||
libc-bin (= 2.42-5),
|
|
||||||
libc-dev-bin (= 2.42-5),
|
|
||||||
libc-gconv-modules-extra (= 2.42-5),
|
|
||||||
libc6 (= 2.42-5),
|
|
||||||
libc6-dev (= 2.42-5),
|
|
||||||
libcap-ng0 (= 0.8.5-4+b2),
|
|
||||||
libcap2 (= 1:2.75-10+b5),
|
|
||||||
libcc1-0 (= 15.2.0-12),
|
|
||||||
libcom-err2 (= 1.47.2-3+b8),
|
|
||||||
libcrypt-dev (= 1:4.5.1-1),
|
|
||||||
libcrypt1 (= 1:4.5.1-1),
|
|
||||||
libctf-nobfd0 (= 2.45.50.20251209-1+b1),
|
|
||||||
libctf0 (= 2.45.50.20251209-1+b1),
|
|
||||||
libcurl4t64 (= 8.18.0-2),
|
|
||||||
libdb5.3t64 (= 5.3.28+dfsg2-11),
|
|
||||||
libdebconfclient0 (= 0.282+b2),
|
|
||||||
libdebhelper-perl (= 13.31),
|
|
||||||
libdpkg-perl (= 1.23.3+kali1),
|
|
||||||
libedit2 (= 3.1-20251016-1),
|
|
||||||
libelf1t64 (= 0.194-4),
|
|
||||||
libffi8 (= 3.5.2-3+b1),
|
|
||||||
libfile-stripnondeterminism-perl (= 1.15.0-1),
|
|
||||||
libgcc-15-dev (= 15.2.0-12),
|
|
||||||
libgcc-s1 (= 15.2.0-12),
|
|
||||||
libgdbm-compat4t64 (= 1.26-1+b1),
|
|
||||||
libgdbm6t64 (= 1.26-1+b1),
|
|
||||||
libgit2-1.9 (= 1.9.2+ds-6),
|
|
||||||
libgmp10 (= 2:6.3.0+dfsg-5+b1),
|
|
||||||
libgnutls30t64 (= 3.8.11-3),
|
|
||||||
libgomp1 (= 15.2.0-12),
|
|
||||||
libgprofng0 (= 2.45.50.20251209-1+b1),
|
|
||||||
libgssapi-krb5-2 (= 1.22.1-2),
|
|
||||||
libhogweed6t64 (= 3.10.2-1),
|
|
||||||
libhwasan0 (= 15.2.0-12),
|
|
||||||
libidn2-0 (= 2.3.8-4+b1),
|
|
||||||
libisl23 (= 0.27-1+b1),
|
|
||||||
libitm1 (= 15.2.0-12),
|
|
||||||
libjansson4 (= 2.14-2+b4),
|
|
||||||
libk5crypto3 (= 1.22.1-2),
|
|
||||||
libkeyutils1 (= 1.6.3-6+b1),
|
|
||||||
libkrb5-3 (= 1.22.1-2),
|
|
||||||
libkrb5support0 (= 1.22.1-2),
|
|
||||||
libldap2 (= 2.6.10+dfsg-1+b1),
|
|
||||||
libllhttp9.3 (= 9.3.3~really9.3.0+~cs12.11.8-3),
|
|
||||||
libllvm21 (= 1:21.1.8-1+b1),
|
|
||||||
liblsan0 (= 15.2.0-12),
|
|
||||||
liblzma5 (= 5.8.2-2),
|
|
||||||
libmagic-mgc (= 1:5.46-5+b1),
|
|
||||||
libmagic1t64 (= 1:5.46-5+b1),
|
|
||||||
libmbedcrypto16 (= 3.6.5-0.1),
|
|
||||||
libmbedtls21 (= 3.6.5-0.1),
|
|
||||||
libmbedx509-7 (= 3.6.5-0.1),
|
|
||||||
libmd0 (= 1.1.0-2+b2),
|
|
||||||
libmount1 (= 2.41.3-4),
|
|
||||||
libmpc3 (= 1.3.1-2+b1),
|
|
||||||
libmpfr6 (= 4.2.2-2+b1),
|
|
||||||
libnettle8t64 (= 3.10.2-1),
|
|
||||||
libnghttp2-14 (= 1.64.0-1.1+b1),
|
|
||||||
libnghttp3-9 (= 1.12.0-1),
|
|
||||||
libngtcp2-16 (= 1.16.0-1),
|
|
||||||
libngtcp2-crypto-ossl0 (= 1.16.0-1),
|
|
||||||
libp11-kit0 (= 0.25.10-1+b1),
|
|
||||||
libpam-modules (= 1.7.0-5+b1),
|
|
||||||
libpam-modules-bin (= 1.7.0-5+b1),
|
|
||||||
libpam-runtime (= 1.7.0-5),
|
|
||||||
libpam0g (= 1.7.0-5+b1),
|
|
||||||
libpcre2-8-0 (= 10.46-1+b1),
|
|
||||||
libperl5.40 (= 5.40.1-7),
|
|
||||||
libpipeline1 (= 1.5.8-2),
|
|
||||||
libpkgconf7 (= 2.5.1-4),
|
|
||||||
libpsl5t64 (= 0.21.2-1.1+b2),
|
|
||||||
libquadmath0 (= 15.2.0-12),
|
|
||||||
librtmp1 (= 2.4+20151223.gitfa8646d.1-3+b1),
|
|
||||||
libsasl2-2 (= 2.1.28+dfsg1-10),
|
|
||||||
libsasl2-modules-db (= 2.1.28+dfsg1-10),
|
|
||||||
libseccomp2 (= 2.6.0-2+b1),
|
|
||||||
libselinux1 (= 3.9-4+b1),
|
|
||||||
libsframe2 (= 2.45.50.20251209-1+b1),
|
|
||||||
libsmartcols1 (= 2.41.3-4),
|
|
||||||
libsqlite3-0 (= 3.46.1-9),
|
|
||||||
libssh2-1t64 (= 1.11.1-1+b1),
|
|
||||||
libssl3t64 (= 3.5.4-1+b1),
|
|
||||||
libstd-rust-1.92 (= 1.92.0+dfsg1-2),
|
|
||||||
libstd-rust-dev (= 1.92.0+dfsg1-2),
|
|
||||||
libstdc++-15-dev (= 15.2.0-12),
|
|
||||||
libstdc++6 (= 15.2.0-12),
|
|
||||||
libsystemd-dev (= 259.1-1),
|
|
||||||
libsystemd0 (= 259.1-1),
|
|
||||||
libtasn1-6 (= 4.21.0-2),
|
|
||||||
libtinfo6 (= 6.6+20251231-1),
|
|
||||||
libtool (= 2.5.4-10),
|
|
||||||
libtsan2 (= 15.2.0-12),
|
|
||||||
libubsan1 (= 15.2.0-12),
|
|
||||||
libuchardet0 (= 0.0.8-2+b1),
|
|
||||||
libudev1 (= 259-1),
|
|
||||||
libunistring5 (= 1.3-2+b1),
|
|
||||||
libuuid1 (= 2.41.3-4),
|
|
||||||
libxml2-16 (= 2.15.1+dfsg-2+b1),
|
|
||||||
libz3-4 (= 4.13.3-1+b1),
|
|
||||||
libzstd1 (= 1.5.7+dfsg-3+b1),
|
|
||||||
linux-libc-dev (= 6.18.5-1kali1),
|
|
||||||
m4 (= 1.4.21-1),
|
|
||||||
make (= 4.4.1-3),
|
|
||||||
man-db (= 2.13.1-1),
|
|
||||||
mawk (= 1.3.4.20250131-2),
|
|
||||||
ncurses-base (= 6.6+20251231-1),
|
|
||||||
ncurses-bin (= 6.6+20251231-1),
|
|
||||||
openssl-provider-legacy (= 3.5.4-1+b1),
|
|
||||||
patch (= 2.8-2),
|
|
||||||
perl (= 5.40.1-7),
|
|
||||||
perl-base (= 5.40.1-7),
|
|
||||||
perl-modules-5.40 (= 5.40.1-7),
|
|
||||||
pkg-config (= 2.5.1-4),
|
|
||||||
pkgconf (= 2.5.1-4),
|
|
||||||
pkgconf-bin (= 2.5.1-4),
|
|
||||||
po-debconf (= 1.0.22),
|
|
||||||
rpcsvc-proto (= 1.4.3-1),
|
|
||||||
rustc (= 1.92.0+dfsg1-2),
|
|
||||||
sed (= 4.9-2),
|
|
||||||
sensible-utils (= 0.0.26),
|
|
||||||
sysvinit-utils (= 3.15-6),
|
|
||||||
tar (= 1.35+dfsg-3.1),
|
|
||||||
util-linux (= 2.41.3-4),
|
|
||||||
xz-utils (= 5.8.2-2),
|
|
||||||
zlib1g (= 1:1.3.dfsg+really1.3.1-1+b2)
|
|
||||||
Environment:
|
|
||||||
DEB_BUILD_OPTIONS="parallel=12"
|
|
||||||
LANG="en_US.UTF-8"
|
|
||||||
LANGUAGE="en_US:en"
|
|
||||||
LC_ALL="en_US.UTF-8"
|
|
||||||
SOURCE_DATE_EPOCH="1775779032"
|
|
||||||
TZ="UTC"
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
Format: 1.8
|
|
||||||
Date: Thu, 09 Apr 2026 18:57:12 -0500
|
|
||||||
Source: linux-patch-api
|
|
||||||
Binary: linux-patch-api
|
|
||||||
Architecture: amd64
|
|
||||||
Version: 1.0.0-1
|
|
||||||
Distribution: stable
|
|
||||||
Urgency: medium
|
|
||||||
Maintainer: Echo <echo@moon-dragon.us>
|
|
||||||
Changed-By: Echo <echo@moon-dragon.us>
|
|
||||||
Description:
|
|
||||||
linux-patch-api - Secure remote package management API for Linux systems
|
|
||||||
Changes:
|
|
||||||
linux-patch-api (1.0.0-1) stable; urgency=medium
|
|
||||||
.
|
|
||||||
* Initial production release
|
|
||||||
* Secure mTLS-authenticated REST API for remote package management
|
|
||||||
* 15 API endpoints for package install/remove, patch application, system management
|
|
||||||
* Asynchronous job processing with WebSocket status streaming
|
|
||||||
* IP whitelist enforcement and comprehensive audit logging
|
|
||||||
* Systemd integration with security hardening
|
|
||||||
* Supports Debian 11/12, Ubuntu 20.04/22.04/24.04
|
|
||||||
Checksums-Sha1:
|
|
||||||
6eacada3e35f2b5d4e76ca6d0dfa2d12588e235a 6044 linux-patch-api_1.0.0-1_amd64.buildinfo
|
|
||||||
29bfe7427b42f05b4c0fd886d02b2550289df356 2624992 linux-patch-api_1.0.0-1_amd64.deb
|
|
||||||
Checksums-Sha256:
|
|
||||||
1d7c683fa9bb147f11cc4b8dc949b34d2bd7bdef0e2ba0f04e66e74bab955acc 6044 linux-patch-api_1.0.0-1_amd64.buildinfo
|
|
||||||
353b49ef3f83c0bf2c556bcfc1b3e8bb46b8e629a34659d4d5b63ac25c5a80c0 2624992 linux-patch-api_1.0.0-1_amd64.deb
|
|
||||||
Files:
|
|
||||||
ab758ad6130467303e536c3aacc901a1 6044 admin optional linux-patch-api_1.0.0-1_amd64.buildinfo
|
|
||||||
a64eb068fd021dd3a559bf1429960165 2624992 admin optional linux-patch-api_1.0.0-1_amd64.deb
|
|
||||||
Binary file not shown.
147
scripts/build-package.sh
Normal file
147
scripts/build-package.sh
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# Linux Patch API — Build .deb Package for Ubuntu 24.04
|
||||||
|
# =============================================================================
|
||||||
|
# Produces: linux-patch-api_<version>-1_amd64.deb
|
||||||
|
# Prerequisites:
|
||||||
|
# - Rust toolchain (cargo, rustc >= 1.75)
|
||||||
|
# - dpkg-deb
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
VERSION="1.2.0"
|
||||||
|
RELEASE="1"
|
||||||
|
PKG_NAME="linux-patch-api"
|
||||||
|
DEB_NAME="${PKG_NAME}_${VERSION}-${RELEASE}_amd64.deb"
|
||||||
|
BUILD_DIR="${PROJECT_ROOT}/package-build"
|
||||||
|
|
||||||
|
info "=== Linux Patch API — Package Build ==="
|
||||||
|
info "Version: ${VERSION}-${RELEASE}"
|
||||||
|
info "Target: Ubuntu 24.04 (noble) amd64"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. Build Rust binary (release mode)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
info "Step 1/4: Building Rust binary (release mode)..."
|
||||||
|
cd "${PROJECT_ROOT}"
|
||||||
|
cargo build --release 2>&1 | tail -5
|
||||||
|
|
||||||
|
# Verify binary exists
|
||||||
|
[[ -f "${PROJECT_ROOT}/target/release/linux-patch-api" ]] || error "linux-patch-api not found in target/release/"
|
||||||
|
info "Rust binary built successfully."
|
||||||
|
|
||||||
|
# Strip debug symbols for smaller package
|
||||||
|
strip "${PROJECT_ROOT}/target/release/linux-patch-api" 2>/dev/null || warn "strip failed (may already be stripped)"
|
||||||
|
info "Binary stripped."
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. Assemble package directory structure
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
info "Step 2/4: Assembling package structure..."
|
||||||
|
rm -rf "${BUILD_DIR}"
|
||||||
|
mkdir -p "${BUILD_DIR}/DEBIAN"
|
||||||
|
mkdir -p "${BUILD_DIR}/usr/bin"
|
||||||
|
mkdir -p "${BUILD_DIR}/etc/linux_patch_api"
|
||||||
|
mkdir -p "${BUILD_DIR}/etc/linux_patch_api/certs"
|
||||||
|
mkdir -p "${BUILD_DIR}/lib/systemd/system"
|
||||||
|
mkdir -p "${BUILD_DIR}/var/log/linux_patch_api"
|
||||||
|
mkdir -p "${BUILD_DIR}/var/lib/linux_patch_api"
|
||||||
|
|
||||||
|
# Binary
|
||||||
|
cp "${PROJECT_ROOT}/target/release/linux-patch-api" "${BUILD_DIR}/usr/bin/linux-patch-api"
|
||||||
|
chmod 755 "${BUILD_DIR}/usr/bin/linux-patch-api"
|
||||||
|
|
||||||
|
# Systemd service
|
||||||
|
cp "${PROJECT_ROOT}/configs/linux-patch-api.service" "${BUILD_DIR}/lib/systemd/system/"
|
||||||
|
|
||||||
|
# Configuration files (live configs for admin editing)
|
||||||
|
cp "${PROJECT_ROOT}/configs/config.yaml.example" "${BUILD_DIR}/etc/linux_patch_api/config.yaml"
|
||||||
|
cp "${PROJECT_ROOT}/configs/whitelist.yaml.example" "${BUILD_DIR}/etc/linux_patch_api/whitelist.yaml"
|
||||||
|
|
||||||
|
# Example config files (referenced by postinst for first-run setup)
|
||||||
|
cp "${PROJECT_ROOT}/configs/config.yaml.example" "${BUILD_DIR}/etc/linux_patch_api/config.yaml.example"
|
||||||
|
cp "${PROJECT_ROOT}/configs/whitelist.yaml.example" "${BUILD_DIR}/etc/linux_patch_api/whitelist.yaml.example"
|
||||||
|
|
||||||
|
# Calculate installed size BEFORE generating control file
|
||||||
|
INSTALLED_SIZE=$(du -sk "${BUILD_DIR}" | cut -f1)
|
||||||
|
|
||||||
|
# Generate DEBIAN/control from scratch for dpkg-deb --build
|
||||||
|
# (debian/control uses dpkg-buildpackage substitution variables like
|
||||||
|
# ${shlibs:Depends} that dpkg-deb cannot resolve)
|
||||||
|
cat > "${BUILD_DIR}/DEBIAN/control" <<EOF
|
||||||
|
Package: linux-patch-api
|
||||||
|
Version: ${VERSION}-${RELEASE}
|
||||||
|
Architecture: amd64
|
||||||
|
Maintainer: Echo <echo@moon-dragon.us>
|
||||||
|
Installed-Size: ${INSTALLED_SIZE}
|
||||||
|
Depends: systemd, libsystemd0
|
||||||
|
Section: admin
|
||||||
|
Priority: optional
|
||||||
|
Homepage: https://github.com/Draco-Lunaris/Linux-Patch-Api
|
||||||
|
Description: Secure remote package management API for Linux systems
|
||||||
|
Linux Patch API provides a secure, mTLS-authenticated REST API for
|
||||||
|
remote package management operations including package installation
|
||||||
|
and removal, security patch application, system health monitoring,
|
||||||
|
and job queue management with WebSocket status streaming.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Conffiles
|
||||||
|
cat > "${BUILD_DIR}/DEBIAN/conffiles" << 'EOF'
|
||||||
|
/etc/linux_patch_api/config.yaml
|
||||||
|
/etc/linux_patch_api/whitelist.yaml
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Maintainer scripts
|
||||||
|
cp "${PROJECT_ROOT}/debian/postinst" "${BUILD_DIR}/DEBIAN/postinst"
|
||||||
|
cp "${PROJECT_ROOT}/debian/prerm" "${BUILD_DIR}/DEBIAN/prerm"
|
||||||
|
cp "${PROJECT_ROOT}/debian/postrm" "${BUILD_DIR}/DEBIAN/postrm"
|
||||||
|
chmod 755 "${BUILD_DIR}/DEBIAN/postinst" "${BUILD_DIR}/DEBIAN/prerm" "${BUILD_DIR}/DEBIAN/postrm"
|
||||||
|
|
||||||
|
info "Package structure assembled (${INSTALLED_SIZE} KB)."
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. Build .deb package
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
info "Step 3/4: Building .deb package..."
|
||||||
|
dpkg-deb --build "${BUILD_DIR}" "${PROJECT_ROOT}/${DEB_NAME}"
|
||||||
|
info ".deb package created: ${DEB_NAME}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4. Verify and summarize
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
info "Step 4/4: Verifying package..."
|
||||||
|
dpkg-deb --info "${PROJECT_ROOT}/${DEB_NAME}"
|
||||||
|
echo
|
||||||
|
dpkg-deb --contents "${PROJECT_ROOT}/${DEB_NAME}" | head -20 || true
|
||||||
|
echo
|
||||||
|
|
||||||
|
PKG_SIZE=$(du -h "${PROJECT_ROOT}/${DEB_NAME}" | cut -f1)
|
||||||
|
|
||||||
|
info "=== Package Build Complete ==="
|
||||||
|
info "Package: ${DEB_NAME}"
|
||||||
|
info "Size: ${PKG_SIZE}"
|
||||||
|
echo
|
||||||
|
echo -e "${CYAN}Installation instructions:${NC}"
|
||||||
|
echo " 1. Copy ${DEB_NAME} to the target Ubuntu 24.04 host"
|
||||||
|
echo " 2. Install: sudo dpkg -i ${DEB_NAME}"
|
||||||
|
echo " 3. Or with auto-deps: sudo apt install ./${DEB_NAME}"
|
||||||
|
echo " 4. Configure: /etc/linux_patch_api/config.yaml"
|
||||||
|
echo " 5. Start: systemctl enable --now linux-patch-api.service"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Cleanup build directory
|
||||||
|
rm -rf "${BUILD_DIR}"
|
||||||
|
info "Build directory cleaned up."
|
||||||
67
scripts/bump-version.sh
Executable file
67
scripts/bump-version.sh
Executable file
@ -0,0 +1,67 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Bump version across all version source files for linux_patch_api
|
||||||
|
# Usage: ./scripts/bump-version.sh <new_version> <old_version>
|
||||||
|
# Example: ./scripts/bump-version.sh 1.1.18 1.1.17
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NEW_VERSION="${1:?Usage: bump-version.sh <new_version> <old_version>}"
|
||||||
|
OLD_VERSION="${2:?Usage: bump-version.sh <new_version> <old_version>}"
|
||||||
|
|
||||||
|
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
|
||||||
|
echo "=== Bumping version from $OLD_VERSION to $NEW_VERSION ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Cargo.toml (PRIMARY)
|
||||||
|
sed -i "s/^version = \"$OLD_VERSION\"/version = \"$NEW_VERSION\"/" Cargo.toml
|
||||||
|
echo "[1/3] Cargo.toml: $OLD_VERSION -> $NEW_VERSION"
|
||||||
|
|
||||||
|
# 2. debian/changelog - Prepend new entry using temp file
|
||||||
|
TEMP_CHANGELOG=$(mktemp)
|
||||||
|
echo "linux-patch-api ($NEW_VERSION) unstable; urgency=low" > "$TEMP_CHANGELOG"
|
||||||
|
echo "" >> "$TEMP_CHANGELOG"
|
||||||
|
echo " * Release v$NEW_VERSION" >> "$TEMP_CHANGELOG"
|
||||||
|
echo "" >> "$TEMP_CHANGELOG"
|
||||||
|
echo " -- git-echo <git-echo@moon-dragon.us> $(date -R)" >> "$TEMP_CHANGELOG"
|
||||||
|
echo "" >> "$TEMP_CHANGELOG"
|
||||||
|
cat debian/changelog >> "$TEMP_CHANGELOG"
|
||||||
|
mv "$TEMP_CHANGELOG" debian/changelog
|
||||||
|
echo "[2/3] debian/changelog: Added entry for $NEW_VERSION"
|
||||||
|
|
||||||
|
# 3. install.sh - Use generic pattern to match any VERSION value
|
||||||
|
if [ -f install.sh ]; then
|
||||||
|
sed -i "s/^VERSION=\".*\"/VERSION=\"$NEW_VERSION\"/" install.sh
|
||||||
|
echo "[3/3] install.sh: -> $NEW_VERSION"
|
||||||
|
else
|
||||||
|
echo "[3/3] install.sh: Not found, skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. linux-patch-api.spec (uses VERSION_PLACEHOLDER, no update needed)
|
||||||
|
if grep -q 'VERSION_PLACEHOLDER' linux-patch-api.spec 2>/dev/null; then
|
||||||
|
echo "[4/4] linux-patch-api.spec: Uses VERSION_PLACEHOLDER (derived at build time)"
|
||||||
|
else
|
||||||
|
echo "[4/4] linux-patch-api.spec: WARNING - does not use VERSION_PLACEHOLDER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Version bump complete ==="
|
||||||
|
echo ""
|
||||||
|
echo "Verification:"
|
||||||
|
echo " Cargo.toml: $(grep '^version' Cargo.toml)"
|
||||||
|
echo " debian/changelog: $(head -1 debian/changelog)"
|
||||||
|
if [ -f install.sh ]; then
|
||||||
|
echo " install.sh: $(grep '^VERSION=' install.sh)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Stale references check:"
|
||||||
|
grep -r "$OLD_VERSION" --include='*.toml' --include='*.sh' --include='*.json' --include='changelog' --include='control' --include='*.spec' . 2>/dev/null | grep -v 'target/' | grep -v '.git/' | grep -v 'bump-version.sh' || echo " No stale references found"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Review changes: git diff"
|
||||||
|
echo " 2. Commit: git commit -am 'chore: bump version to $NEW_VERSION'"
|
||||||
|
echo " 3. Push: git push origin master"
|
||||||
|
echo " 4. Tag: git tag v$NEW_VERSION && git push origin v$NEW_VERSION"
|
||||||
|
echo " 5. Create release via Gitea API"
|
||||||
82
scripts/generate-dev-certs.sh
Executable file
82
scripts/generate-dev-certs.sh
Executable file
@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generate development/test certificates for Linux Patch API.
|
||||||
|
#
|
||||||
|
# This script creates a self-signed CA, server certificate, and client
|
||||||
|
# certificate suitable for local development and testing. It is NOT
|
||||||
|
# intended for production use.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/generate-dev-certs.sh [OUTPUT_DIR]
|
||||||
|
#
|
||||||
|
# If OUTPUT_DIR is omitted, certificates are written to configs/certs/
|
||||||
|
# relative to the repository root. The e2e Python test certs are also
|
||||||
|
# regenerated under tests/e2e/certs/.
|
||||||
|
#
|
||||||
|
# Private keys (*.key, *.key.pem) are excluded from git via .gitignore
|
||||||
|
# and must NEVER be committed to version control.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
OUTPUT_DIR="${1:-$REPO_ROOT/configs/certs}"
|
||||||
|
E2E_DIR="$REPO_ROOT/tests/e2e/certs"
|
||||||
|
|
||||||
|
DAYS_CA=3650
|
||||||
|
DAYS_CERT=365
|
||||||
|
|
||||||
|
echo "Generating development certificates..."
|
||||||
|
echo " Output dir: $OUTPUT_DIR"
|
||||||
|
echo " E2E dir: $E2E_DIR"
|
||||||
|
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
mkdir -p "$E2E_DIR"
|
||||||
|
|
||||||
|
# CA
|
||||||
|
echo "[1/6] Generating CA key and certificate..."
|
||||||
|
openssl genrsa -out "$OUTPUT_DIR/ca.key.pem" 4096 2>/dev/null
|
||||||
|
chmod 600 "$OUTPUT_DIR/ca.key.pem"
|
||||||
|
openssl req -x509 -new -nodes -key "$OUTPUT_DIR/ca.key.pem" -sha256 -days "$DAYS_CA" -out "$OUTPUT_DIR/ca.pem" -subj "/CN=LinuxPatchAPI Dev CA/O=Internal/C=US"
|
||||||
|
|
||||||
|
# Server certificate
|
||||||
|
echo "[2/6] Generating server key and certificate..."
|
||||||
|
openssl genrsa -out "$OUTPUT_DIR/server.key.pem" 2048 2>/dev/null
|
||||||
|
chmod 600 "$OUTPUT_DIR/server.key.pem"
|
||||||
|
openssl req -new -key "$OUTPUT_DIR/server.key.pem" -out "$OUTPUT_DIR/server.csr.pem" -subj "/CN=localhost/O=Internal/C=US"
|
||||||
|
openssl x509 -req -in "$OUTPUT_DIR/server.csr.pem" -CA "$OUTPUT_DIR/ca.pem" -CAkey "$OUTPUT_DIR/ca.key.pem" -CAcreateserial -out "$OUTPUT_DIR/server.pem" -days "$DAYS_CERT" -sha256
|
||||||
|
|
||||||
|
# Client certificate
|
||||||
|
echo "[3/6] Generating client key and certificate..."
|
||||||
|
openssl genrsa -out "$OUTPUT_DIR/client001.key.pem" 2048 2>/dev/null
|
||||||
|
chmod 600 "$OUTPUT_DIR/client001.key.pem"
|
||||||
|
openssl req -new -key "$OUTPUT_DIR/client001.key.pem" -out "$OUTPUT_DIR/client001.csr.pem" -subj "/CN=client001/O=Internal/C=US"
|
||||||
|
openssl x509 -req -in "$OUTPUT_DIR/client001.csr.pem" -CA "$OUTPUT_DIR/ca.pem" -CAkey "$OUTPUT_DIR/ca.key.pem" -CAcreateserial -out "$OUTPUT_DIR/client001.pem" -days "$DAYS_CERT" -sha256
|
||||||
|
|
||||||
|
# E2E test certificates
|
||||||
|
echo "[4/6] Generating e2e test CA certificate..."
|
||||||
|
cp "$OUTPUT_DIR/ca.pem" "$E2E_DIR/ca.crt"
|
||||||
|
|
||||||
|
echo "[5/6] Generating e2e test client certificate..."
|
||||||
|
openssl genrsa -out "$E2E_DIR/client.key" 2048 2>/dev/null
|
||||||
|
chmod 600 "$E2E_DIR/client.key"
|
||||||
|
openssl req -new -key "$E2E_DIR/client.key" -out "$E2E_DIR/client.csr" -subj "/CN=e2e-test-client/O=Internal/C=US"
|
||||||
|
openssl x509 -req -in "$E2E_DIR/client.csr" -CA "$OUTPUT_DIR/ca.pem" -CAkey "$OUTPUT_DIR/ca.key.pem" -CAcreateserial -out "$E2E_DIR/client.crt" -days "$DAYS_CERT" -sha256
|
||||||
|
|
||||||
|
# Cleanup CSR files
|
||||||
|
echo "[6/6] Cleaning up CSR files..."
|
||||||
|
rm -f "$OUTPUT_DIR/server.csr.pem" "$OUTPUT_DIR/client001.csr.pem" "$E2E_DIR/client.csr"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Development certificates generated successfully."
|
||||||
|
echo " CA cert: $OUTPUT_DIR/ca.pem"
|
||||||
|
echo " Server cert: $OUTPUT_DIR/server.pem"
|
||||||
|
echo " Server key: $OUTPUT_DIR/server.key.pem"
|
||||||
|
echo " Client cert: $OUTPUT_DIR/client001.pem"
|
||||||
|
echo " Client key: $OUTPUT_DIR/client001.key.pem"
|
||||||
|
echo " E2E CA cert: $E2E_DIR/ca.crt"
|
||||||
|
echo " E2E client cert: $E2E_DIR/client.crt"
|
||||||
|
echo " E2E client key: $E2E_DIR/client.key"
|
||||||
|
echo
|
||||||
|
echo "⚠ WARNING: These are development-only certificates. Do NOT use in production."
|
||||||
|
echo "⚠ Private keys (*.key, *.key.pem) are excluded from git via .gitignore."
|
||||||
@ -13,7 +13,7 @@ TAG_NAME="${1:?Usage: upload-release.sh <tag_name> <file_path>}"
|
|||||||
FILE_PATH="${2}"
|
FILE_PATH="${2}"
|
||||||
|
|
||||||
GITEA_API="${GITEA_API:-https://gitea-lxc.moon-dragon.us/api/v1}"
|
GITEA_API="${GITEA_API:-https://gitea-lxc.moon-dragon.us/api/v1}"
|
||||||
REPO="echo/linux_patch_api"
|
REPO="git-echo/linux_patch_api"
|
||||||
|
|
||||||
if [ -z "$GITEA_TOKEN" ]; then
|
if [ -z "$GITEA_TOKEN" ]; then
|
||||||
echo "Error: GITEA_TOKEN environment variable not set"
|
echo "Error: GITEA_TOKEN environment variable not set"
|
||||||
|
|||||||
@ -190,6 +190,19 @@ pub async fn rollback_job(
|
|||||||
|
|
||||||
info!(request_id = %request_id, job_id = %job_id_str, "Initiating job rollback");
|
info!(request_id = %request_id, job_id = %job_id_str, "Initiating job rollback");
|
||||||
|
|
||||||
|
// Check job queue capacity
|
||||||
|
if !job_manager.can_accept_job().await {
|
||||||
|
let response = ApiResponse::<()>::error(
|
||||||
|
"QUEUE_FULL",
|
||||||
|
"Job queue is at capacity. Please retry later.",
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return HttpResponse::TooManyRequests()
|
||||||
|
.insert_header(("Retry-After", "60"))
|
||||||
|
.json(response);
|
||||||
|
}
|
||||||
|
|
||||||
// Parse job ID
|
// Parse job ID
|
||||||
let job_id = match Uuid::parse_str(&job_id_str) {
|
let job_id = match Uuid::parse_str(&job_id_str) {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
@ -321,7 +334,7 @@ pub async fn delete_job(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure routes for job endpoints
|
/// Configure all job routes
|
||||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("/jobs")
|
web::scope("/jobs")
|
||||||
|
|||||||
@ -15,4 +15,5 @@ pub mod websocket;
|
|||||||
|
|
||||||
// Re-export commonly used types
|
// Re-export commonly used types
|
||||||
pub use packages::{ApiError, ApiResponse};
|
pub use packages::{ApiError, ApiResponse};
|
||||||
pub use websocket::{WsClientMessage, WsServerMessage};
|
// WebSocket message types are now in crate::jobs::websocket
|
||||||
|
pub use crate::jobs::websocket::{WsClientMessage, WsServerMessage};
|
||||||
|
|||||||
@ -14,29 +14,18 @@ use tracing::{error, info, warn};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::jobs::manager::{JobManager, JobOperation, JobStatus};
|
use crate::jobs::manager::{JobManager, JobOperation, JobStatus};
|
||||||
use crate::packages::{InstallOptions, Package, PackageManagerBackend, PackageSpec};
|
use crate::packages::{
|
||||||
|
validate_package_name, validate_version_string, InstallOptions, Package, PackageManagerBackend,
|
||||||
|
PackageSpec,
|
||||||
|
};
|
||||||
|
|
||||||
/// Maximum allowed length for package names
|
/// Validate all package names and versions in a request
|
||||||
const MAX_PACKAGE_NAME_LENGTH: usize = 256;
|
|
||||||
|
|
||||||
/// Validate package name: must not be empty and must not exceed max length
|
|
||||||
fn validate_package_name(name: &str) -> Result<(), String> {
|
|
||||||
if name.is_empty() {
|
|
||||||
return Err("Package name cannot be empty".to_string());
|
|
||||||
}
|
|
||||||
if name.len() > MAX_PACKAGE_NAME_LENGTH {
|
|
||||||
return Err(format!(
|
|
||||||
"Package name exceeds maximum length of {} characters",
|
|
||||||
MAX_PACKAGE_NAME_LENGTH
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate all package names in a request
|
|
||||||
fn validate_package_names(packages: &[PackageSpec]) -> Result<(), String> {
|
fn validate_package_names(packages: &[PackageSpec]) -> Result<(), String> {
|
||||||
for pkg in packages {
|
for pkg in packages {
|
||||||
validate_package_name(&pkg.name)?;
|
validate_package_name(&pkg.name)?;
|
||||||
|
if let Some(version) = &pkg.version {
|
||||||
|
validate_version_string(version)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -263,6 +252,19 @@ pub async fn install_packages(
|
|||||||
|
|
||||||
info!(request_id = %request_id, packages = ?package_names, "Installing packages");
|
info!(request_id = %request_id, packages = ?package_names, "Installing packages");
|
||||||
|
|
||||||
|
// Check job queue capacity
|
||||||
|
if !job_manager.can_accept_job().await {
|
||||||
|
let response = ApiResponse::<()>::error(
|
||||||
|
"QUEUE_FULL",
|
||||||
|
"Job queue is at capacity. Please retry later.",
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return HttpResponse::TooManyRequests()
|
||||||
|
.insert_header(("Retry-After", "60"))
|
||||||
|
.json(response);
|
||||||
|
}
|
||||||
|
|
||||||
// Create async job
|
// Create async job
|
||||||
match job_manager
|
match job_manager
|
||||||
.create_job(JobOperation::Install, package_names.clone())
|
.create_job(JobOperation::Install, package_names.clone())
|
||||||
@ -348,6 +350,19 @@ pub async fn update_package(
|
|||||||
|
|
||||||
info!(request_id = %request_id, package = %package_name, "Updating package");
|
info!(request_id = %request_id, package = %package_name, "Updating package");
|
||||||
|
|
||||||
|
// Check job queue capacity
|
||||||
|
if !job_manager.can_accept_job().await {
|
||||||
|
let response = ApiResponse::<()>::error(
|
||||||
|
"QUEUE_FULL",
|
||||||
|
"Job queue is at capacity. Please retry later.",
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return HttpResponse::TooManyRequests()
|
||||||
|
.insert_header(("Retry-After", "60"))
|
||||||
|
.json(response);
|
||||||
|
}
|
||||||
|
|
||||||
// Create async job
|
// Create async job
|
||||||
match job_manager
|
match job_manager
|
||||||
.create_job(JobOperation::Update, vec![package_name.clone()])
|
.create_job(JobOperation::Update, vec![package_name.clone()])
|
||||||
@ -431,6 +446,20 @@ pub async fn remove_package(
|
|||||||
}
|
}
|
||||||
|
|
||||||
info!(request_id = %request_id, package = %package_name, "Removing package");
|
info!(request_id = %request_id, package = %package_name, "Removing package");
|
||||||
|
|
||||||
|
// Check job queue capacity
|
||||||
|
if !job_manager.can_accept_job().await {
|
||||||
|
let response = ApiResponse::<()>::error(
|
||||||
|
"QUEUE_FULL",
|
||||||
|
"Job queue is at capacity. Please retry later.",
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return HttpResponse::TooManyRequests()
|
||||||
|
.insert_header(("Retry-After", "60"))
|
||||||
|
.json(response);
|
||||||
|
}
|
||||||
|
|
||||||
match job_manager
|
match job_manager
|
||||||
.create_job(JobOperation::Remove, vec![package_name.clone()])
|
.create_job(JobOperation::Remove, vec![package_name.clone()])
|
||||||
.await
|
.await
|
||||||
@ -495,7 +524,7 @@ pub async fn remove_package(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure routes for package endpoints
|
/// Configure all package routes
|
||||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("/packages")
|
web::scope("/packages")
|
||||||
|
|||||||
@ -11,7 +11,7 @@ use tracing::{error, info};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::jobs::manager::{JobManager, JobOperation, JobStatus};
|
use crate::jobs::manager::{JobManager, JobOperation, JobStatus};
|
||||||
use crate::packages::PackageManagerBackend;
|
use crate::packages::{validate_package_name, PackageManagerBackend};
|
||||||
|
|
||||||
use super::packages::{ApiResponse, JobResponseData};
|
use super::packages::{ApiResponse, JobResponseData};
|
||||||
|
|
||||||
@ -81,12 +81,23 @@ pub async fn apply_patches(
|
|||||||
body: web::Json<PatchApplyRequest>,
|
body: web::Json<PatchApplyRequest>,
|
||||||
backend: web::Data<Box<dyn PackageManagerBackend>>,
|
backend: web::Data<Box<dyn PackageManagerBackend>>,
|
||||||
job_manager: web::Data<JobManager>,
|
job_manager: web::Data<JobManager>,
|
||||||
|
cache_state: web::Data<crate::packages::cache::PackageCacheState>,
|
||||||
_req: HttpRequest,
|
_req: HttpRequest,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let request_id = Uuid::new_v4().to_string();
|
let request_id = Uuid::new_v4().to_string();
|
||||||
let _timestamp = Utc::now().to_rfc3339();
|
let _timestamp = Utc::now().to_rfc3339();
|
||||||
let packages_count = body.packages.as_ref().map(|p| p.len()).unwrap_or(0);
|
let packages_count = body.packages.as_ref().map(|p| p.len()).unwrap_or(0);
|
||||||
|
|
||||||
|
// SECURITY: Validate all package names in the request to prevent argument injection
|
||||||
|
if let Some(ref pkgs) = body.packages {
|
||||||
|
for pkg in pkgs {
|
||||||
|
if let Err(e) = validate_package_name(pkg) {
|
||||||
|
let response = ApiResponse::<()>::error("VALIDATION_ERROR", &e, None, false);
|
||||||
|
return HttpResponse::BadRequest().json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
request_id = %request_id,
|
request_id = %request_id,
|
||||||
packages = ?body.packages,
|
packages = ?body.packages,
|
||||||
@ -94,6 +105,19 @@ pub async fn apply_patches(
|
|||||||
"Applying patches"
|
"Applying patches"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check job queue capacity
|
||||||
|
if !job_manager.can_accept_job().await {
|
||||||
|
let response = ApiResponse::<()>::error(
|
||||||
|
"QUEUE_FULL",
|
||||||
|
"Job queue is at capacity. Please retry later.",
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return HttpResponse::TooManyRequests()
|
||||||
|
.insert_header(("Retry-After", "60"))
|
||||||
|
.json(response);
|
||||||
|
}
|
||||||
|
|
||||||
// Create async job
|
// Create async job
|
||||||
let package_list = body.packages.clone().unwrap_or_default();
|
let package_list = body.packages.clone().unwrap_or_default();
|
||||||
match job_manager
|
match job_manager
|
||||||
@ -104,6 +128,7 @@ pub async fn apply_patches(
|
|||||||
// Spawn background task to execute the patching
|
// Spawn background task to execute the patching
|
||||||
let backend_clone = backend.clone();
|
let backend_clone = backend.clone();
|
||||||
let job_manager_clone = job_manager.clone();
|
let job_manager_clone = job_manager.clone();
|
||||||
|
let cache_state_clone = cache_state.clone();
|
||||||
let request = body.clone();
|
let request = body.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
@ -122,8 +147,52 @@ pub async fn apply_patches(
|
|||||||
.add_job_log(&job_id_clone, "Job started".to_string())
|
.add_job_log(&job_id_clone, "Job started".to_string())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Execute patching
|
// MANDATORY: Refresh package cache before applying patches
|
||||||
match backend_clone.apply_patches(request.packages.as_deref()) {
|
let _ = job_manager_clone
|
||||||
|
.update_job(
|
||||||
|
&job_id_clone,
|
||||||
|
JobStatus::Running,
|
||||||
|
Some(0),
|
||||||
|
Some("Refreshing package index...".to_string()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let _ = job_manager_clone
|
||||||
|
.add_job_log(&job_id_clone, "Refreshing package cache...".to_string())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match backend_clone.refresh_package_cache(&cache_state_clone) {
|
||||||
|
Ok(_) => {
|
||||||
|
let _ = job_manager_clone
|
||||||
|
.add_job_log(
|
||||||
|
&job_id_clone,
|
||||||
|
"Package cache refreshed successfully".to_string(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let _ = job_manager_clone
|
||||||
|
.update_job(
|
||||||
|
&job_id_clone,
|
||||||
|
JobStatus::Running,
|
||||||
|
Some(10),
|
||||||
|
Some("Cache refreshed, applying patches...".to_string()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let err_msg = format!("Package cache refresh failed: {}", e);
|
||||||
|
error!(job_id = %job_id_clone, error = %e, "Cache refresh failed");
|
||||||
|
let _ = job_manager_clone
|
||||||
|
.add_job_log(&job_id_clone, err_msg.clone())
|
||||||
|
.await;
|
||||||
|
let _ = job_manager_clone.fail_job(&job_id_clone, err_msg).await;
|
||||||
|
return; // Exit the spawned task
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute patching with 404 retry
|
||||||
|
let packages_ref = request.packages.as_deref();
|
||||||
|
let apply_result = backend_clone.apply_patches(packages_ref);
|
||||||
|
|
||||||
|
match apply_result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let _ = job_manager_clone.complete_job(&job_id_clone).await;
|
let _ = job_manager_clone.complete_job(&job_id_clone).await;
|
||||||
info!(job_id = %job_id_clone, "Patch application completed");
|
info!(job_id = %job_id_clone, "Patch application completed");
|
||||||
@ -157,7 +226,83 @@ pub async fn apply_patches(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) if crate::packages::cache::is_fetch_error(&e) => {
|
||||||
|
// 404/fetch error: refresh cache and retry once
|
||||||
|
info!(job_id = %job_id_clone, "Patch apply failed with fetch error, refreshing cache and retrying");
|
||||||
|
let _ = job_manager_clone
|
||||||
|
.add_job_log(
|
||||||
|
&job_id_clone,
|
||||||
|
"Fetch error detected, refreshing cache and retrying..."
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match backend_clone.refresh_package_cache(&cache_state_clone) {
|
||||||
|
Ok(_) => {
|
||||||
|
let _ = job_manager_clone
|
||||||
|
.add_job_log(
|
||||||
|
&job_id_clone,
|
||||||
|
"Cache refreshed, retrying patch apply...".to_string(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Err(refresh_err) => {
|
||||||
|
let err_msg =
|
||||||
|
format!("Cache refresh on retry failed: {}", refresh_err);
|
||||||
|
let _ = job_manager_clone.fail_job(&job_id_clone, err_msg).await;
|
||||||
|
error!(job_id = %job_id_clone, error = %refresh_err, "Cache refresh on retry failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry the apply
|
||||||
|
match backend_clone.apply_patches(packages_ref) {
|
||||||
|
Ok(_) => {
|
||||||
|
let _ = job_manager_clone.complete_job(&job_id_clone).await;
|
||||||
|
info!(job_id = %job_id_clone, "Patch application completed after retry");
|
||||||
|
|
||||||
|
// Handle reboot if requested
|
||||||
|
if request.reboot {
|
||||||
|
let _ = job_manager_clone
|
||||||
|
.add_job_log(
|
||||||
|
&job_id_clone,
|
||||||
|
format!(
|
||||||
|
"Reboot scheduled in {} seconds",
|
||||||
|
request.reboot_delay_seconds
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match backend_clone.reboot_system(request.reboot_delay_seconds)
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
let _ = job_manager_clone
|
||||||
|
.add_job_log(
|
||||||
|
&job_id_clone,
|
||||||
|
"Reboot command executed".to_string(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
let _ = job_manager_clone
|
||||||
|
.add_job_log(
|
||||||
|
&job_id_clone,
|
||||||
|
format!("Reboot failed: {}", e),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(retry_err) => {
|
||||||
|
let _ = job_manager_clone
|
||||||
|
.fail_job(&job_id_clone, retry_err.to_string())
|
||||||
|
.await;
|
||||||
|
error!(job_id = %job_id_clone, error = %retry_err, "Patch application failed after retry");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Non-fetch error: fail immediately
|
||||||
let _ = job_manager_clone
|
let _ = job_manager_clone
|
||||||
.fail_job(&job_id_clone, e.to_string())
|
.fail_job(&job_id_clone, e.to_string())
|
||||||
.await;
|
.await;
|
||||||
@ -189,7 +334,7 @@ pub async fn apply_patches(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure routes for patch endpoints
|
/// Configure all patch routes
|
||||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("/patches")
|
web::scope("/patches")
|
||||||
|
|||||||
@ -12,6 +12,7 @@ use tracing::{error, info, warn};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::packages::ApiResponse;
|
use super::packages::ApiResponse;
|
||||||
|
use crate::auth::crl::{CrlStatus, SharedCrlState};
|
||||||
use crate::jobs::manager::{JobManager, JobOperation, JobStatus};
|
use crate::jobs::manager::{JobManager, JobOperation, JobStatus};
|
||||||
use crate::packages::PackageManagerBackend;
|
use crate::packages::PackageManagerBackend;
|
||||||
|
|
||||||
@ -42,9 +43,26 @@ pub struct SystemInfoData {
|
|||||||
/// Health check response data
|
/// Health check response data
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct HealthData {
|
pub struct HealthData {
|
||||||
pub status: String,
|
pub status: String, // "healthy" or "degraded"
|
||||||
pub uptime_seconds: u64,
|
pub uptime_seconds: u64,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
|
pub last_cache_update: Option<String>, // RFC3339 timestamp
|
||||||
|
pub cache_status: String, // "fresh", "stale", "unknown", "failed"
|
||||||
|
pub crl_status: Option<String>, // "valid", "expired", "missing", "invalid", "degraded"
|
||||||
|
pub crl_age_seconds: Option<u64>, // age of on-disk CRL file
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service status response data
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ServiceStatusData {
|
||||||
|
pub name: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub active_state: String,
|
||||||
|
pub sub_state: String,
|
||||||
|
pub load_state: String,
|
||||||
|
pub enabled_state: String,
|
||||||
|
pub main_pid: Option<u32>,
|
||||||
|
pub healthy: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reboot request
|
/// Reboot request
|
||||||
@ -95,7 +113,12 @@ pub async fn get_system_info(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Health check endpoint
|
/// Health check endpoint
|
||||||
pub async fn health_check(_req: HttpRequest) -> impl Responder {
|
pub async fn health_check(
|
||||||
|
backend: web::Data<Box<dyn PackageManagerBackend>>,
|
||||||
|
cache_state: web::Data<crate::packages::cache::PackageCacheState>,
|
||||||
|
crl_state: web::Data<SharedCrlState>,
|
||||||
|
_req: HttpRequest,
|
||||||
|
) -> impl Responder {
|
||||||
let _request_id = Uuid::new_v4().to_string();
|
let _request_id = Uuid::new_v4().to_string();
|
||||||
let _timestamp = Utc::now().to_rfc3339();
|
let _timestamp = Utc::now().to_rfc3339();
|
||||||
|
|
||||||
@ -113,10 +136,60 @@ pub async fn health_check(_req: HttpRequest) -> impl Responder {
|
|||||||
|
|
||||||
let version = env!("CARGO_PKG_VERSION").to_string();
|
let version = env!("CARGO_PKG_VERSION").to_string();
|
||||||
|
|
||||||
|
// Check cache status and refresh if stale
|
||||||
|
let cache_status_val = cache_state.status();
|
||||||
|
let (mut status, cache_status_str, last_cache_update) = if cache_state.is_stale() {
|
||||||
|
match backend.refresh_package_cache(&cache_state) {
|
||||||
|
Ok(_) => {
|
||||||
|
let updated = cache_state.status();
|
||||||
|
(
|
||||||
|
"healthy".to_string(),
|
||||||
|
"fresh".to_string(),
|
||||||
|
updated.last_update.map(|dt| dt.to_rfc3339()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Health check cache refresh failed: {}", e);
|
||||||
|
(
|
||||||
|
"degraded".to_string(),
|
||||||
|
"failed".to_string(),
|
||||||
|
cache_status_val.last_update.map(|dt| dt.to_rfc3339()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
"healthy".to_string(),
|
||||||
|
"fresh".to_string(),
|
||||||
|
cache_status_val.last_update.map(|dt| dt.to_rfc3339()),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// CRL status from shared state
|
||||||
|
let crl = crl_state.load();
|
||||||
|
let crl_status_str = match crl.status {
|
||||||
|
CrlStatus::Valid
|
||||||
|
| CrlStatus::Expired
|
||||||
|
| CrlStatus::Missing
|
||||||
|
| CrlStatus::Invalid
|
||||||
|
| CrlStatus::Degraded => {
|
||||||
|
// Downgrade overall health if CRL is invalid
|
||||||
|
if crl.status == CrlStatus::Invalid {
|
||||||
|
status = "degraded".to_string();
|
||||||
|
}
|
||||||
|
crl.status.to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let crl_age = crl.crl_age_seconds();
|
||||||
|
|
||||||
let response = ApiResponse::success(HealthData {
|
let response = ApiResponse::success(HealthData {
|
||||||
status: "healthy".to_string(),
|
status,
|
||||||
uptime_seconds,
|
uptime_seconds,
|
||||||
version,
|
version,
|
||||||
|
last_cache_update,
|
||||||
|
cache_status: cache_status_str,
|
||||||
|
crl_status: Some(crl_status_str),
|
||||||
|
crl_age_seconds: crl_age,
|
||||||
});
|
});
|
||||||
|
|
||||||
HttpResponse::Ok().json(response)
|
HttpResponse::Ok().json(response)
|
||||||
@ -156,6 +229,19 @@ pub async fn reboot_system(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check job queue capacity
|
||||||
|
if !job_manager.can_accept_job().await {
|
||||||
|
let response = ApiResponse::<()>::error(
|
||||||
|
"QUEUE_FULL",
|
||||||
|
"Job queue is at capacity. Please retry later.",
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return HttpResponse::TooManyRequests()
|
||||||
|
.insert_header(("Retry-After", "60"))
|
||||||
|
.json(response);
|
||||||
|
}
|
||||||
|
|
||||||
// Create async job for reboot
|
// Create async job for reboot
|
||||||
match job_manager.create_job(JobOperation::Reboot, vec![]).await {
|
match job_manager.create_job(JobOperation::Reboot, vec![]).await {
|
||||||
Ok(job_id) => {
|
Ok(job_id) => {
|
||||||
@ -228,14 +314,84 @@ pub async fn reboot_system(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get service status
|
||||||
|
pub async fn get_service_status(
|
||||||
|
path: web::Path<String>,
|
||||||
|
backend: web::Data<Box<dyn PackageManagerBackend>>,
|
||||||
|
_req: HttpRequest,
|
||||||
|
) -> impl Responder {
|
||||||
|
let request_id = Uuid::new_v4().to_string();
|
||||||
|
let service_name = path.into_inner();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
request_id = %request_id,
|
||||||
|
service = %service_name,
|
||||||
|
"Getting service status"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Validate service name
|
||||||
|
if service_name.is_empty() || service_name.contains('/') || service_name.contains("..") {
|
||||||
|
let response = ApiResponse::<()>::error(
|
||||||
|
"INVALID_SERVICE_NAME",
|
||||||
|
&format!("Invalid service name: {}", service_name),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
return HttpResponse::BadRequest().json(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
match backend.get_service_status(&service_name) {
|
||||||
|
Ok(Some(status)) => {
|
||||||
|
let response = ApiResponse::success(ServiceStatusData {
|
||||||
|
name: status.name,
|
||||||
|
display_name: status.display_name,
|
||||||
|
active_state: status.active_state,
|
||||||
|
sub_state: status.sub_state,
|
||||||
|
load_state: status.load_state,
|
||||||
|
enabled_state: status.enabled_state,
|
||||||
|
main_pid: status.main_pid,
|
||||||
|
healthy: status.healthy,
|
||||||
|
});
|
||||||
|
HttpResponse::Ok().json(response)
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
let response = ApiResponse::<()>::error(
|
||||||
|
"SERVICE_NOT_FOUND",
|
||||||
|
&format!("Service '{}' not found", service_name),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
HttpResponse::NotFound().json(response)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
request_id = %request_id,
|
||||||
|
service = %service_name,
|
||||||
|
error = %e,
|
||||||
|
"Failed to get service status"
|
||||||
|
);
|
||||||
|
let response = ApiResponse::<()>::error(
|
||||||
|
"SERVICE_STATUS_ERROR",
|
||||||
|
&format!("Failed to get service status: {}", e),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
HttpResponse::InternalServerError().json(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Configure routes for system endpoints
|
/// Configure routes for system endpoints
|
||||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("/system")
|
web::scope("/system")
|
||||||
.route("/info", web::get().to(get_system_info))
|
.route("/info", web::get().to(get_system_info))
|
||||||
.route("/reboot", web::post().to(reboot_system)),
|
.route("/reboot", web::post().to(reboot_system))
|
||||||
|
.route("/services/{name}", web::get().to(get_service_status)),
|
||||||
)
|
)
|
||||||
.route("/health", web::get().to(health_check));
|
.route("/health", web::get().to(health_check));
|
||||||
|
// Note: health_check receives backend and cache_state via app_data injection
|
||||||
|
// They are registered in routes.rs and main.rs as web::Data
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -264,9 +420,15 @@ mod tests {
|
|||||||
status: "healthy".to_string(),
|
status: "healthy".to_string(),
|
||||||
uptime_seconds: 12345,
|
uptime_seconds: 12345,
|
||||||
version: "0.1.0".to_string(),
|
version: "0.1.0".to_string(),
|
||||||
|
last_cache_update: Some("2026-05-27T14:00:00+00:00".to_string()),
|
||||||
|
cache_status: "fresh".to_string(),
|
||||||
|
crl_status: Some("valid".to_string()),
|
||||||
|
crl_age_seconds: Some(3600),
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&health).unwrap();
|
let json = serde_json::to_string(&health).unwrap();
|
||||||
assert!(json.contains("healthy"));
|
assert!(json.contains("healthy"));
|
||||||
assert!(json.contains("12345"));
|
assert!(json.contains("12345"));
|
||||||
|
assert!(json.contains("fresh"));
|
||||||
|
assert!(json.contains("last_cache_update"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,128 +3,34 @@
|
|||||||
//! Implements WebSocket endpoint for real-time job status updates:
|
//! Implements WebSocket endpoint for real-time job status updates:
|
||||||
//! - WS /api/v1/ws/jobs - Real-time job status streaming
|
//! - WS /api/v1/ws/jobs - Real-time job status streaming
|
||||||
//!
|
//!
|
||||||
//! Note: Full WebSocket implementation requires actix-web-actors compatibility.
|
//! Uses actix-web-actors for proper WebSocket handshake and protocol handling.
|
||||||
//! This stub provides the endpoint structure for future enhancement.
|
//! The actual actor logic lives in crate::jobs::websocket::WsJobActor.
|
||||||
|
|
||||||
use actix_web::{http::StatusCode, web, Error, HttpRequest, HttpResponse};
|
use actix_web::{web, Error, HttpRequest, HttpResponse};
|
||||||
use chrono::Utc;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::jobs::manager::JobManager;
|
use crate::jobs::manager::JobManager;
|
||||||
|
use crate::jobs::websocket::WsJobActor;
|
||||||
/// WebSocket message from client
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
|
||||||
#[serde(tag = "action")]
|
|
||||||
pub enum WsClientMessage {
|
|
||||||
#[serde(rename = "subscribe")]
|
|
||||||
Subscribe {
|
|
||||||
#[serde(default)]
|
|
||||||
job_id: Option<String>,
|
|
||||||
},
|
|
||||||
#[serde(rename = "unsubscribe")]
|
|
||||||
Unsubscribe { job_id: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// WebSocket message to client
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
|
||||||
pub struct WsServerMessage {
|
|
||||||
pub event: String,
|
|
||||||
pub job_id: String,
|
|
||||||
pub status: String,
|
|
||||||
pub progress: u8,
|
|
||||||
pub message: String,
|
|
||||||
pub timestamp: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WsServerMessage {
|
|
||||||
pub fn job_status(job_id: &str, status: &str, progress: u8, message: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
event: "job_status".to_string(),
|
|
||||||
job_id: job_id.to_string(),
|
|
||||||
status: status.to_string(),
|
|
||||||
progress,
|
|
||||||
message: message.to_string(),
|
|
||||||
timestamp: Utc::now().to_rfc3339(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn job_complete(job_id: &str, status: &str, message: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
event: "job_complete".to_string(),
|
|
||||||
job_id: job_id.to_string(),
|
|
||||||
status: status.to_string(),
|
|
||||||
progress: 100,
|
|
||||||
message: message.to_string(),
|
|
||||||
timestamp: Utc::now().to_rfc3339(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle WebSocket connection request
|
/// Handle WebSocket connection request
|
||||||
/// Returns upgrade response for WebSocket handshake
|
/// Performs the WebSocket handshake and spawns a WsJobActor
|
||||||
|
/// that streams job status events to the connected client.
|
||||||
pub async fn websocket_handler(
|
pub async fn websocket_handler(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
_job_manager: web::Data<JobManager>,
|
stream: web::Payload,
|
||||||
|
job_manager: web::Data<JobManager>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let ws_id = Uuid::new_v4();
|
info!("WebSocket connection request received");
|
||||||
info!(ws_id = %ws_id, "WebSocket connection request");
|
|
||||||
|
|
||||||
// Check if this is a WebSocket upgrade request
|
// Subscribe to job status events from the JobManager broadcast channel
|
||||||
if req
|
let event_rx = job_manager.subscribe();
|
||||||
.headers()
|
|
||||||
.get("upgrade")
|
|
||||||
.and_then(|v| v.to_str().ok())
|
|
||||||
.map(|v| v.eq_ignore_ascii_case("websocket"))
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
// WebSocket upgrade requested
|
|
||||||
// In full implementation, this would use actix-web-actors::ws::start()
|
|
||||||
// For now, return a response indicating WebSocket support
|
|
||||||
|
|
||||||
let response_msg = serde_json::json!({
|
// Create the WebSocket actor with the broadcast receiver
|
||||||
"event": "connected",
|
let actor = WsJobActor::new(event_rx);
|
||||||
"ws_id": ws_id.to_string(),
|
|
||||||
"timestamp": Utc::now().to_rfc3339(),
|
|
||||||
"message": "WebSocket endpoint ready. Full implementation requires actix-web-actors compatibility.",
|
|
||||||
"polling_alternative": "Use GET /api/v1/jobs/{id} for job status polling"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return HTTP 101 Switching Protocols for WebSocket upgrade
|
// Perform the WebSocket handshake and start the actor
|
||||||
// In production, this would be handled by actix-web-actors
|
// This computes the proper Sec-WebSocket-Accept header and upgrades the connection
|
||||||
Ok(HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS)
|
actix_web_actors::ws::start(actor, &req, stream)
|
||||||
.insert_header(("upgrade", "websocket"))
|
|
||||||
.insert_header(("connection", "upgrade"))
|
|
||||||
.json(response_msg))
|
|
||||||
} else {
|
|
||||||
// Not a WebSocket request - return info about the endpoint
|
|
||||||
let info_msg = serde_json::json!({
|
|
||||||
"endpoint": "/api/v1/ws/jobs",
|
|
||||||
"method": "GET",
|
|
||||||
"upgrade_required": "websocket",
|
|
||||||
"headers": {
|
|
||||||
"upgrade": "websocket",
|
|
||||||
"connection": "Upgrade",
|
|
||||||
"sec-websocket-key": "<base64-key>",
|
|
||||||
"sec-websocket-version": "13"
|
|
||||||
},
|
|
||||||
"alternative": "Use GET /api/v1/jobs/{id} for job status polling"
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(info_msg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Broadcast job status update to subscribed WebSocket clients
|
|
||||||
pub async fn broadcast_job_update(
|
|
||||||
job_id: &Uuid,
|
|
||||||
status: &crate::jobs::manager::JobStatus,
|
|
||||||
progress: u8,
|
|
||||||
_message: &str,
|
|
||||||
) {
|
|
||||||
info!(job_id = %job_id, status = ?status, progress = progress, "Job status update available for broadcast");
|
|
||||||
// In production, would use a broadcast channel to notify all subscribed WebSocket clients
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure WebSocket route
|
/// Configure WebSocket route
|
||||||
@ -134,7 +40,7 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use crate::jobs::websocket::{WsClientMessage, WsServerMessage};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ws_server_message_serialization() {
|
fn test_ws_server_message_serialization() {
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
//! - WebSocket endpoint for real-time job status streaming
|
//! - WebSocket endpoint for real-time job status streaming
|
||||||
|
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
|
pub mod rate_limit;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
|
||||||
// Re-export handlers for convenience
|
// Re-export handlers for convenience
|
||||||
|
|||||||
209
src/api/rate_limit.rs
Normal file
209
src/api/rate_limit.rs
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
//! Rate Limiting Middleware
|
||||||
|
//!
|
||||||
|
//! Custom Actix-web middleware that provides per-IP rate limiting with two tiers:
|
||||||
|
//! - **Destructive tier**: POST/PUT/DELETE methods (20 req/min, burst 10 by default)
|
||||||
|
//! - **Read tier**: GET methods (120 req/min, burst 30 by default)
|
||||||
|
//! - **Health exempt**: /health, /api/v1/system/info bypass rate limiting entirely
|
||||||
|
|
||||||
|
use actix_governor::governor::clock::{Clock, DefaultClock};
|
||||||
|
use actix_governor::governor::middleware::NoOpMiddleware;
|
||||||
|
use actix_governor::governor::state::keyed::DefaultKeyedStateStore;
|
||||||
|
use actix_governor::governor::{Quota, RateLimiter};
|
||||||
|
use actix_web::body::BoxBody;
|
||||||
|
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
|
||||||
|
use actix_web::http::Method;
|
||||||
|
use actix_web::{HttpResponse, ResponseError};
|
||||||
|
use std::future::{ready, Ready};
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::num::NonZeroU32;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::config::loader::RateLimitConfig;
|
||||||
|
|
||||||
|
/// Paths exempt from rate limiting
|
||||||
|
const EXEMPT_PATHS: &[&str] = &["/health", "/api/v1/system/info"];
|
||||||
|
|
||||||
|
/// Rate limiting middleware factory
|
||||||
|
pub struct RateLimitMiddleware {
|
||||||
|
config: RateLimitConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RateLimitMiddleware {
|
||||||
|
pub fn new(config: RateLimitConfig) -> Self {
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error returned when rate limit is exceeded
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RateLimitError {
|
||||||
|
retry_after_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RateLimitError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Rate limit exceeded. Retry after {} seconds.",
|
||||||
|
self.retry_after_secs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseError for RateLimitError {
|
||||||
|
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||||
|
actix_web::http::StatusCode::TOO_MANY_REQUESTS
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_response(&self) -> HttpResponse {
|
||||||
|
HttpResponse::TooManyRequests()
|
||||||
|
.insert_header(("Retry-After", self.retry_after_secs.to_string()))
|
||||||
|
.content_type("text/plain; charset=utf-8")
|
||||||
|
.body(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type alias for per-IP rate limiter
|
||||||
|
pub type KeyedRateLimiter =
|
||||||
|
RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, DefaultClock, NoOpMiddleware>;
|
||||||
|
|
||||||
|
/// Shared rate limiter state
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RateLimiters {
|
||||||
|
/// Rate limiter for destructive operations (POST/PUT/DELETE)
|
||||||
|
destructive: Arc<KeyedRateLimiter>,
|
||||||
|
/// Rate limiter for read operations (GET)
|
||||||
|
read: Arc<KeyedRateLimiter>,
|
||||||
|
/// Whether rate limiting is enabled
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RateLimiters {
|
||||||
|
/// Build rate limiters from configuration
|
||||||
|
pub fn new(config: &RateLimitConfig) -> Self {
|
||||||
|
let destructive_quota =
|
||||||
|
Quota::per_minute(NonZeroU32::new(config.destructive_per_minute).unwrap())
|
||||||
|
.allow_burst(NonZeroU32::new(config.destructive_burst).unwrap());
|
||||||
|
|
||||||
|
let read_quota = Quota::per_minute(NonZeroU32::new(config.read_per_minute).unwrap())
|
||||||
|
.allow_burst(NonZeroU32::new(config.read_burst).unwrap());
|
||||||
|
|
||||||
|
let destructive = Arc::new(KeyedRateLimiter::keyed(destructive_quota));
|
||||||
|
let read = Arc::new(KeyedRateLimiter::keyed(read_quota));
|
||||||
|
|
||||||
|
info!(
|
||||||
|
enabled = config.enabled,
|
||||||
|
destructive_per_min = config.destructive_per_minute,
|
||||||
|
destructive_burst = config.destructive_burst,
|
||||||
|
read_per_min = config.read_per_minute,
|
||||||
|
read_burst = config.read_burst,
|
||||||
|
"Rate limiters configured"
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
destructive,
|
||||||
|
read,
|
||||||
|
enabled: config.enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a request should be rate limited
|
||||||
|
/// Returns Ok(()) if the request is allowed, Err(RateLimitError) if rate limited
|
||||||
|
pub fn check(
|
||||||
|
&self,
|
||||||
|
method: &Method,
|
||||||
|
path: &str,
|
||||||
|
peer_ip: IpAddr,
|
||||||
|
) -> Result<(), RateLimitError> {
|
||||||
|
if !self.enabled {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exempt paths bypass rate limiting entirely
|
||||||
|
if EXEMPT_PATHS.contains(&path) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let limiter = match *method {
|
||||||
|
Method::POST | Method::PUT | Method::DELETE => &self.destructive,
|
||||||
|
Method::GET => &self.read,
|
||||||
|
_ => &self.read, // Default to read tier for other methods
|
||||||
|
};
|
||||||
|
|
||||||
|
match limiter.check_key(&peer_ip) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(negative) => {
|
||||||
|
let retry_after = negative
|
||||||
|
.wait_time_from(DefaultClock::default().now())
|
||||||
|
.as_secs();
|
||||||
|
Err(RateLimitError {
|
||||||
|
retry_after_secs: retry_after.max(1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Transform<S, ServiceRequest> for RateLimitMiddleware
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = actix_web::Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<BoxBody>;
|
||||||
|
type Error = actix_web::Error;
|
||||||
|
type Transform = RateLimitService<S>;
|
||||||
|
type InitError = ();
|
||||||
|
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||||
|
|
||||||
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
|
ready(Ok(RateLimitService {
|
||||||
|
service,
|
||||||
|
limiters: RateLimiters::new(&self.config),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rate limiting service wrapper
|
||||||
|
pub struct RateLimitService<S> {
|
||||||
|
service: S,
|
||||||
|
limiters: RateLimiters,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Service<ServiceRequest> for RateLimitService<S>
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = actix_web::Error>,
|
||||||
|
S::Future: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<BoxBody>;
|
||||||
|
type Error = actix_web::Error;
|
||||||
|
type Future =
|
||||||
|
std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>>>>;
|
||||||
|
|
||||||
|
forward_ready!(service);
|
||||||
|
|
||||||
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
|
// Extract peer IP
|
||||||
|
let peer_ip = req
|
||||||
|
.connection_info()
|
||||||
|
.peer_addr()
|
||||||
|
.and_then(|addr| addr.parse::<IpAddr>().ok());
|
||||||
|
|
||||||
|
// Check rate limiting
|
||||||
|
if let Some(ip) = peer_ip {
|
||||||
|
let method = req.method().clone();
|
||||||
|
let path = req.path().to_string();
|
||||||
|
|
||||||
|
if let Err(e) = self.limiters.check(&method, &path, ip) {
|
||||||
|
// Rate limited - return 429 response
|
||||||
|
let (http_req, _) = req.into_parts();
|
||||||
|
let response = e.error_response();
|
||||||
|
let srv_resp = ServiceResponse::new(http_req, response);
|
||||||
|
return Box::pin(ready(Ok(srv_resp)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not rate limited - pass through to the inner service
|
||||||
|
Box::pin(self.service.call(req))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +1,17 @@
|
|||||||
//! API Routes Configuration
|
//! API Routes Configuration
|
||||||
//!
|
//!
|
||||||
//! Aggregates all endpoint routes and configures the Actix-web application.
|
//! Aggregates all endpoint routes and configures the Actix-web application.
|
||||||
|
//! Rate limiting is applied at the App level in main.rs using actix-governor
|
||||||
|
//! with method-based filtering:
|
||||||
|
//! - **Read tier** (120 req/min, burst 30): GET methods
|
||||||
|
//! - **Destructive tier** (20 req/min, burst 10): POST/PUT/DELETE methods
|
||||||
|
//! - **Health exempt**: /health, /api/v1/system/info (health-exempt routes)
|
||||||
|
|
||||||
use actix_web::{web, HttpResponse};
|
use actix_web::{web, HttpResponse};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::jobs::manager::JobManager;
|
use crate::jobs::manager::JobManager;
|
||||||
|
use crate::packages::cache::PackageCacheState;
|
||||||
|
|
||||||
use super::handlers::{jobs, packages, patches, system, websocket};
|
use super::handlers::{jobs, packages, patches, system, websocket};
|
||||||
|
|
||||||
@ -16,32 +22,37 @@ async fn method_not_allowed() -> HttpResponse {
|
|||||||
.insert_header(("Allow", "GET, POST, PUT, DELETE"))
|
.insert_header(("Allow", "GET, POST, PUT, DELETE"))
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure all API routes for the application
|
/// Configure all API routes for the application
|
||||||
pub fn configure_api_routes(
|
pub fn configure_api_routes(
|
||||||
cfg: &mut web::ServiceConfig,
|
cfg: &mut web::ServiceConfig,
|
||||||
job_manager: web::Data<JobManager>,
|
job_manager: web::Data<JobManager>,
|
||||||
backend: web::Data<Box<dyn crate::packages::PackageManagerBackend>>,
|
backend: web::Data<Box<dyn crate::packages::PackageManagerBackend>>,
|
||||||
|
cache_state: web::Data<PackageCacheState>,
|
||||||
) {
|
) {
|
||||||
info!("Configuring API v1 routes");
|
info!("Configuring API v1 routes");
|
||||||
|
|
||||||
cfg.app_data(job_manager).app_data(backend).service(
|
// Health-exempt endpoint: /api/v1/system/info is registered separately
|
||||||
|
// so it can bypass rate limiting applied at the App level
|
||||||
|
cfg.service(web::resource("/api/v1/system/info").route(web::get().to(system::get_system_info)));
|
||||||
|
|
||||||
|
cfg.app_data(job_manager)
|
||||||
|
.app_data(backend)
|
||||||
|
.app_data(cache_state)
|
||||||
|
.service(
|
||||||
web::scope("/api/v1")
|
web::scope("/api/v1")
|
||||||
// VULN-005: Default handler for unsupported methods returns 405 instead of 404
|
// VULN-005: Default handler for unsupported methods returns 405 instead of 404
|
||||||
.default_service(web::route().to(method_not_allowed))
|
.default_service(web::route().to(method_not_allowed))
|
||||||
// Package Management Endpoints
|
|
||||||
.configure(packages::configure_routes)
|
.configure(packages::configure_routes)
|
||||||
// Patch Management Endpoints
|
|
||||||
.configure(patches::configure_routes)
|
.configure(patches::configure_routes)
|
||||||
// System Management Endpoints
|
|
||||||
.configure(system::configure_routes)
|
.configure(system::configure_routes)
|
||||||
// Job Management Endpoints
|
|
||||||
.configure(jobs::configure_routes)
|
.configure(jobs::configure_routes)
|
||||||
// WebSocket Endpoint
|
|
||||||
.configure(websocket::configure_routes),
|
.configure(websocket::configure_routes),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Health check route (outside API scope for load balancer checks)
|
/// Health check route (outside API scope for load balancer checks)
|
||||||
|
/// Note: backend and cache_state are injected via app_data registered in main.rs
|
||||||
pub fn configure_health_route(cfg: &mut web::ServiceConfig) {
|
pub fn configure_health_route(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.route("/health", web::get().to(system::health_check));
|
cfg.route("/health", web::get().to(system::health_check));
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user