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
97 changes: 97 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,103 @@ metadata:
type: kubernetes.io/tls
```

### Custom Header in Signing Requests

The issuer supports an optional `customHeader` field in the Custom Resources of `StepIssuer` and `StepClusterIssuer` to inject a custom HTTP header into every signing request sent to the Step CA. This is useful when the CA sits behind an API gateway or proxy that requires a specific authentication header (e.g. a bearer token or an API key).

#### Configuration

Add `spec.customHeader` to your issuer manifest with two sub-fields:

| Field | Required | Description |
|-------|----------|-------------|
| `name` | yes | The HTTP header name (e.g. `Authorization`, `X-Api-Key`) |
| `value` | yes | A static string **or** a `file://` URI whose contents are read on every request |

Both `name` and `value` must be non-empty; the controller will refuse to reconcile an issuer whose `customHeader` has a blank field.

#### Static value

Use a literal string as the header value:

```yaml
spec:
customHeader:
name: X-Custom-Auth
value: "my-static-token"
```

#### Dynamic value via `file://` URI

Use a `file://` URI to read the value from a file at request time. The file is re-read on **every** signing request, so rotating the file content (e.g. via a [projected volume](https://kubernetes.io/docs/concepts/storage/projected-volumes/) ServiceAccount token) is automatically picked up without restarting the controller.

```yaml
spec:
customHeader:
name: Authorization
value: "file:///etc/step-issuer/token"
```

A typical setup mounts a projected ServiceAccount token into the pod and references it here:

```yaml
# In your Deployment
volumes:
- name: jwt-token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: my-ca-audience
volumeMounts:
- name: jwt-token
mountPath: /etc/step-issuer
readOnly: true
```

```yaml
# In the StepIssuer / StepClusterIssuer
spec:
customHeader:
name: Authorization
value: "file:///etc/step-issuer/token"
```

#### Full example

```yaml
apiVersion: certmanager.step.sm/v1beta1
kind: StepIssuer
metadata:
name: step-issuer
namespace: default
spec:
url: https://step-certificates.default.svc.cluster.local
caBundle: <base64-encoded-root-ca>
provisioner:
name: admin
kid: <provisioner-kid>
passwordRef:
name: step-certificates-provisioner-password
key: password
customHeader:
name: Authorization
value: "file:///etc/step-issuer/token"
```

The same `customHeader` field is available on `StepClusterIssuer`.

#### Implementation notes

The header injection is implemented as an `http.RoundTripper` decorator
(`CustomHeaderTransport`) that wraps the TLS transport built by `ca.WithCABundle`.
It is registered via `ca.WithTransportDecorator` so that it is compatible with
the CA bundle transport — using `ca.WithTransport` directly would conflict with
`ca.WithCABundle` since both methods claim exclusive ownership of the transport.
When a `file://` value is configured, `readHeaderValue` opens and reads the file
on every call to `RoundTrip`, meaning token rotation takes effect immediately
without any controller restart.
## Upgrading

### Migrating from kube-rbac-proxy to native metrics authentication
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta1/stepclusterissuer_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ type StepClusterIssuerSpec struct {
// to the step certificates server. If not set the system root certificates
// are used to validate the TLS connection.
CABundle []byte `json:"caBundle"`

// CustomHeader is an optional custom HTTP header to include in signing requests.
// The header value can be a static string or a file:// URI to read from dynamically.
// +optional
CustomHeader *CustomHeader `json:"customHeader,omitempty"`
}

// StepClusterIssuerStatus defines the observed state of StepClusterIssuer
Expand Down
19 changes: 19 additions & 0 deletions api/v1beta1/stepissuer_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ type StepIssuerSpec struct {
// to the step certificates server. If not set the system root certificates
// are used to validate the TLS connection.
CABundle []byte `json:"caBundle"`

// CustomHeader is an optional custom HTTP header to include in signing requests.
// The header value can be a static string or a file:// URI to read from dynamically.
// +optional
CustomHeader *CustomHeader `json:"customHeader,omitempty"`
}

// StepIssuerStatus defines the observed state of StepIssuer
Expand Down Expand Up @@ -97,6 +102,20 @@ type StepProvisioner struct {
PasswordRef StepIssuerSecretKeySelector `json:"passwordRef"`
}

// CustomHeader represents a custom HTTP header to include in signing requests.
type CustomHeader struct {
// Name is the header name (e.g., "X-Custom-Auth" or "Authorization").
// +kubebuilder:validation:MinLength=1
Name string `json:"name"`

// Value is the header value. It can be:
// - A static string value (e.g., "my-auth-token")
// - A file:// URI to read from (e.g., "file:///etc/step-issuer/token")
// The file is read fresh on each signing request.
// +kubebuilder:validation:MinLength=1
Value string `json:"value"`
}

// ConditionType represents a StepIssuer condition type.
// +kubebuilder:validation:Enum=Ready
type ConditionType string
Expand Down
25 changes: 25 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions config/crd/bases/certmanager.step.sm_stepclusterissuers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ spec:
are used to validate the TLS connection.
format: byte
type: string
customHeader:
description: |-
CustomHeader is an optional custom HTTP header to include in signing requests.
The header value can be a static string or a file:// URI to read from dynamically.
properties:
name:
description: Name is the header name (e.g., "X-Custom-Auth" or "Authorization").
minLength: 1
type: string
value:
description: |-
Value is the header value. It can be:
- A static string value (e.g., "my-auth-token")
- A file:// URI to read from (e.g., "file:///etc/step-issuer/token")
The file is read fresh on each signing request.
minLength: 1
type: string
required:
- name
- value
type: object
provisioner:
description: Provisioner contains the step certificates provisioner
configuration.
Expand Down
21 changes: 21 additions & 0 deletions config/crd/bases/certmanager.step.sm_stepissuers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ spec:
are used to validate the TLS connection.
format: byte
type: string
customHeader:
description: |-
CustomHeader is an optional custom HTTP header to include in signing requests.
The header value can be a static string or a file:// URI to read from dynamically.
properties:
name:
description: Name is the header name (e.g., "X-Custom-Auth" or "Authorization").
minLength: 1
type: string
value:
description: |-
Value is the header value. It can be:
- A static string value (e.g., "my-auth-token")
- A file:// URI to read from (e.g., "file:///etc/step-issuer/token")
The file is read fresh on each signing request.
minLength: 1
type: string
required:
- name
- value
type: object
provisioner:
description: Provisioner contains the step certificates provisioner
configuration.
Expand Down
11 changes: 10 additions & 1 deletion config/samples/stepclusterissuer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,13 @@ spec:
kid: N6I99Yuk7iGDMk_eW3QaN2admCsrC9UuDN27dlFXUOs
passwordRef:
name: step-certificates-provisioner-password
key: password
key: password
# Optional custom header to include in signing requests.
# Example with file reference (e.g., JWT token from projected volume):
customHeader:
name: Authorization
value: "file:///etc/step-issuer/jwt-token"
# Example with static value:
# customHeader:
# name: X-Custom-Auth
# value: "my-static-auth-token"
11 changes: 10 additions & 1 deletion config/samples/stepissuer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,13 @@ spec:
kid: N6I99Yuk7iGDMk_eW3QaN2admCsrC9UuDN27dlFXUOs
passwordRef:
name: step-certificates-provisioner-password
key: password
key: password
# Optional custom header to include in signing requests.
# Example with static value:
customHeader:
name: X-Custom-Auth
value: "my-static-auth-token"
# Example with file reference (e.g., mounted via projected volume):
# customHeader:
# name: Authorization
# value: "file:///etc/step-issuer/jwt-token"
9 changes: 9 additions & 0 deletions controllers/stepclusterissuer_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@ func validateStepClusterIssuerSpec(s api.StepClusterIssuerSpec) error {
case s.Provisioner.PasswordRef.Key == "":
return fmt.Errorf("spec.provisioner.passwordRef.key cannot be empty")
default:
// Validate custom header if present
if s.CustomHeader != nil {
if s.CustomHeader.Name == "" {
return fmt.Errorf("spec.customHeader.name cannot be empty")
}
if s.CustomHeader.Value == "" {
return fmt.Errorf("spec.customHeader.value cannot be empty")
}
}
return nil
}
}
9 changes: 9 additions & 0 deletions controllers/stepissuer_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ func validateStepIssuerSpec(s api.StepIssuerSpec) error {
case s.Provisioner.PasswordRef.Key == "":
return fmt.Errorf("spec.provisioner.passwordRef.key cannot be empty")
default:
// Validate custom header if present
if s.CustomHeader != nil {
if s.CustomHeader.Name == "" {
return fmt.Errorf("spec.customHeader.name cannot be empty")
}
if s.CustomHeader.Value == "" {
return fmt.Errorf("spec.customHeader.value cannot be empty")
}
}
return nil
}
}
Expand Down
Loading