Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
22 changes: 19 additions & 3 deletions deployment-configuration/helm/templates/certs/letsencrypt.yaml
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
{{- if and (not .Values.local) (not (not .Values.tls)) }}
{{- $le := .Values.ingress.letsencrypt }}
{{- if and (not .Values.local) (not (not .Values.tls)) (ne $le.enabled false) }}
{{- range $name, $data := $le.secrets }}
apiVersion: v1
kind: Secret
metadata:
name: {{ $name }}
namespace: {{ $.Values.namespace }}
type: Opaque
stringData:
{{ toYaml $data | indent 2 }}
---
{{- end }}
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: {{ printf "%s-%s" "letsencrypt" .Values.namespace }}
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: {{ .Values.ingress.letsencrypt.email }}
email: {{ $le.email }}
privateKeySecretRef:
name: tls-secret-issuer
name: {{ $le.privateKeySecretName | default "tls-secret-issuer" }}
solvers:
{{- if $le.solvers }}
{{ toYaml $le.solvers | indent 4 }}
{{- else }}
- http01:
ingress:
class: {{ .Values.ingress.ingressClass }}
{{- end }}
{{ end }}
2 changes: 1 addition & 1 deletion deployment-configuration/helm/templates/httproute.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: {{ .Values.ingress.name | quote }}
{{- if and (not .Values.local) $tls }}
{{- if and (not .Values.local) $tls (ne .Values.ingress.letsencrypt.enabled false) }}
annotations:
cert-manager.io/issuer: {{ printf "%s-%s" "letsencrypt" .Values.namespace }}
{{- end }}
Expand Down
4 changes: 2 additions & 2 deletions deployment-configuration/helm/templates/ingress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ metadata:
name: {{ $appIngressName | quote }}
annotations:
kubernetes.io/ingress.class: {{ $.Values.ingress.ingressClass }} # Deprecated by Kubernetes, however still required for GKE
{{- if and (not $.Values.local) $tls }}
{{- if and (not $.Values.local) $tls (ne $.Values.ingress.letsencrypt.enabled false) }}
kubernetes.io/tls-acme: 'true'
cert-manager.io/issuer: {{ printf "%s-%s" "letsencrypt" $.Values.namespace }}
{{- end }}
Expand Down Expand Up @@ -152,7 +152,7 @@ metadata:
name: {{ printf "%s-proxy" $appIngressName | quote }}
annotations:
kubernetes.io/ingress.class: {{ $.Values.ingress.ingressClass }} # Deprecated by Kubernetes, however still required for GKE
{{- if and (not $.Values.local) $tls }}
{{- if and (not $.Values.local) $tls (ne $.Values.ingress.letsencrypt.enabled false) }}
kubernetes.io/tls-acme: 'true'
cert-manager.io/issuer: {{ printf "%s-%s" "letsencrypt" $.Values.namespace }}
{{- end }}
Expand Down
29 changes: 29 additions & 0 deletions deployment-configuration/helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,37 @@ ingress:
# -- Enables/disables SSL redirect.
ssl_redirect: true
letsencrypt:
# -- Whether to provision a cert-manager ACME Issuer for Let's Encrypt. Set to
# false to use externally provided TLS certificates (e.g. ACM/ALB, commercial
# wildcard, internal CA, air-gapped) — the per-app `tls-secret-<name>` Secrets
# must then exist in the namespace.
Comment on lines +47 to +48
enabled: true
# -- Email for letsencrypt.
email: cloudharness@metacell.us
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should not set a default email here, as any CH deployment which does not set this value will default to this email.

# -- Name of the Secret cert-manager uses to store the ACME account private key.
privateKeySecretName: tls-secret-issuer
# -- ACME solvers passed through to the cert-manager Issuer. If empty, defaults to
# an http01 solver using `ingress.ingressClass`. Override with one or more dns01
# solvers to obtain certificates for non-public domains. See `secrets` below to
# declare any credential Secrets referenced by `*SecretRef` here.
# Example (Cloudflare):
# solvers:
# - dns01:
# cloudflare:
# apiTokenSecretRef:
# name: cloudflare-api-token
# key: api-token
solvers: []
# -- Credential Secrets created in the namespace alongside the Issuer. Each entry
# becomes a Kubernetes Secret named after the key, with the inner map rendered as
# `stringData`. Reference these from `solvers` above. Leave empty if you create
# provider credential secrets out-of-band (e.g. with sealed-secrets / external
# secrets / `kubectl create secret`).
# Example:
# secrets:
# cloudflare-api-token:
# api-token: ${CLOUDFLARE_API_TOKEN}
secrets: {}
# -- Default regex segment for routes (used in paths like '/(pattern)').
path: "/"
# -- The pathType for the Ingress path. Default is Prefix. For regex paths, set to ImplementationSpecific
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- [Send errors to Sentry](./sentry.md)
- [Use the events queue to send notifications](./notifications.md)
- [Network policies](./network-policies.md)
- [Ingress, domains, proxies and TLS](./ingress-domains-proxies.md)
- [Writing and running automated tests](./testing.md)
- [Tutorial: Writing a simple webapp with cloud-harness](./tutorials/simple-date-clock-application.adoc)

