Skip to content
Merged
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
28 changes: 20 additions & 8 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,6 @@ Configure general Wave application settings, such as application name, port, ano
: URL of the Seqera platform API service (default: [`https://api.cloud.seqera.io`](https://api.cloud.seqera.io)).
Can be set using the `${TOWER_ENDPOINT_URL}` environment variable.

`wave.allowAnonymous` *(required)*
: When `true`, anonymous users can access the Wave server (default: `false`).
Modify this option based on your security requirements.

`wave.container.pull.enabled` *(optional)*
: When `true`, Wave can provision a container by pulling an existing image directly, applying any container configuration on the fly (the augmentation path) (default: `true`).
Set to `false` to disable the pull path in locked-down deployments where Wave must not act as an augmenting or pass-through proxy; such requests are rejected and must instead use `freeze` mode to provision the container via an actual image build.

`wave.denyHosts` *(optional)*
: Hostname patterns to deny. Requests targeting these hosts are rejected.
Example patterns: `ngrok.app`, `ngrok-free.app`, `//localhost`.
Expand Down Expand Up @@ -69,6 +61,26 @@ Configure general Wave application settings, such as application name, port, ano
`wave.tokens.watcher.count` *(optional)*
: Maximum number of container requests processed in a single watcher cycle (default: `250`).

## Capabilities

The `wave.capabilities.*` flags are installation-level toggles for optional Wave capabilities. All default to `true` (permissive). Set a flag to `false` to lock down that capability, or enable the `strict` environment (`MICRONAUT_ENVIRONMENTS=strict`) to disable all of them at once — intended for regulated deployments where Wave must not serve images directly, broker credentials, expose its HTML pages, or accept anonymous requests.

`wave.capabilities.anonymous-access` *(optional)*
: When `true`, anonymous (unauthenticated) users can access the Wave server (default: `true`).
Set to `false` to require authenticated access. Modify this option based on your security requirements.

`wave.capabilities.ephemeral-token` *(optional)*
: When `true`, Wave can provision a container by pulling an existing image directly, applying any container configuration on the fly (the augmentation path) (default: `true`).
Set to `false` to disable this path in locked-down deployments where Wave must not act as an augmenting or pass-through proxy; such requests are rejected and must instead use `freeze` mode to provision the container via an actual image build.

`wave.capabilities.credentials-federation` *(optional)*
: When `true`, Wave brokers the resolved workspace registry credentials when proxying image pulls (default: `true`).
When `false`, Wave does not broker registry credentials on the proxy pull path, so images are pulled directly from the target registry using the caller's own credentials (for example an EC2 instance profile or IAM role). Build, inspect, and augmentation flows are unaffected.

`wave.capabilities.web-views` *(optional)*
: When `true`, the HTML view pages served under `/view/**` (build, mirror, scan, container, and inspect pages) are enabled (default: `true`).
Set to `false` to disable these pages globally, in which case all `/view/**` routes return `404`.

## Container registry

Wave uses the generic format `wave.registries.<REGISTRY_NAME>.username` and `wave.registries.<REGISTRY_NAME>.password` for registry authentication.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,18 @@ class ContainerController {
private JwtAuthStore jwtAuthStore

@Inject
@Value('${wave.allowAnonymous}')
@Value('${wave.capabilities.anonymous-access}')
private Boolean allowAnonymous

/**
* When {@code false} the container pull path (on-the-fly augmentation and pass-through proxying
* of an existing image) is disabled, and requests must use "freeze" mode to provision a container
* via an actual image build. Useful for locked-down deployments where Wave must not act as an
* augmenting/pass-through pull proxy.
* When {@code false} the ephemeral-token capability is disabled, i.e. the container pull path
* (on-the-fly augmentation and pass-through proxying of an existing image) is turned off, and
* requests must use "freeze" mode to provision a container via an actual image build. Useful for
* locked-down deployments where Wave must not act as an augmenting/pass-through pull proxy.
*/
@Inject
@Value('${wave.container.pull.enabled:true}')
private Boolean allowPull
@Value('${wave.capabilities.ephemeral-token:true}')
Comment thread
pditommaso marked this conversation as resolved.
private Boolean ephemeralToken

@Inject
@Value('${wave.server.url}')
Expand Down Expand Up @@ -573,8 +573,8 @@ class ContainerController {
// deployments; such requests must instead use "freeze" mode to build the container.
// note: a freeze request never reaches this branch because it has already been rewritten
// into a container build request by freezeService.freezeBuildRequest(..) above
if( allowPull == Boolean.FALSE )
throw new BadRequestException("Container pull is not allowed in this Wave deployment - use 'freeze' mode to provision this container")
if( ephemeralToken == Boolean.FALSE )
throw new BadRequestException("Ephemeral container provisioning is not enabled in this Wave deployment - use 'freeze' mode to provision this container")
// normalize container image
final coords = ContainerCoordinates.parse(req.containerImage)
targetImage = coords.getTargetContainer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import java.util.regex.Pattern
import groovy.transform.Canonical
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import io.micronaut.context.annotation.Requires
import io.micronaut.context.annotation.Value
import io.micronaut.core.annotation.Nullable
import io.micronaut.http.HttpRequest
Expand Down Expand Up @@ -74,6 +75,7 @@ import static io.seqera.wave.util.DataTimeUtils.formatTimestamp
*/
@Slf4j
@CompileStatic
@Requires(property = 'wave.capabilities.web-views', value = 'true', defaultValue = 'true')
@Controller("/view")
@ExecuteOn(TaskExecutors.BLOCKING)
class ViewController {
Expand Down
15 changes: 15 additions & 0 deletions src/main/groovy/io/seqera/wave/core/RegistryProxyService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import io.micronaut.cache.annotation.Cacheable
import io.micronaut.context.annotation.Context
import io.micronaut.context.annotation.Value
import io.micronaut.core.io.buffer.ByteBuffer
import io.micronaut.http.client.annotation.Client
import io.micronaut.reactor.http.client.ReactorStreamingHttpClient
Expand Down Expand Up @@ -82,6 +83,16 @@ class RegistryProxyService {
@Inject
private RegistryCredentialsProvider credentialsProvider

/**
* When {@code true} (default) Wave brokers the resolved registry credentials on the proxy pull
* path (credentials federation). When {@code false} no credentials are injected, so images are
* expected to be pulled directly from the target registry using the caller's own credentials
* (for example an EC2 instance profile or IAM role). Build, inspect and augmentation flows are
* unaffected.
*/
@Value('${wave.capabilities.credentials-federation:true}')
private boolean credentialsFederation

/**
* Service to query credentials stored into tower
*/
Expand Down Expand Up @@ -130,6 +141,10 @@ class RegistryProxyService {
}

protected RegistryCredentials getCredentials(RoutePath route) {
if( !credentialsFederation ) {
log.debug "Credentials federation disabled - skipping credentials for route path=${route.targetContainer}"
return null
}
final result = credentialsProvider.getCredentials(route, route.identity)
log.debug "Credentials for route path=${route.targetContainer}; identity=${route.identity} => ${result}"
return result
Expand Down
20 changes: 20 additions & 0 deletions src/main/resources/application-strict.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
# Configuration for locked-down / regulated Wave deployments.
#
# Enable this profile by setting MICRONAUT_ENVIRONMENTS=strict
#
# It disables all optional Wave capabilities so that the service cannot serve
# images directly, broker registry credentials, or expose the HTML view pages.
# In this mode containers must be provisioned via "freeze" mode (actual image
# builds) and pulls are expected to use the caller's own registry credentials.
#
wave:
capabilities:
# disallow anonymous (unauthenticated) access to the Wave server
anonymous-access: false
# disable the ephemeral container pull/augmentation path; requests must use "freeze" mode
ephemeral-token: false
# do not broker registry credentials on the proxy pull path; use the caller's own credentials
credentials-federation: false
# disable the HTML view pages under /view/** (return 404)
web-views: false
18 changes: 13 additions & 5 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,19 @@ micronaut:
- isAnonymous()
---
wave:
allowAnonymous: true
container:
pull:
# when false, disable the container pull/augmentation path; requests must use "freeze" mode
enabled: true
# installation-level capability toggles; all default to `true` (permissive).
# See the `strict` environment (application-strict.yml) to lock them all down.
capabilities:
# when false, anonymous (unauthenticated) users cannot access the Wave server
# (legacy alias: wave.allowAnonymous)
anonymous-access: ${wave.allowAnonymous:true}
# when false, disable the ephemeral container pull/augmentation path; requests must use "freeze" mode
ephemeral-token: true
# when false, Wave does not broker registry credentials on the proxy pull path;
# images are pulled directly from the target registry with the caller's own credentials
credentials-federation: true
# when false, the HTML view pages under /view/** are disabled globally (return 404)
web-views: true
server:
url: "${WAVE_SERVER_URL:`http://localhost:9090`}"
security:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Wave, containers provisioning service
* Copyright (c) 2023-2026, Seqera Labs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.controller

import spock.lang.Specification

import io.micronaut.context.ApplicationContext

/**
* Verify the legacy 'wave.allowAnonymous' key still drives the
* 'wave.capabilities.anonymous-access' capability for backward compatibility.
*/
class AnonymousAccessLegacyAliasTest extends Specification {

def 'legacy wave.allowAnonymous=#legacy resolves anonymous-access=#expected'() {
given:
def ctx = ApplicationContext.run(props)

expect:
ctx.getRequiredProperty('wave.capabilities.anonymous-access', Boolean) == expected

cleanup:
ctx.close()

where:
legacy | props | expected
'unset' | [:] | true // shipped default
'false' | ['wave.allowAnonymous': false] | false // legacy override honored
'true' | ['wave.allowAnonymous': true] | true
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -150,19 +150,19 @@ class ContainerControllerTest extends Specification {

def 'should reject pull request when container pull is disabled' () {
given:
def controller = new ContainerController(inclusionService: Mock(ContainerInclusionService), registryProxyService: proxyRegistry, allowPull: false)
def controller = new ContainerController(inclusionService: Mock(ContainerInclusionService), registryProxyService: proxyRegistry, ephemeralToken: false)

when: 'a plain container image (pull/augment) request is rejected'
def req = new SubmitContainerTokenRequest(containerImage: 'ubuntu:latest', containerPlatform: 'linux/amd64')
controller.makeRequestData(req, PlatformId.NULL, "")
then:
def e = thrown(BadRequestException)
e.message == "Container pull is not allowed in this Wave deployment - use 'freeze' mode to provision this container"
e.message == "Ephemeral container provisioning is not enabled in this Wave deployment - use 'freeze' mode to provision this container"
}

def 'should allow pull request when container pull is enabled' () {
given:
def controller = new ContainerController(inclusionService: Mock(ContainerInclusionService), registryProxyService: proxyRegistry, allowPull: true)
def controller = new ContainerController(inclusionService: Mock(ContainerInclusionService), registryProxyService: proxyRegistry, ephemeralToken: true)

when:
def req = new SubmitContainerTokenRequest(containerImage: 'ubuntu:latest', containerPlatform: 'linux/amd64')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Wave, containers provisioning service
* Copyright (c) 2023-2026, Seqera Labs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.controller

import spock.lang.Specification

import io.micronaut.context.ApplicationContext
import io.micronaut.context.annotation.Property
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject

/**
* Verify that the /view/** pages are disabled when 'wave.capabilities.web-views' is false.
*
* @author Gavin Elder
*/
@Property(name = 'wave.server.url', value = 'http://foo.com')
@Property(name = 'wave.capabilities.web-views', value = 'false')
@MicronautTest
class ViewControllerDisabledTest extends Specification {

@Inject
@Client("/")
HttpClient client

@Inject
ApplicationContext applicationContext

def 'should not load the view controller when views are disabled'() {
expect:
!applicationContext.containsBean(ViewController)
}

def 'should return not found for view pages when views are disabled'() {
when:
client.toBlocking().exchange(HttpRequest.GET(uri))

then:
def e = thrown(HttpClientResponseException)
e.status == HttpStatus.NOT_FOUND

where:
uri << [
'/view/builds/bd-12345',
'/view/mirrors/mr-12345',
'/view/scans/sc-12345',
'/view/containers/1234567890ab',
]
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Wave, containers provisioning service
* Copyright (c) 2023-2026, Seqera Labs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.controller

import spock.lang.Specification

import io.micronaut.context.ApplicationContext
import io.micronaut.context.annotation.Property
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject

/**
* Verify that the /view/** pages are served when 'wave.capabilities.web-views' is true.
*
* @author Gavin Elder
*/
@Property(name = 'wave.server.url', value = 'http://foo.com')
@Property(name = 'wave.capabilities.web-views', value = 'true')
@MicronautTest
class ViewControllerEnabledTest extends Specification {

@Inject
ApplicationContext applicationContext

def 'should load the view controller when views are enabled'() {
expect:
applicationContext.containsBean(ViewController)
}

}
Loading
Loading