diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ec6e06..f0fd39f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,23 +13,17 @@ jobs: runs-on: ubuntu-latest name: Build test steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - run: npx @dappnode/dappnodesdk build --skip_save - env: - # Read access to the private dappnode-nexus-sdk repository. The - # default GITHUB_TOKEN is scoped to this repository only and cannot - # fetch the proxy source. - NEXUS_SDK_TOKEN: ${{ secrets.NEXUS_SDK_TOKEN }} release: name: Release runs-on: ubuntu-latest if: github.event_name == 'push' || github.event_name == 'repository_dispatch' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Publish run: npx @dappnode/dappnodesdk publish patch --dappnode_team_preset --timeout 2h env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NEXUS_SDK_TOKEN: ${{ secrets.NEXUS_SDK_TOKEN }} DEVELOPER_ADDRESS: "0xf35960302a07022aba880dffaec2fdd64d5bf1c1" diff --git a/Dockerfile b/Dockerfile index 7e39ad3..cd63364 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1.7 -ARG GO_IMAGE=golang:1.26.4-alpine@sha256:3ad57304ad93bbec8548a0437ad9e06a455660655d9af011d58b993f6f615648 +ARG GO_IMAGE=golang:1.26.8-alpine@sha256:34efdd6036c92e155c8b0162a5da7626586b612ea636590035602c970eece564 ARG RUNTIME_IMAGE=gcr.io/distroless/static-debian12:nonroot@sha256:1b7b9f0f0e0a1d2155f531db587cc48ec26aaf97ab64364225f5bf18a054e66a FROM ${GO_IMAGE} AS build @@ -15,13 +15,7 @@ RUN apk add --no-cache ca-certificates git WORKDIR /src RUN git init \ && git remote add origin https://github.com/dappnode/dappnode-nexus-sdk.git -RUN --mount=type=secret,id=github_token \ - if test -s /run/secrets/github_token; then \ - auth_header="$(printf 'x-access-token:%s' "$(cat /run/secrets/github_token)" | base64 | tr -d '\n')"; \ - git -c http.extraHeader="Authorization: Basic ${auth_header}" fetch --depth=1 origin "${UPSTREAM_VERSION}"; \ - else \ - git fetch --depth=1 origin "${UPSTREAM_VERSION}"; \ - fi \ +RUN git fetch --depth=1 origin "${UPSTREAM_VERSION}" \ && git checkout --detach FETCH_HEAD \ && test "$(git rev-parse HEAD)" = "${UPSTREAM_VERSION}" @@ -42,6 +36,7 @@ FROM ${RUNTIME_IMAGE} COPY --from=build /out/nexus-proxy /usr/local/bin/nexus-proxy COPY --from=build /out/nexus-proxy-healthcheck /usr/local/bin/nexus-proxy-healthcheck COPY --from=build --chown=nonroot:nonroot /out/state /var/lib/nexus-proxy +COPY --from=build /src/LICENSE /usr/share/doc/nexus-sdk/LICENSE COPY nexus-gateway-policy.json /etc/nexus/nexus-gateway-policy.json COPY THIRD_PARTY_NOTICES.md /usr/share/doc/nexus-local-proxy/THIRD_PARTY_NOTICES.md diff --git a/README.md b/README.md index 62ec801..5972493 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,74 @@ # Nexus Local Proxy for DAppNode -This DAppNode package runs one shared Nexus proxy for OpenAI-compatible -applications installed on the same DAppNode. The proxy verifies fresh AWS -Nitro attestation against an independently pinned policy and encrypts chat -request and response bodies with EHBP before they cross Cloudflare. +Private, OpenAI-compatible access to Nexus for applications on your DAppNode. -## Client configuration +This package runs a shared local instance of the DAppNode Nexus SDK for +applications installed on the same DAppNode. It verifies the Nexus Gateway +before accepting traffic and protects prompt and response bodies on their way +to and from Nexus. -Configure DAppNode applications with: +## Connect an application + +After installing **Nexus Local Proxy**, configure applications on the same +DAppNode with: ```text Base URL: http://nexus-local-proxy.dappnode.private:3301/v1 -API key: the application's normal Nexus API key +API key: your Nexus API key API: OpenAI Chat Completions ``` -The API key remains in the normal `Authorization: Bearer ...` header. The -proxy does not store it and does not automatically retry inference requests. +Create and manage API keys at +[nexus.dappnode.com](https://nexus.dappnode.com). Applications can use +`POST /v1/chat/completions` for regular or streaming responses and +`GET /v1/models` to list the available models. -Applications may declare the package dependency once and then receive proxy -and trust-policy updates independently: +An application can declare the package as a dependency: ```json { "dependencies": { - "nexus-local-proxy.dnp.dappnode.eth": "^0.1.0" + "nexus-local-proxy.dnp.dappnode.eth": "^0.2.0" } } ``` -`POST /v1/chat/completions` and `GET /v1/models` are OpenAI-compatible, so an -application that lists models against its configured base URL works without a -second endpoint. `GET /healthz` reports whether the local proxy is ready. - -The model catalog is the one route here that is **not** confidential. It is -public, unauthenticated, cacheable data with no prompt, completion or -credential in it, so the proxy passes it through over ordinary TLS rather than -over EHBP, and does not forward the caller's `Authorization` header. These -requests are not counted on the verification page, because nothing about them -crossed the attested channel. Add `--model-catalog=false` to the service -command to remove the route. - -## Privacy verification page +## Check your privacy connection -The package serves a page showing whether the Gateway is currently verified and -what was checked: +Open the local verification page: ```text http://nexus-local-proxy.dappnode.private:3301/verification ``` -It reports the verdict in plain language, lists the checks the proxy performed -before it would encrypt anything, shows the enclave measurements and attested -key, and lists recent requests with the attested key each one was encrypted to. -From there the raw COSE_Sign1 attestation document and its signed manifest can -be downloaded and re-checked with an independent AWS Nitro verifier. - -The page shows no prompt or completion content. The ledger behind it records -only verification evidence and per request identifiers, timing and sizes, and -holds them in memory: nothing is written to disk and history starts empty after -a restart. Because it is served on the proxy port, anything on the DAppNode -internal network can read this metadata. Add `--verification-ui=false` to the -service command to remove the page and its API. - -## Security boundary +It shows whether the Nexus service passed verification and which protected +connection handled each recent request. Verification evidence and request +metadata can be kept across package restarts; prompts and responses are never +part of that history. -Callers trust the DAppNode host, this package, and the DAppNode internal -network. The caller-to-proxy hop is ordinary HTTP and is outside EHBP. From -this local proxy to the measured Nexus Gateway process inside AWS Nitro -Enclaves, prompt and completion bodies are encrypted and integrity-protected. +## What is protected -EHBP does not hide the HTTP method, path, headers, body length, frame sizes, -timing, or bearer API key. This package does not extend the claim to a -downstream inference provider. Do not publish container port 3301 to the host -or Internet. +- Prompt and response bodies are encrypted between this package and the + verified Nexus confidential service. +- The package refuses to accept traffic when it cannot verify that service. +- Prompt and response content is not written to verification history or logs. -## Current pin +The DAppNode host and internal network remain trusted. Request metadata, +including headers, sizes, and timing, is outside the body-encryption boundary. +The protection does not extend beyond Nexus to a downstream model provider. +Port `3301` must remain private to the DAppNode network. -This package pins: +## Development -- SDK commit `7ecb47b27122d41f010d33811236328e7ce3af17` (`main`). -- Gateway release `v0.1.57`, source revision `bda15a3549b7a9fbb37004281852079e9013f73b`. -- The PCR values in `nexus-gateway-policy.json`. -- Gateway origin `https://nexus-api-tee.dappnode.com`. - -The trust policy must always describe a Gateway release actually deployed at -that origin. It is fail-closed: if no pinned release matches the running -enclave, the proxy refuses to start. Take measurements from the signed release -record rather than from the live attestation endpoint. - -`releases` accepts several entries, so a Gateway can be rolled out without -installed proxies failing closed in between. Publish a policy listing both the -outgoing and incoming release, let it reach nodes, deploy the Gateway, then -publish a policy listing only the new release. - -## Build +The package builds the Nexus SDK from the exact public commit set in +`UPSTREAM_VERSION`: ```sh -NEXUS_SDK_TOKEN="$(gh auth token)" docker compose build +docker compose build ``` -The image builds the SDK from a full Git commit, verifies the checked-out -revision matches, and runs as an unprivileged user in a minimal runtime image. - -`NEXUS_SDK_TOKEN` must hold a token with read access to the private -`dappnode-nexus-sdk` repository. `docker-compose.yml` passes it to the build as -a BuildKit secret, so it never reaches an image layer. In CI it comes from the -`NEXUS_SDK_TOKEN` repository secret; the default `GITHUB_TOKEN` cannot be used -because it is scoped to this repository only. +## License -Remove the secret, the `secrets:` blocks in `docker-compose.yml`, and this -section once the SDK repository is public: the Dockerfile already falls back to -an unauthenticated fetch when no secret is supplied. +This package is licensed under the [MIT License](LICENSE). The bundled DAppNode +Nexus SDK is licensed under Apache-2.0; dependency notices are in +[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). diff --git a/dappnode_package.json b/dappnode_package.json index c9bd7e6..1a06d60 100644 --- a/dappnode_package.json +++ b/dappnode_package.json @@ -1,11 +1,11 @@ { "name": "nexus-local-proxy.dnp.dappnode.eth", "version": "0.2.0", - "upstreamVersion": "1fcfe23b2c2b141b2f7937ebb808646b37ed7439", + "upstreamVersion": "d49c902f480ae0db34e2c0621b0ef13384e992f0", "upstreamRepo": "dappnode/dappnode-nexus-sdk", "upstreamArg": "UPSTREAM_VERSION", - "shortDescription": "Attested private AI proxy for DAppNode applications", - "description": "A shared OpenAI-compatible Nexus proxy for applications on the same DAppNode. It verifies the measured Nexus Gateway inside AWS Nitro Enclaves and encrypts prompt and completion bodies with EHBP before they cross Cloudflare. A local verification page at /verification shows the attestation evidence behind that protection and lets you export it for independent checking. GET /v1/models passes the Gateway public model catalog through so ordinary OpenAI clients can list models against the proxy.", + "shortDescription": "Private Nexus access for DAppNode applications", + "description": "Runs the DAppNode Nexus SDK as a shared OpenAI-compatible local endpoint that verifies the Nexus Gateway and protects prompt and response bodies. Includes a local verification page and model listing.", "type": "dncore", "backup": [ { @@ -28,12 +28,10 @@ "keywords": [ "ai", "confidential", - "ehbp", - "nitro-enclaves", - "attestation", "openai", + "privacy", "proxy", - "tee" + "verification" ], "author": "DAppNode Association (https://github.com/dappnode)", "repository": { diff --git a/docker-compose.yml b/docker-compose.yml index 97041dd..7dd8614 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,9 +6,7 @@ services: context: . dockerfile: Dockerfile args: - UPSTREAM_VERSION: 1fcfe23b2c2b141b2f7937ebb808646b37ed7439 - secrets: - - github_token + UPSTREAM_VERSION: d49c902f480ae0db34e2c0621b0ef13384e992f0 image: nexus-local-proxy.dnp.dappnode.eth:0.2.0 container_name: DAppNodePackage-nexus-local-proxy.dnp.dappnode.eth restart: unless-stopped @@ -48,7 +46,3 @@ services: volumes: verification_state: {} - -secrets: - github_token: - environment: NEXUS_SDK_TOKEN