217 changes: 217 additions & 0 deletions docs/ingress-domains-proxies.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,220 @@ harness:
Customization notes:
- The pattern is inserted into the generated Ingress `path` field. Make sure the regex
is valid for your ingress controller and matches the expected path syntax.

## TLS and Let's Encrypt

TLS is enabled by default for non-local deployments. Cloud Harness provisions a
`cert-manager` ACME `Issuer` named `letsencrypt-<namespace>` and annotates every
generated Ingress (and Gateway, when using the Gateway API) so that certificates
are obtained and renewed automatically.

All configuration lives under `ingress.letsencrypt` in
`deployment-configuration/helm/values.yaml`:

```yaml
ingress:
letsencrypt:
enabled: true # provision the ACME Issuer
email: cloudharness@metacell.us # ACME account email
privateKeySecretName: tls-secret-issuer # ACME account private-key Secret
solvers: [] # solver list; empty = http01 default
secrets: {} # credential Secrets created in-namespace
```

### Default — public domains via http01

For publicly reachable domains, the defaults are sufficient. Set `email` and
leave the rest untouched:

```yaml
ingress:
letsencrypt:
email: ops@example.com
```

This renders a single `http01` solver bound to the configured `ingressClass`.
HTTP01 requires no credentials, so the rest of this section only applies to
DNS01 setups.

### Defining credential secrets

Every DNS01 solver references its provider credentials through a `*SecretRef`
block (the exact field name depends on the provider — `apiTokenSecretRef`,
`tokenSecretRef`, `secretAccessKeySecretRef`, `serviceAccountSecretRef`,
`clientSecretSecretRef`, `tsigSecretSecretRef`, …). All of them have the same
shape:

```yaml
someProviderSecretRef:
name: <kubernetes-secret-name> # name of the Secret in the release namespace
key: <data-key-inside-secret> # which field of the Secret holds the credential
```

You have two options for providing the referenced Secret:

**Option 1 — declare it inline (Cloud Harness creates it):** add an entry under
`ingress.letsencrypt.secrets`. The top-level key becomes the `Secret`'s name;
the nested map becomes its `stringData`. Each inner key is one credential
field, and the `*SecretRef.key` in the solver must match one of those inner
keys exactly.

```yaml
ingress:
letsencrypt:
secrets:
cloudflare-api-token: # → Secret/cloudflare-api-token
api-token: s3cr3t # → stringData.api-token = "s3cr3t"
route53-credentials: # → Secret/route53-credentials
secret-access-key: AKIA... # → stringData.secret-access-key = "AKIA..."
```

A single Secret can hold multiple keys, so you can group related credentials
together (e.g. an `accessKey` and a `secretAccessKey`) and reference each by
its own key. Names you choose for both the Secret and its keys are arbitrary —
the only constraint is that the `*SecretRef.name`/`key` in the solver match.

Because `values.yaml` is committed to source control, prefer injecting real
secrets via environment-variable interpolation at deploy time:

```yaml
ingress:
letsencrypt:
secrets:
cloudflare-api-token:
api-token: ${CLOUDFLARE_API_TOKEN} # resolved by harness-deployment
```

**Option 2 — provision the Secret out-of-band:** create the `Secret` with any
external tool (`kubectl create secret`, sealed-secrets, External Secrets
Operator, a CI/CD pipeline secret, etc.) in the same namespace as the Issuer.
Leave `ingress.letsencrypt.secrets` empty and just reference the existing
Secret from the solver.

```bash
kubectl -n ch create secret generic cloudflare-api-token \
--from-literal=api-token="$CLOUDFLARE_API_TOKEN"
```

```yaml
ingress:
letsencrypt:
# secrets: {} ← intentionally omitted; the Secret already exists
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
```

Both options are interchangeable from cert-manager's perspective — pick the
one that matches how you manage other secrets in the cluster.

### DNS01 — Cloudflare (non-public domains)

DNS01 challenges work for any domain — including domains that aren't reachable
from the internet — as long as cert-manager can update the zone's TXT records.

```yaml
ingress:
letsencrypt:
email: ops@example.com
secrets:
cloudflare-api-token:
api-token: ${CLOUDFLARE_API_TOKEN}
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
```

The `secrets:` map materializes a `Secret` per entry in the release namespace.
Skip it if you provision credentials out-of-band (sealed-secrets, External
Secrets, manual `kubectl create secret`, etc.) — only the `solvers` entry is
required in that case.

### Multiple solvers (mixed http01 + DNS01)

Selectors route hostnames to the matching solver. Default solver applies to
anything that doesn't match a selector:

