Compare commits

..
1 Commits
Author SHA1 Message Date
claudeandClaude Opus 5.5 f59d2b72fb www: redirect alias hostnames (e.g. apex) to canonical origin
Adds www_redirect_hostnames; renders a www-redirect ingress that 301s
to https://<www_hostname>$request_uri while leaving more specific paths
on the same host (e.g. /.well-known/matrix/*) to their own ingresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 13:04:11 -05:00
5 changed files with 47 additions and 1 deletions
+5 -1
View File
@@ -54,6 +54,7 @@ Defaults deliberately refuse incomplete membership deployments. Configure:
| --- | --- | | --- | --- |
| `www_namespace` | Namespace, default `www` | | `www_namespace` | Namespace, default `www` |
| `www_hostname` | Canonical HTTPS hostname, default **www.eom.dev** | | `www_hostname` | Canonical HTTPS hostname, default **www.eom.dev** |
| `www_redirect_hostnames` | Hostnames 301-redirected to `www_hostname`, default `[]` |
| `www_membership_enabled` | Deploy membership/proxy, default true | | `www_membership_enabled` | Deploy membership/proxy, default true |
| `www_membership_image` | Required versioned application image/digest | | `www_membership_image` | Required versioned application image/digest |
| `www_membership_secret_name` | Existing Secret, default `membership` | | `www_membership_secret_name` | Existing Secret, default `membership` |
@@ -239,7 +240,10 @@ app defaults to eom.dev. This role sets `MEMBERSHIP_PUBLIC_URL` to
To serve the apex instead, set `www_hostname: eom.dev` and align DNS, To serve the apex instead, set `www_hostname: eom.dev` and align DNS,
certificates, callbacks and redirects. Membership requires one canonical HTTPS certificates, callbacks and redirects. Membership requires one canonical HTTPS
origin for sessions, CSRF, email verification and OIDC. origin for sessions, CSRF, email verification and OIDC. To send apex visitors
to the canonical origin without serving it there, set
`www_redirect_hostnames: [eom.dev]`; other ingresses' more specific apex paths
(such as `/.well-known/matrix/*`) keep working.
## Upgrades and operations ## Upgrades and operations
+2
View File
@@ -15,6 +15,8 @@ www_repo_url: https://git.example.com/organization/www.git
www_namespace: www www_namespace: www
www_hostname: www.example.com www_hostname: www.example.com
www_ingress_class: nginx www_ingress_class: nginx
# Extra hostnames 301-redirected to https://{{ www_hostname }} (e.g. [eom.dev]).
www_redirect_hostnames: []
www_cluster_issuer: letsencrypt-prod www_cluster_issuer: letsencrypt-prod
# Set to your published website image, including any required metrics exporter. # Set to your published website image, including any required metrics exporter.
www_httpd_image: registry.example.com/organization/www:release www_httpd_image: registry.example.com/organization/www:release
+36
View File
@@ -171,3 +171,39 @@ spec:
tls: tls:
- hosts: [{{ www_hostname }}] - hosts: [{{ www_hostname }}]
secretName: www secretName: www
{% if www_redirect_hostnames | length > 0 %}
---
# Send browsers on alias hostnames (e.g. the apex, whose /.well-known/matrix/*
# paths are owned by another ingress) to the canonical origin. Exact/longer
# paths on the same host in other ingresses still take precedence over "/".
# A snippet is used because the controller's annotation validation rejects
# "$request_uri" in permanent-redirect.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: www-redirect
namespace: {{ www_namespace }}
annotations:
cert-manager.io/cluster-issuer: {{ www_cluster_issuer | to_json }}
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/enable-access-log: "false"
nginx.ingress.kubernetes.io/configuration-snippet: {{ ("return 301 https://" ~ www_hostname ~ "$request_uri;") | to_json }}
spec:
ingressClassName: {{ www_ingress_class }}
rules:
{% for host in www_redirect_hostnames %}
- host: {{ host }}
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: httpd
port:
number: 80
{% endfor %}
tls:
- hosts: {{ www_redirect_hostnames | to_json }}
secretName: www-redirect
{% endif %}
+1
View File
@@ -11,6 +11,7 @@
- name: Supply nonsecret fixture settings - name: Supply nonsecret fixture settings
ansible.builtin.set_fact: ansible.builtin.set_fact:
www_membership_image: registry.example.org/www-membership:release-test www_membership_image: registry.example.org/www-membership:release-test
www_redirect_hostnames: [apex.example.com]
www_membership_service_groups: www_membership_service_groups:
"0": ["cn=free-service,ou=groups,dc=example,dc=org"] "0": ["cn=free-service,ou=groups,dc=example,dc=org"]
"2": ["cn=paid-service,ou=groups,dc=example,dc=org", "cn=second-service,ou=groups,dc=example,dc=org"] "2": ["cn=paid-service,ou=groups,dc=example,dc=org", "cn=second-service,ou=groups,dc=example,dc=org"]
+3
View File
@@ -92,6 +92,9 @@ ingress = find(all_objects, "Ingress", "www")
assert ingress["metadata"]["annotations"]["nginx.ingress.kubernetes.io/enable-access-log"] == "false" assert ingress["metadata"]["annotations"]["nginx.ingress.kubernetes.io/enable-access-log"] == "false"
assert ingress["metadata"]["annotations"]["nginx.ingress.kubernetes.io/proxy-body-size"] == "2m" assert ingress["metadata"]["annotations"]["nginx.ingress.kubernetes.io/proxy-body-size"] == "2m"
assert ingress["spec"]["rules"][0]["http"]["paths"][0]["backend"]["service"]["name"] == "httpd" assert ingress["spec"]["rules"][0]["http"]["paths"][0]["backend"]["service"]["name"] == "httpd"
redirect = find(all_objects, "Ingress", "www-redirect")
assert redirect["spec"]["rules"][0]["host"] == "apex.example.com"
assert redirect["metadata"]["annotations"]["nginx.ingress.kubernetes.io/configuration-snippet"] == "return 301 " + config["MEMBERSHIP_PUBLIC_URL"] + "$request_uri;"
for obj in docs("external-membership-workloads"): for obj in docs("external-membership-workloads"):
if obj["kind"] == "Deployment": if obj["kind"] == "Deployment":
pod_checks(obj["spec"]["template"]["spec"], external=True) pod_checks(obj["spec"]["template"]["spec"], external=True)