Skip to content

Commit 5d16f86

Browse files
gavinelderclaude
andcommitted
feature: disable the ability for wave to "pull" images directly
Add a `wave.container.pull.enabled` setting (default true) that, when set to false, disables the container pull/augmentation path so that Wave no longer acts as an on-the-fly augmenting or pass-through proxy. In that mode plain container image requests are rejected and callers must use "freeze" mode to provision the container via an actual image build. The guard is applied in ContainerController.makeRequestData in the branch that resolves to ContainerRequest.Type.Container - the only path that produces an augmented/pulled container. It sits after the freeze rewrite, so freeze, build and mirror requests are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1bfc55c commit 5d16f86

4 files changed

Lines changed: 49 additions & 0 deletions

File tree

docs/configuration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ Configure general Wave application settings, such as application name, port, ano
3030
: When `true`, anonymous users can access the Wave server (default: `false`).
3131
Modify this option based on your security requirements.
3232

33+
`wave.container.pull.enabled` *(optional)*
34+
: 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`).
35+
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.
36+
3337
`wave.denyHosts` *(optional)*
3438
: Hostname patterns to deny. Requests targeting these hosts are rejected.
3539
Example patterns: `ngrok.app`, `ngrok-free.app`, `//localhost`.

src/main/groovy/io/seqera/wave/controller/ContainerController.groovy

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@ class ContainerController {
127127
@Value('${wave.allowAnonymous}')
128128
private Boolean allowAnonymous
129129

130+
/**
131+
* When {@code false} the container pull path (on-the-fly augmentation and pass-through proxying
132+
* of an existing image) is disabled, and requests must use "freeze" mode to provision a container
133+
* via an actual image build. Useful for locked-down deployments where Wave must not act as an
134+
* augmenting/pass-through pull proxy.
135+
*/
136+
@Inject
137+
@Value('${wave.container.pull.enabled:true}')
138+
private Boolean allowPull
139+
130140
@Inject
131141
@Value('${wave.server.url}')
132142
private String serverUrl
@@ -559,6 +569,12 @@ class ContainerController {
559569
type = ContainerRequest.Type.Mirror
560570
}
561571
else if( req.containerImage ) {
572+
// the container pull path (augmentation / pass-through) may be disabled in locked-down
573+
// deployments; such requests must instead use "freeze" mode to build the container.
574+
// note: a freeze request never reaches this branch because it has already been rewritten
575+
// into a container build request by freezeService.freezeBuildRequest(..) above
576+
if( allowPull == Boolean.FALSE )
577+
throw new BadRequestException("Container pull is not allowed in this Wave deployment - use 'freeze' mode to provision this container")
562578
// normalize container image
563579
final coords = ContainerCoordinates.parse(req.containerImage)
564580
targetImage = coords.getTargetContainer()

src/main/resources/application.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ micronaut:
5656
---
5757
wave:
5858
allowAnonymous: true
59+
container:
60+
pull:
61+
# when false, disable the container pull/augmentation path; requests must use "freeze" mode
62+
enabled: true
5963
server:
6064
url: "${WAVE_SERVER_URL:`http://localhost:9090`}"
6165
security:

src/test/groovy/io/seqera/wave/controller/ContainerControllerTest.groovy

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import io.seqera.service.pairing.PairingService
6060
import io.seqera.service.pairing.socket.PairingChannel
6161
import io.seqera.wave.service.persistence.PersistenceService
6262
import io.seqera.wave.service.persistence.WaveContainerRecord
63+
import io.seqera.wave.service.request.ContainerRequest
6364
import io.seqera.wave.service.request.ContainerRequestService
6465
import io.seqera.wave.service.request.TokenData
6566
import io.seqera.wave.service.validation.ValidationService
@@ -147,6 +148,30 @@ class ContainerControllerTest extends Specification {
147148

148149
}
149150

151+
def 'should reject pull request when container pull is disabled' () {
152+
given:
153+
def controller = new ContainerController(inclusionService: Mock(ContainerInclusionService), registryProxyService: proxyRegistry, allowPull: false)
154+
155+
when: 'a plain container image (pull/augment) request is rejected'
156+
def req = new SubmitContainerTokenRequest(containerImage: 'ubuntu:latest', containerPlatform: 'linux/amd64')
157+
controller.makeRequestData(req, PlatformId.NULL, "")
158+
then:
159+
def e = thrown(BadRequestException)
160+
e.message == "Container pull is not allowed in this Wave deployment - use 'freeze' mode to provision this container"
161+
}
162+
163+
def 'should allow pull request when container pull is enabled' () {
164+
given:
165+
def controller = new ContainerController(inclusionService: Mock(ContainerInclusionService), registryProxyService: proxyRegistry, allowPull: true)
166+
167+
when:
168+
def req = new SubmitContainerTokenRequest(containerImage: 'ubuntu:latest', containerPlatform: 'linux/amd64')
169+
def data = controller.makeRequestData(req, PlatformId.NULL, "")
170+
then:
171+
data.containerImage == 'docker.io/library/ubuntu:latest'
172+
data.type == ContainerRequest.Type.Container
173+
}
174+
150175
def 'should create request data with freeze mode' () {
151176
given:
152177
def freeze = Mock(FreezeService)

0 commit comments

Comments
 (0)