```yaml
ingress:
letsencrypt:
email: ops@example.com
secrets:
cloudflare-api-token: { api-token: ${CLOUDFLARE_API_TOKEN} }
solvers:
- http01:
ingress:
class: nginx
- dns01:
cloudflare:
apiTokenSecretRef: { name: cloudflare-api-token, key: api-token }
selector:
dnsZones: ["internal.example.com"]
```

### Other DNS providers

Any provider supported by `cert-manager` works — the `solvers[*].dns01` block
is passed through verbatim. Each provider expects credentials via a
`*SecretRef` (see [Defining credential secrets](#defining-credential-secrets)).
Common shapes:

```yaml
# --- Route 53 ---
secrets:
route53-credentials:
secret-access-key: ${AWS_SECRET_ACCESS_KEY}
solvers:
- dns01:
route53:
region: us-east-1
accessKeyID: AKIA...
secretAccessKeySecretRef: { name: route53-credentials, key: secret-access-key }

# --- Google Cloud DNS ---
secrets:
clouddns-sa:
key.json: ${GCP_SERVICE_ACCOUNT_JSON} # entire JSON service-account file
solvers:
- dns01:
cloudDNS:
project: my-gcp-project
serviceAccountSecretRef: { name: clouddns-sa, key: key.json }

# --- DigitalOcean ---
secrets:
digitalocean-dns:
access-token: ${DO_TOKEN}
solvers:
- dns01:
digitalocean:
tokenSecretRef: { name: digitalocean-dns, key: access-token }
```

See the [cert-manager DNS01 reference](https://cert-manager.io/docs/configuration/acme/dns01/)
for the full per-provider schema (Azure DNS, RFC2136, AcmeDNS, webhook, …).

### Bring your own certificates (no ACME)

Set `enabled: false` to skip the ACME Issuer and the `cert-manager.io/issuer`
annotation on every generated Ingress/Gateway. TLS is still wired through —
each app's Ingress references a Secret named `tls-secret-<appName>` which you
must populate yourself (cloud load-balancer integration, sealed-secrets, an
internal CA, ESO, etc.):

```yaml
ingress:
letsencrypt:
enabled: false
```

### Disabling TLS entirely

For local or development deployments, set `tls: false` at the root of the
values file. This is what `harness-deployment ... -dtls -l` does for you.
4 changes: 4 additions & 0 deletions docs/model/GatewayGlobalConfigAllOfLetsencrypt.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**enabled** | **bool** | Whether to provision a cert-manager ACME Issuer for Let&#39;s Encrypt. Set to false to use externally provided TLS Secrets without ACME (e.g. ACM/ALB, commercial wildcards, internal CAs, air-gapped clusters). | [optional]
**email** | **str** | | [optional]
**private_key_secret_name** | **str** | Name of the Secret cert-manager uses to store the ACME account private key. Defaults to &#x60;tls-secret-issuer&#x60;. | [optional]
**solvers** | **List[Dict[str, object]]** | ACME solvers passed through to the cert-manager Issuer. Defaults to an http01 solver using the configured ingressClass. Set to a dns01 solver list to obtain certificates for non-public domains. | [optional]
**secrets** | **Dict[str, Dict[str, str]]** | Credential Secrets created in the namespace alongside the Issuer. Map of &#x60;&lt;secret-name&gt;&#x60; to a &#x60;&lt;key&gt;: &lt;value&gt;&#x60; map rendered as &#x60;stringData&#x60;. Reference these from &#x60;solvers&#x60;. | [optional]

## Example

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def _get_public_key_cache_path():
from django.conf import settings
return os.path.join(settings.PERSISTENT_ROOT, "cloudharness_public_key")
except ImportError:
return "/tmp/cloudharness_public_key"
return "/tmp/cloudharness_public_key"
except Exception:
log.exception("Could not get Django settings, using /tmp for public key cache")
return "/tmp/cloudharness_public_key"
Expand Down
32 changes: 32 additions & 0 deletions libraries/models/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -912,8 +912,40 @@ components:
description: ''
type: object
properties:
enabled:
description: |
Whether to provision a cert-manager ACME Issuer for
Let's Encrypt. Set to false to use externally provided
TLS Secrets without ACME (e.g. ACM/ALB, commercial
wildcards, internal CAs, air-gapped clusters).
type: boolean
email:
type: string
privateKeySecretName:
description: |
Name of the Secret cert-manager uses to store the ACME
account private key. Defaults to `tls-secret-issuer`.
type: string
solvers:
description: |
ACME solvers passed through to the cert-manager Issuer.
Defaults to an http01 solver using the configured
ingressClass. Set to a dns01 solver list to obtain
certificates for non-public domains.
type: array
items:
type: object
additionalProperties: true
secrets:
description: |
Credential Secrets created in the namespace alongside the
Issuer. Map of `<secret-name>` to a `<key>: <value>` map
rendered as `stringData`. Reference these from `solvers`.
type: object
additionalProperties:
type: object
additionalProperties:
type: string
enabled:
description: ''
type: boolean
Expand Down
Loading
Loading