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
50 changes: 50 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,31 @@ time has expired, the resouce will be automatically deleted on the next reconcil
<p>Valid time units are &ldquo;s&rdquo;, &ldquo;m&rdquo;, &ldquo;h&rdquo;.</p>
</td>
</tr>
<tr>
<td>
<code>image</code><br/>
<em>
string
</em>
</td>
<td>
<p>Image optionally overrides the container image used for the &ldquo;default&rdquo;
container of the Pod that Oz launches. This lets a developer run a
purpose-built debugging, shell or migration image without an
administrator having to author a dedicated <code>PodAccessTemplate</code> for it.</p>
<p>The image must match one of the patterns that the Oz controller was
deployed with (<code>--allowed-image-patterns</code>, set through the
<code>controllerManager.manager.allowedImagePatterns</code> Helm value). If the
controller has no patterns configured then image overrides are disabled
entirely and this field is rejected.</p>
<p>The rest of the PodSpec - volumes, environment, service account - is
still inherited from the template&rsquo;s target controller, so the overriding
image runs with the same identity and secrets as the workload it is
standing in for. Note also that <code>imagePullSecrets</code> are inherited, so an
image from a registry the workload cannot pull from will fail to start.</p>
<p>This field is immutable; request a new <code>PodAccessRequest</code> to change it.</p>
</td>
</tr>
</table>
</td>
</tr>
Expand Down Expand Up @@ -835,6 +860,31 @@ time has expired, the resouce will be automatically deleted on the next reconcil
<p>Valid time units are &ldquo;s&rdquo;, &ldquo;m&rdquo;, &ldquo;h&rdquo;.</p>
</td>
</tr>
<tr>
<td>
<code>image</code><br/>
<em>
string
</em>
</td>
<td>
<p>Image optionally overrides the container image used for the &ldquo;default&rdquo;
container of the Pod that Oz launches. This lets a developer run a
purpose-built debugging, shell or migration image without an
administrator having to author a dedicated <code>PodAccessTemplate</code> for it.</p>
<p>The image must match one of the patterns that the Oz controller was
deployed with (<code>--allowed-image-patterns</code>, set through the
<code>controllerManager.manager.allowedImagePatterns</code> Helm value). If the
controller has no patterns configured then image overrides are disabled
entirely and this field is rejected.</p>
<p>The rest of the PodSpec - volumes, environment, service account - is
still inherited from the template&rsquo;s target controller, so the overriding
image runs with the same identity and secrets as the workload it is
standing in for. Note also that <code>imagePullSecrets</code> are inherited, so an
image from a registry the workload cannot pull from will fail to start.</p>
<p>This field is immutable; request a new <code>PodAccessRequest</code> to change it.</p>
</td>
</tr>
</tbody>
</table>
<h3 id="crds.wizardofoz.co/v1alpha1.PodAccessRequestStatus">PodAccessRequestStatus
Expand Down
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,60 @@ spec:
maxStorage: 1Gi
```

#### Overriding the container image

By default the Pod that *Oz* launches runs the same image as the workload the
[`PodAccessTemplate`][pod_access_template] points at. Sometimes that image is
not what a developer needs - a distroless production image has no shell, and a
schema migration may need tooling that is deliberately kept out of the
production build.

A [`PodAccessRequest`][pod_access_request] can therefore ask for a different
image, without an administrator authoring a dedicated template for it:

```yaml
apiVersion: crds.wizardofoz.co/v1alpha1
kind: PodAccessRequest
metadata:
name: deployment-example
spec:
templateName: deployment-example
duration: 5m
image: registry.example.com/team/debug:v1
```

or from the CLI:

```bash
$ ozctl create PodAccessRequest deployment-example \
--image registry.example.com/team/debug:v1
```

This is off by default. The cluster administrator opts in by allow-listing one
or more image patterns when installing the controller, using the
`controllerManager.manager.allowedImagePatterns` Helm value:

```yaml
controllerManager:
manager:
allowedImagePatterns:
- registry.example.com/team/*
```

`*` matches within a single path segment and `**` matches across segments, so
the pattern above permits `registry.example.com/team/debug:v1` but not
`evil.example.com/registry.example.com/team/debug:v1`. Patterns are compared
against the reference exactly as the developer wrote it - *Oz* does not expand a
bare `nginx` into `docker.io/library/nginx` - so write patterns for the fully
qualified form your developers use.

Treat this as a security boundary. Only the image is replaced; the Pod still
inherits the target workload's service account, secrets, environment and
network identity, so any image you allow-list can run code with that workload's
privileges. Restrict the patterns to registries you control. Note also that
`imagePullSecrets` are inherited from the target workload, so an image from a
registry that workload cannot pull from will simply fail to start.

#### [`ExecAccessTemplate`][exec_access_template]

### Exec Access into Existing Pods
Expand Down
1 change: 1 addition & 0 deletions charts/oz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Kubernetes: `>=1.26.0-0`

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| controllerManager.manager.allowedImagePatterns | `[]string` | `[]` | Glob patterns describing which container images a `PodAccessRequest` is allowed to select through its `spec.image` field. This lets developers launch a purpose-built debugging or migration image without an administrator authoring a dedicated `PodAccessTemplate` for it. `*` matches within a single path segment and `**` matches across segments, so `registry.example.com/team/*` permits `registry.example.com/team/debug:v1` but not `evil.example.com/registry.example.com/team/debug:v1`. Patterns are compared against the reference exactly as the developer wrote it - Oz does not expand a bare `nginx` into `docker.io/library/nginx` - so write patterns for the fully qualified form your developers use. **This is a security boundary.** An overriding image runs with the target workload's service account, secrets and network identity, so restrict this to registries you control. The default of `[]` disables image overrides entirely. |
| controllerManager.manager.image.repository | `string` | `"ghcr.io/diranged/oz"` | Docker Image repository and name to use for the controller. |
| controllerManager.manager.image.tag | `string` | `nil` | If set, overrides the .Chart.AppVersion field to set the target image version for the Oz controller. |
| controllerManager.manager.resources.limits.cpu | string | `"500m"` | |
Expand Down
3 changes: 3 additions & 0 deletions charts/oz/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ spec:
- --health-probe-bind-address=:8081
- --metrics-bind-address=:8443
- --leader-elect
{{- with .Values.controllerManager.manager.allowedImagePatterns }}
- --allowed-image-patterns={{ join "," . }}
{{- end }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
Expand Down
22 changes: 22 additions & 0 deletions charts/oz/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ controllerManager:
# target image version for the Oz controller.
tag:

# -- (`[]string`) Glob patterns describing which container images a
# `PodAccessRequest` is allowed to select through its `spec.image` field.
# This lets developers launch a purpose-built debugging or migration image
# without an administrator authoring a dedicated `PodAccessTemplate` for
# it.
#
# `*` matches within a single path segment and `**` matches across
# segments, so `registry.example.com/team/*` permits
# `registry.example.com/team/debug:v1` but not
# `evil.example.com/registry.example.com/team/debug:v1`.
#
# Patterns are compared against the reference exactly as the developer
# wrote it - Oz does not expand a bare `nginx` into
# `docker.io/library/nginx` - so write patterns for the fully qualified
# form your developers use.
#
# **This is a security boundary.** An overriding image runs with the target
# workload's service account, secrets and network identity, so restrict
# this to registries you control. The default of `[]` disables image
# overrides entirely.
allowedImagePatterns: []

resources:
limits:
cpu: 500m
Expand Down
28 changes: 28 additions & 0 deletions config/crd/bases/crds.wizardofoz.co_podaccessrequests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ spec:
jsonPath: .status.ready
name: Ready
type: boolean
- description: Overridden container image
jsonPath: .spec.image
name: Image
priority: 1
type: string
name: v1alpha1
schema:
openAPIV3Schema:
Expand Down Expand Up @@ -62,6 +67,29 @@ spec:
Valid time units are "s", "m", "h".
pattern: ^[0-9]+(s|m|h)$
type: string
image:
description: |-
Image optionally overrides the container image used for the "default"
container of the Pod that Oz launches. This lets a developer run a
purpose-built debugging, shell or migration image without an
administrator having to author a dedicated `PodAccessTemplate` for it.

The image must match one of the patterns that the Oz controller was
deployed with (`--allowed-image-patterns`, set through the
`controllerManager.manager.allowedImagePatterns` Helm value). If the
controller has no patterns configured then image overrides are disabled
entirely and this field is rejected.

The rest of the PodSpec - volumes, environment, service account - is
still inherited from the template's target controller, so the overriding
image runs with the same identity and secrets as the workload it is
standing in for. Note also that `imagePullSecrets` are inherited, so an
image from a registry the workload cannot pull from will fail to start.

This field is immutable; request a new `PodAccessRequest` to change it.
maxLength: 512
pattern: ^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$
type: string
templateName:
description: |-
Defines the name of the `ExecAcessTemplate` that should be used to grant access to the target
Expand Down
9 changes: 9 additions & 0 deletions examples/pod_access_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@ metadata:
spec:
templateName: deployment-example
duration: 5m

# Optionally run a different image than the one the target workload uses -
# handy when the production image has no shell, or when you need migration
# tooling that is not baked into it.
#
# This only works if the cluster administrator allow-listed the registry via
# the controller's --allowed-image-patterns flag
# (controllerManager.manager.allowedImagePatterns in the Helm chart).
# image: registry.example.com/team/debug:v1
64 changes: 64 additions & 0 deletions internal/api/v1alpha1/pod_access_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"k8s.io/client-go/tools/clientcmd/api"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

"github.com/diranged/oz/internal/imagepolicy"
"github.com/diranged/oz/internal/testing/utils"
)

Expand Down Expand Up @@ -206,6 +207,69 @@ var _ = Describe("PodAccessRequest", Ordered, func() {
Expect(err).To(Not(HaveOccurred()))
})

It("Create with a disallowed image override...", func() {
// No policy configured - the default state of the process - so any
// image override at all must be refused.
imagepolicy.SetActive(nil)

imageRequest := &PodAccessRequest{
Spec: PodAccessRequestSpec{
TemplateName: "",
Image: "registry.example.com/team/debug:v1",
},
}
_, err = imageRequest.ValidateCreate(*admissionRequest)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("image overrides are disabled"))
})

It("Create with an allowed image override...", func() {
policy, policyErr := imagepolicy.New([]string{"registry.example.com/team/*"})
Expect(policyErr).ToNot(HaveOccurred())
imagepolicy.SetActive(policy)
DeferCleanup(func() { imagepolicy.SetActive(nil) })

imageRequest := &PodAccessRequest{
Spec: PodAccessRequestSpec{
TemplateName: "",
Image: "registry.example.com/team/debug:v1",
},
}
_, err = imageRequest.ValidateCreate(*admissionRequest)
Expect(err).ToNot(HaveOccurred())

// ... but an image outside the allow-list is still refused.
imageRequest.Spec.Image = "evil.example.com/backdoor:v1"
_, err = imageRequest.ValidateCreate(*admissionRequest)
Expect(err).To(HaveOccurred())
})

It("Update that changes the image is rejected...", func() {
oldRequest := &PodAccessRequest{
Spec: PodAccessRequestSpec{Image: "registry.example.com/team/debug:v1"},
}
newRequest := &PodAccessRequest{
Spec: PodAccessRequestSpec{Image: "registry.example.com/team/other:v2"},
}
_, err = newRequest.ValidateUpdate(*admissionRequest, oldRequest)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("spec.image is immutable"))
})

It("Update that leaves the image alone is allowed...", func() {
oldRequest := &PodAccessRequest{
Spec: PodAccessRequestSpec{Image: "registry.example.com/team/debug:v1"},
}
newRequest := &PodAccessRequest{
Spec: PodAccessRequestSpec{
Image: "registry.example.com/team/debug:v1",
Duration: "2h",
},
}
_, err = newRequest.ValidateUpdate(*admissionRequest, oldRequest)
Expect(err).ToNot(HaveOccurred())
})

It("Update without UserInfo...", func() {
requestBytes, _ := json.Marshal(request)
admissionRequest = &admission.Request{
Expand Down
25 changes: 25 additions & 0 deletions internal/api/v1alpha1/pod_access_request_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,30 @@ type PodAccessRequestSpec struct {
// +kubebuilder:validation:Optional
// +kubebuilder:validation:Pattern="^[0-9]+(s|m|h)$"
Duration string `json:"duration,omitempty"`

// Image optionally overrides the container image used for the "default"
// container of the Pod that Oz launches. This lets a developer run a
// purpose-built debugging, shell or migration image without an
// administrator having to author a dedicated `PodAccessTemplate` for it.
//
// The image must match one of the patterns that the Oz controller was
// deployed with (`--allowed-image-patterns`, set through the
// `controllerManager.manager.allowedImagePatterns` Helm value). If the
// controller has no patterns configured then image overrides are disabled
// entirely and this field is rejected.
//
// The rest of the PodSpec - volumes, environment, service account - is
// still inherited from the template's target controller, so the overriding
// image runs with the same identity and secrets as the workload it is
// standing in for. Note also that `imagePullSecrets` are inherited, so an
// image from a registry the workload cannot pull from will fail to start.
//
// This field is immutable; request a new `PodAccessRequest` to change it.
//
// +kubebuilder:validation:Optional
// +kubebuilder:validation:MaxLength=512
// +kubebuilder:validation:Pattern="^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]*$"
Image string `json:"image,omitempty"`
}

// PodAccessRequestStatus defines the observed state of AccessRequest
Expand All @@ -65,6 +89,7 @@ type PodAccessRequestStatus struct {
// +kubebuilder:printcolumn:name="Template",type="string",JSONPath=".spec.templateName",description="Access Template"
// +kubebuilder:printcolumn:name="Pod",type="string",JSONPath=".status.podName",description="Target Pod Name"
// +kubebuilder:printcolumn:name="Ready",type="boolean",JSONPath=".status.ready",description="Is request ready?"
// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=".spec.image",description="Overridden container image",priority=1
type PodAccessRequest struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Expand Down
Loading
Loading