Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
11 changes: 3 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}"

Expand All @@ -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

Expand Down
118 changes: 39 additions & 79 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).
12 changes: 5 additions & 7 deletions dappnode_package.json
Original file line number Diff line number Diff line change
@@ -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": [
{
Expand All @@ -28,12 +28,10 @@
"keywords": [
"ai",
"confidential",
"ehbp",
"nitro-enclaves",
"attestation",
"openai",
"privacy",
"proxy",
"tee"
"verification"
],
"author": "DAppNode Association <admin@dappnode.io> (https://github.com/dappnode)",
"repository": {
Expand Down
8 changes: 1 addition & 7 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -48,7 +46,3 @@ services:

volumes:
verification_state: {}

secrets:
github_token:
environment: NEXUS_SDK_TOKEN
Loading