Skip to content
Open
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
29 changes: 15 additions & 14 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1402,20 +1402,21 @@ starts a free compute job and returns jobId if succesfull

#### Parameters

| name | type | required | description |
| ----------------- | ------ | -------- | ----------------------------------------------------------------------------- |
| command | string | v | command name |
| node | string | | if not present it means current node |
| consumerAddress | string | v | consumer address |
| signature | string | v | signature (msg=String(nonce) ) |
| nonce | string | v | nonce for the request |
| datasets | object | | list of ComputeAsset to be used as inputs |
| algorithm | object | | ComputeAlgorithm definition |
| environment | string | v | compute environment to use |
| resources | object | | optional list of required resources |
| metadata | object | | optional metadata for the job, data provided by the user |
| additionalViewers | object | | optional array of addresses that are allowed to fetch the result |
| queueMaxWaitTime | number | | optional max time in seconds a job can wait in the queue before being started |
| name | type | required | description |
| --------------------------- | ------ | -------------------------------------------- | ----------------------------------------------------------------------------- |
| command | string | v | command name |
| node | string | | if not present it means current node |
| consumerAddress | string | v | consumer address |
| signature | string | v | signature (msg=String(nonce) ) |
| nonce | string | v | nonce for the request |
| datasets | object | | list of ComputeAsset to be used as inputs |
| algorithm | object | | ComputeAlgorithm definition |
| environment | string | v | compute environment to use |
| resources | object | | optional list of required resources |
| metadata | object | | optional metadata for the job, data provided by the user |
| additionalViewers | object | | optional array of addresses that are allowed to fetch the result |
| queueMaxWaitTime | number | | optional max time in seconds a job can wait in the queue before being started |
| encryptedDockerRegistryAuth | string | Ecies encrypted docker auth schema for image (see [Private Docker Registries with Per-Job Authentication](../env.md#private-docker-registries-with-per-job-authentication)) |

#### Request

Expand Down
167 changes: 167 additions & 0 deletions docs/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,170 @@ The `DOCKER_COMPUTE_ENVIRONMENTS` environment variable should be a JSON array of
- For GitLab Container Registry, use a personal access token (PAT) or deploy token.
- The registry URL must match exactly (including protocol) with the registry used in the Docker image reference.
- If no credentials are configured for a registry, the node will attempt unauthenticated access (works for public images only).

---

## Private Docker Registries with Per-Job Authentication

In addition to node-level registry authentication via `DOCKER_REGISTRY_AUTHS`, you can provide encrypted Docker registry authentication credentials on a per-job basis. This allows different users to use different private registries or credentials for their compute jobs.

### Overview

The `encryptedDockerRegistryAuth` parameter allows you to securely provide Docker registry credentials that are:

- Encrypted using ECIES (Elliptic Curve Integrated Encryption Scheme) with the node's public key
- Validated to ensure proper format (either `auth` string OR `username`+`password`)
- Used only for the specific compute job, overriding node-level configuration if provided

### Encryption Format

The `encryptedDockerRegistryAuth` must be:

1. A JSON object matching the Docker registry auth schema (see below)
2. Encrypted using ECIES with the node's public key
3. Hex-encoded as a string

**Auth Schema Format:**

The decrypted JSON must follow this structure:

```json
{
"username": "myuser",
"password": "mypassword"
}
```

OR

```json
{
"auth": "base64-encoded-username:password"
}
```

OR (all fields present)

```json
{
"username": "myuser",
"password": "mypassword",
"auth": "base64-encoded-username:password"
}
```

**Validation Rules:**

- Either `auth` string must be provided (non-empty), OR
- Both `username` AND `password` must be provided (both non-empty)
- Empty strings are not accepted

### Usage Examples

#### 1. Paid Compute Start (`POST /api/services/compute`)

```json
{
"command": "startCompute",
"consumerAddress": "0x...",
"signature": "...",
"nonce": "123",
"environment": "0x...",
"algorithm": {
"meta": {
"container": {
"image": "registry.example.com/myorg/myimage:latest"
}
}
},
"datasets": [],
"payment": { ... },
"encryptedDockerRegistryAuth": "0xdeadbeef..." // ECIES encrypted hex string
}
```

#### 2. Free Compute Start (`POST /api/services/freeCompute`)

```json
{
"command": "freeStartCompute",
"consumerAddress": "0x...",
"signature": "...",
"nonce": "123",
"environment": "0x...",
"algorithm": {
"meta": {
"container": {
"image": "ghcr.io/myorg/myimage:latest"
}
}
},
"datasets": [],
"encryptedDockerRegistryAuth": "0xdeadbeef..." // ECIES encrypted hex string
}
```

#### 3. Initialize Compute

The `initialize` command accepts `encryptedDockerRegistryAuth` as part of the command payload, as it validates the image

```json
{
"command": "initialize",
"datasets": [...],
"algorithm": {
"meta": {
"container": {
"image": "registry.gitlab.com/myorg/myimage:latest"
}
}
},
"environment": "0x...",
"payment": { ... },
"consumerAddress": "0x...",
"maxJobDuration": 3600,
"encryptedDockerRegistryAuth": "0xdeadbeef..." // ECIES encrypted hex string
}
```

### Encryption Process

To create `encryptedDockerRegistryAuth`, you need to:

1. **Prepare the auth JSON object:**

```json
{
"username": "myuser",
"password": "mypassword"
}
```

2. **Get the node's public key** (available via the node's API or P2P interface)

3. **Encrypt the JSON string** using ECIES with the node's public key

4. **Hex-encode the encrypted result**

### Behavior

- **Priority**: If `encryptedDockerRegistryAuth` is provided, it takes precedence over node-level `DOCKER_REGISTRY_AUTHS` configuration for that specific job
- **Validation**: The encrypted auth is decrypted and validated before the job starts. Invalid formats will result in an error
- **Scope**: The credentials are used for:
- Validating the Docker image exists (during initialize)
- Pulling the Docker image (during job execution)
- **Security**: Credentials are encrypted and only decrypted by the node using its private key

### Error Handling

If `encryptedDockerRegistryAuth` is invalid, you'll receive an error:

- **Decryption failure**: `Invalid encryptedDockerRegistryAuth: failed to parse JSON - [error message]`
- **Schema validation failure**: `Invalid encryptedDockerRegistryAuth: Either 'auth' must be provided, or both 'username' and 'password' must be provided`

### Notes

- The `encryptedDockerRegistryAuth` parameter is optional. If not provided, the node will use `DOCKER_REGISTRY_AUTHS` configuration or attempt unauthenticated access
- The registry URL in the Docker image reference must match the registry you're authenticating to
- For Docker Hub, use `registry-1.docker.io` as the registry URL
- Credentials are stored encrypted in the job record and decrypted only when needed for image operations
1 change: 1 addition & 0 deletions src/@types/C2D/C2D.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export interface DBComputeJob extends ComputeJob {
metadata?: DBComputeJobMetadata
additionalViewers?: string[] // addresses of additional addresses that can get results
algoDuration: number // duration of the job in seconds
encryptedDockerRegistryAuth?: string
}

// make sure we keep them both in sync
Expand Down
2 changes: 2 additions & 0 deletions src/@types/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export interface ComputeInitializeCommand extends Command {
maxJobDuration: number
policyServer?: any // object to pass to policy server
queueMaxWaitTime?: number // max time in seconds a job can wait in the queue before being started
encryptedDockerRegistryAuth?: string
}

export interface FreeComputeStartCommand extends Command {
Expand All @@ -216,6 +217,7 @@ export interface FreeComputeStartCommand extends Command {
metadata?: DBComputeJobMetadata
additionalViewers?: string[] // addresses of additional addresses that can get results
queueMaxWaitTime?: number // max time in seconds a job can wait in the queue before being started
encryptedDockerRegistryAuth?: string
}
export interface PaidComputeStartCommand extends FreeComputeStartCommand {
payment: ComputePayment
Expand Down
60 changes: 58 additions & 2 deletions src/components/c2d/compute_engine_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import { Escrow } from '../core/utils/escrow.js'
import { KeyManager } from '../KeyManager/index.js'
import { dockerRegistryAuth, dockerRegistrysAuth } from '../../@types/OceanNode.js'
import { ValidateParams } from '../httpRoutes/validateCommands.js'
import { EncryptMethod } from '../../@types/fileObject.js'
import { CORE_LOGGER } from '../../utils/logging/common.js'
import { DockerRegistryAuthSchema } from '../../utils/config/schemas.js'
export abstract class C2DEngine {
private clusterConfig: C2DClusterInfo
public db: C2DDatabase
Expand Down Expand Up @@ -72,7 +75,11 @@ export abstract class C2DEngine {
}

// eslint-disable-next-line require-await
public abstract checkDockerImage(image: string, platform?: any): Promise<ValidateParams>
public abstract checkDockerImage(
image: string,
encryptedDockerRegistryAuth?: string,
platform?: any
): Promise<ValidateParams>

public abstract startComputeJob(
assets: ComputeAsset[],
Expand All @@ -86,7 +93,8 @@ export abstract class C2DEngine {
jobId: string,
metadata?: DBComputeJobMetadata,
additionalViewers?: string[],
queueMaxWaitTime?: number
queueMaxWaitTime?: number,
encryptedDockerRegistryAuth?: string
): Promise<ComputeJob[]>

public abstract stopComputeJob(
Expand Down Expand Up @@ -539,4 +547,52 @@ export abstract class C2DEngine {
}
return null
}

public async checkEncryptedDockerRegistryAuth(
encryptedDockerRegistryAuth: string
): Promise<ValidateParams> {
let decryptedDockerRegistryAuth: dockerRegistryAuth
try {
const decryptedDockerRegistryAuthBuffer = await this.keyManager.decrypt(
Uint8Array.from(Buffer.from(encryptedDockerRegistryAuth, 'hex')),
EncryptMethod.ECIES
)

// Convert decrypted buffer to string and parse as JSON
const decryptedDockerRegistryAuthString =
decryptedDockerRegistryAuthBuffer.toString()

decryptedDockerRegistryAuth = JSON.parse(decryptedDockerRegistryAuthString)
} catch (error: any) {
const errorMessage = `Invalid encryptedDockerRegistryAuth: failed to parse JSON - ${error?.message || String(error)}`
CORE_LOGGER.error(errorMessage)
return {
valid: false,
reason: errorMessage,
status: 400
}
}

// Validate using schema - ensures either auth or username+password are provided
const validationResult = DockerRegistryAuthSchema.safeParse(
decryptedDockerRegistryAuth
)
if (!validationResult.success) {
const errorMessageValidation = validationResult.error.errors
.map((err) => err.message)
.join('; ')
const errorMessage = `Invalid encryptedDockerRegistryAuth: ${errorMessageValidation}`
CORE_LOGGER.error(errorMessage)
return {
valid: false,
reason: errorMessage,
status: 400
}
}
return {
valid: true,
reason: null,
status: 200
}
}
}
Loading
Loading