diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/Chart.yaml b/helm/argo-stack/overlays/ingress-authz-overlay/Chart.yaml
new file mode 100644
index 00000000..75de8fe6
--- /dev/null
+++ b/helm/argo-stack/overlays/ingress-authz-overlay/Chart.yaml
@@ -0,0 +1,16 @@
+apiVersion: v2
+name: ingress-authz-overlay
+description: Authz-aware ingress overlay providing unified path-based routing with centralized authorization for multi-tenant UIs and APIs
+type: application
+version: 0.1.0
+appVersion: "1.0.0"
+keywords:
+ - ingress
+ - authorization
+ - multi-tenant
+ - nginx
+ - argo
+home: https://github.com/calypr/argo-helm
+maintainers:
+ - name: calypr
+ url: https://github.com/calypr
diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/README.md b/helm/argo-stack/overlays/ingress-authz-overlay/README.md
new file mode 100644
index 00000000..02a89464
--- /dev/null
+++ b/helm/argo-stack/overlays/ingress-authz-overlay/README.md
@@ -0,0 +1,65 @@
+# Ingress AuthZ Overlay
+
+A Helm overlay chart providing unified, path-based ingress with centralized authorization for multi-tenant Argo Stack deployments.
+
+## Overview
+
+This overlay provides a **single host, path-based ingress** for all major UIs and APIs:
+
+| Path | Service | Description |
+|------|---------|-------------|
+| `/workflows` | Argo Workflows Server | Workflow UI (port 2746) |
+| `/applications` | Argo CD Server | GitOps applications UI (port 8080) |
+| `/registrations` | GitHub EventSource | Repository registration events (port 12000) |
+| `/api` | Calypr API | Platform API service (port 3000) |
+| `/tenants` | Calypr Tenants | Tenant portal (port 3001) |
+
+All endpoints are protected by the `authz-adapter` via NGINX external authentication.
+
+## Quick Start
+
+```bash
+# Install the overlay
+helm upgrade --install ingress-authz-overlay \
+ helm/argo-stack/overlays/ingress-authz-overlay \
+ --namespace argo-stack \
+ --create-namespace
+
+# With custom host
+helm upgrade --install ingress-authz-overlay \
+ helm/argo-stack/overlays/ingress-authz-overlay \
+ --namespace argo-stack \
+ --set ingressAuthzOverlay.host=my-domain.example.com
+```
+
+## Configuration
+
+See [`values.yaml`](values.yaml) for all configurable options.
+
+Key settings:
+
+```yaml
+ingressAuthzOverlay:
+ enabled: true
+ host: calypr-demo.ddns.net
+ tls:
+ enabled: true
+ secretName: calypr-demo-tls
+ clusterIssuer: letsencrypt-prod
+```
+
+## Documentation
+
+- [User Guide](docs/authz-ingress-user-guide.md) - Complete installation and configuration guide
+- [Acceptance Tests](tests/authz-ingress.feature) - Gherkin-style test scenarios
+
+## Architecture
+
+See the [User Guide](docs/authz-ingress-user-guide.md) for architecture diagrams and detailed flow descriptions.
+
+## Requirements
+
+- Kubernetes 1.19+
+- Helm 3.x
+- NGINX Ingress Controller
+- cert-manager (for TLS)
diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/docs/authz-ingress-user-guide.md b/helm/argo-stack/overlays/ingress-authz-overlay/docs/authz-ingress-user-guide.md
new file mode 100644
index 00000000..a54d5aed
--- /dev/null
+++ b/helm/argo-stack/overlays/ingress-authz-overlay/docs/authz-ingress-user-guide.md
@@ -0,0 +1,367 @@
+# Authz-Aware Ingress Overlay User Guide
+
+## Overview
+
+The `ingress-authz-overlay` is a Helm overlay chart that provides a unified, path-based ingress layer for all major UIs and APIs in the Argo Stack. It centralizes authorization through the `authz-adapter` service, ensuring consistent access control across all endpoints.
+
+## Features
+
+- **Single Host**: All services exposed on one HTTPS hostname
+- **Path-Based Routing**: Clean URL structure (`/workflows`, `/applications`, `/api`, etc.)
+- **Centralized Authorization**: All routes protected by `authz-adapter` via NGINX external auth
+- **TLS via cert-manager**: Automatic Let's Encrypt certificate management
+- **Multi-Tenant Support**: User, email, and group headers passed to backend services
+- **Drop-In Deployment**: Simple Helm overlay that can be enabled or disabled per environment
+
+## Architecture
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Ingress as NGINX Ingress
+ participant AuthzAdapter as authz-adapter
+ participant Workflows as Argo Workflows
+ participant Applications as Argo CD
+ participant Registrations as Event Source
+ participant Api as Calypr API
+ participant Tenants as Calypr Tenants
+
+ User->>Ingress: HTTPS GET /path
+ Ingress->>AuthzAdapter: auth-url check
+ AuthzAdapter-->>Ingress: Allow or Deny
+ alt Allowed
+ Note over Ingress: Route based on path
+ Ingress->>Workflows: /workflows...
+ Ingress->>Applications: /applications...
+ Ingress->>Registrations: /registrations...
+ Ingress->>Api: /api...
+ Ingress->>Tenants: /tenants...
+ else Denied
+ Ingress-->>User: Redirect to /tenants/login
+ end
+```
+
+## Routes
+
+| Path | Service | Port | Namespace | Description |
+|------|---------|------|-----------|-------------|
+| `/workflows` | `argo-stack-argo-workflows-server` | 2746 | `argo-stack` | Argo Workflows UI |
+| `/applications` | `argo-stack-argocd-server` | 8080 | `argo-stack` | Argo CD Applications UI |
+| `/registrations` | `github-repo-registrations-eventsource-svc` | 12000 | `argo-stack` | GitHub Repo Registration Events |
+| `/api` | `calypr-api` | 3000 | `calypr-api` | Calypr API Service |
+| `/tenants` | `calypr-tenants` | 3001 | `calypr-tenants` | Calypr Tenant Portal |
+
+## TLS with Let's Encrypt and cert-manager
+
+This overlay uses [cert-manager](https://cert-manager.io/) to automatically provision and renew TLS certificates from [Let's Encrypt](https://letsencrypt.org/).
+
+### How It Works
+
+```mermaid
+sequenceDiagram
+ participant Ingress as Ingress Resource
+ participant CM as cert-manager
+ participant LE as Let's Encrypt
+ participant DNS as DNS Provider
+
+ Note over Ingress: Created with annotation:
cert-manager.io/cluster-issuer: letsencrypt-prod
+ Ingress->>CM: Ingress triggers Certificate request
+ CM->>LE: Request certificate for domain
+ LE->>CM: ACME challenge (HTTP-01 or DNS-01)
+ CM->>DNS: Prove domain ownership
+ DNS-->>LE: Challenge verified
+ LE-->>CM: Issue certificate
+ CM->>Ingress: Store cert in TLS Secret
+ Note over Ingress: HTTPS now available
+```
+
+### ClusterIssuer: letsencrypt-prod
+
+The `letsencrypt-prod` ClusterIssuer is a cluster-wide cert-manager resource that defines how to obtain certificates from Let's Encrypt's production API.
+
+**Prerequisites**: You must create the ClusterIssuer before deploying this overlay:
+
+```yaml
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+ name: letsencrypt-prod
+spec:
+ acme:
+ # Let's Encrypt production API endpoint
+ server: https://acme-v02.api.letsencrypt.org/directory
+
+ # Email for certificate expiration notifications
+ email: your-email@example.com
+
+ # Secret to store the ACME account private key
+ privateKeySecretRef:
+ name: letsencrypt-prod-account-key
+
+ # HTTP-01 challenge solver using ingress
+ solvers:
+ - http01:
+ ingress:
+ class: nginx
+```
+
+**Apply the ClusterIssuer**:
+
+```bash
+kubectl apply -f cluster-issuer.yaml
+```
+
+### Configuration Options
+
+| Setting | Description | Default |
+|---------|-------------|---------|
+| `tls.enabled` | Enable TLS for ingress | `true` |
+| `tls.secretName` | Name of the TLS Secret (auto-created by cert-manager) | `calypr-demo-tls` |
+| `tls.clusterIssuer` | Name of the ClusterIssuer to use | `letsencrypt-prod` |
+
+### Using letsencrypt-staging (for Testing)
+
+For testing, use the staging issuer to avoid Let's Encrypt rate limits:
+
+```yaml
+apiVersion: cert-manager.io/v1
+kind: ClusterIssuer
+metadata:
+ name: letsencrypt-staging
+spec:
+ acme:
+ server: https://acme-staging-v02.api.letsencrypt.org/directory
+ email: your-email@example.com
+ privateKeySecretRef:
+ name: letsencrypt-staging-account-key
+ solvers:
+ - http01:
+ ingress:
+ class: nginx
+```
+
+Then configure the overlay to use it:
+
+```yaml
+ingressAuthzOverlay:
+ tls:
+ clusterIssuer: letsencrypt-staging
+```
+
+### Verifying Certificate Status
+
+Check if the certificate was issued successfully:
+
+```bash
+# Check Certificate resource
+kubectl get certificate -n argo-stack
+
+# Check certificate details
+kubectl describe certificate -n argo-stack
+
+# Check the TLS secret
+kubectl get secret calypr-demo-tls -n argo-stack
+```
+
+### Troubleshooting Certificates
+
+If the certificate is not being issued:
+
+```bash
+# Check cert-manager logs
+kubectl logs -n cert-manager -l app=cert-manager
+
+# Check Certificate status
+kubectl describe certificate -n argo-stack
+
+# Check CertificateRequest
+kubectl get certificaterequest -n argo-stack
+
+# Check ACME challenges
+kubectl get challenges -A
+```
+
+Common issues:
+- **Domain not reachable**: Ensure your domain's DNS points to the ingress controller's external IP
+- **Rate limited**: Use `letsencrypt-staging` for testing to avoid production rate limits
+- **Challenge failed**: Check that port 80 is accessible for HTTP-01 challenges
+
+## Installation
+
+### Prerequisites
+
+- Kubernetes cluster with NGINX Ingress Controller
+- cert-manager installed and configured with a ClusterIssuer (e.g., `letsencrypt-prod`)
+- Helm 3.x
+
+### Install the Overlay
+
+```bash
+# Install with default values
+helm upgrade --install ingress-authz-overlay \
+ helm/argo-stack/overlays/ingress-authz-overlay \
+ --namespace argo-stack \
+ --create-namespace
+
+# Install with custom host
+helm upgrade --install ingress-authz-overlay \
+ helm/argo-stack/overlays/ingress-authz-overlay \
+ --namespace argo-stack \
+ --set ingressAuthzOverlay.host=my-domain.example.com \
+ --set ingressAuthzOverlay.tls.secretName=my-domain-tls
+```
+
+### Integrate with Parent Chart
+
+Alternatively, add the values to your main `argo-stack` deployment:
+
+```bash
+helm upgrade --install argo-stack \
+ helm/argo-stack \
+ --values helm/argo-stack/values.yaml \
+ --set ingressAuthzOverlay.enabled=true
+```
+
+## Configuration
+
+### Basic Configuration
+
+```yaml
+ingressAuthzOverlay:
+ enabled: true
+ host: calypr-demo.ddns.net
+ tls:
+ enabled: true
+ secretName: calypr-demo-tls
+ clusterIssuer: letsencrypt-prod
+```
+
+### AuthZ Adapter Configuration
+
+```yaml
+ingressAuthzOverlay:
+ authzAdapter:
+ # Disable if authz-adapter is deployed separately
+ deploy: true
+
+ # Service location
+ serviceName: authz-adapter
+ namespace: argo-stack
+ port: 8080
+ path: /check
+
+ # Sign-in redirect URL
+ signinUrl: https://calypr-demo.ddns.net/tenants/login
+
+ # Headers passed from auth response to backends
+ responseHeaders: "X-User,X-Email,X-Groups"
+
+ # Environment configuration
+ env:
+ fenceBase: "https://calypr-dev.ohsu.edu/user"
+```
+
+### Custom Routes
+
+Add or modify routes as needed:
+
+```yaml
+ingressAuthzOverlay:
+ routes:
+ # Custom route example
+ myservice:
+ enabled: true
+ namespace: my-namespace
+ service: my-service
+ port: 8000
+ pathPrefix: /myservice
+ useRegex: true
+ rewriteTarget: /$2
+```
+
+### Disabling a Route
+
+```yaml
+ingressAuthzOverlay:
+ routes:
+ registrations:
+ enabled: false
+```
+
+## Authorization Flow
+
+1. **User Request**: Client sends HTTPS request to the ingress host
+2. **External Auth**: NGINX Ingress calls the `authz-adapter` `/check` endpoint
+3. **Token Validation**: `authz-adapter` validates the Authorization header against Fence/OIDC
+4. **Group Assignment**: User is assigned groups based on their permissions (e.g., `argo-runner`, `argo-viewer`)
+5. **Response Headers**: On success, user info headers are added to the request
+6. **Routing**: Request is forwarded to the appropriate backend service
+7. **Denial**: On failure, user is redirected to the sign-in URL
+
+### Auth Response Headers
+
+The following headers are passed to backend services on successful authentication:
+
+| Header | Description |
+|--------|-------------|
+| `X-Auth-Request-User` | Username or email of the authenticated user |
+| `X-Auth-Request-Email` | Email address of the user |
+| `X-Auth-Request-Groups` | Comma-separated list of groups |
+| `X-User` | Alias for X-Auth-Request-User |
+| `X-Email` | Alias for X-Auth-Request-Email |
+| `X-Groups` | Alias for X-Auth-Request-Groups |
+
+## Troubleshooting
+
+### Check Ingress Status
+
+```bash
+kubectl get ingress -A -l app.kubernetes.io/name=ingress-authz-overlay
+```
+
+### Check AuthZ Adapter
+
+```bash
+# Logs
+kubectl logs -n argo-stack -l app=authz-adapter
+
+# Test health endpoint
+kubectl port-forward -n argo-stack svc/authz-adapter 8080:8080 &
+curl http://localhost:8080/healthz
+```
+
+### Test Authentication
+
+```bash
+# Should redirect to login
+curl -I https://calypr-demo.ddns.net/workflows
+
+# With valid token (should return 200)
+curl -I -H "Authorization: Bearer $TOKEN" https://calypr-demo.ddns.net/workflows
+```
+
+### Common Issues
+
+1. **502 Bad Gateway**: AuthZ adapter not reachable
+ - Check authz-adapter deployment is running
+ - Verify service selector matches pod labels
+
+2. **503 Service Unavailable**: Backend service not available
+ - Check target service exists in the specified namespace
+ - Verify service port matches configuration
+
+3. **Redirect Loop**: Auth signin URL misconfigured
+ - Ensure `/tenants/login` path is accessible
+ - Check signinUrl matches actual login endpoint
+
+## Uninstall
+
+```bash
+helm uninstall ingress-authz-overlay -n argo-stack
+```
+
+## Related Documentation
+
+- [Argo Stack User Guide](../../docs/user-guide.md)
+- [Tenant Onboarding Guide](../../docs/tenant-onboarding.md)
+- [Repo Registration Guide](../../docs/repo-registration-guide.md)
diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/templates/_helpers.tpl b/helm/argo-stack/overlays/ingress-authz-overlay/templates/_helpers.tpl
new file mode 100644
index 00000000..e8f2468d
--- /dev/null
+++ b/helm/argo-stack/overlays/ingress-authz-overlay/templates/_helpers.tpl
@@ -0,0 +1,72 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "ingress-authz-overlay.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+*/}}
+{{- define "ingress-authz-overlay.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "ingress-authz-overlay.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "ingress-authz-overlay.labels" -}}
+helm.sh/chart: {{ include "ingress-authz-overlay.chart" . }}
+{{ include "ingress-authz-overlay.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "ingress-authz-overlay.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "ingress-authz-overlay.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the auth-url for NGINX ingress external auth.
+*/}}
+{{- define "ingress-authz-overlay.authUrl" -}}
+{{- $adapter := .Values.ingressAuthzOverlay.authzAdapter -}}
+http://{{ $adapter.serviceName }}.{{ $adapter.namespace }}.svc.cluster.local:{{ $adapter.port }}{{ $adapter.path }}
+{{- end }}
+
+{{/*
+Create common ingress annotations for NGINX external auth.
+*/}}
+{{- define "ingress-authz-overlay.authAnnotations" -}}
+nginx.ingress.kubernetes.io/auth-url: {{ include "ingress-authz-overlay.authUrl" . | quote }}
+nginx.ingress.kubernetes.io/auth-method: "GET"
+nginx.ingress.kubernetes.io/auth-signin: {{ .Values.ingressAuthzOverlay.authzAdapter.signinUrl | quote }}
+nginx.ingress.kubernetes.io/auth-response-headers: {{ .Values.ingressAuthzOverlay.authzAdapter.responseHeaders | quote }}
+nginx.ingress.kubernetes.io/auth-snippet: |
+ proxy_set_header Authorization $http_authorization;
+ proxy_set_header X-Original-URI $request_uri;
+ proxy_set_header X-Original-Method $request_method;
+ proxy_set_header X-Forwarded-Host $host;
+{{- end }}
diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/templates/authz-adapter.yaml b/helm/argo-stack/overlays/ingress-authz-overlay/templates/authz-adapter.yaml
new file mode 100644
index 00000000..194c1ea8
--- /dev/null
+++ b/helm/argo-stack/overlays/ingress-authz-overlay/templates/authz-adapter.yaml
@@ -0,0 +1,107 @@
+{{/*
+AuthZ Adapter Deployment and Service for the ingress-authz-overlay.
+The authz-adapter provides external authentication for NGINX Ingress,
+validating tokens and returning user/group information.
+*/}}
+{{- if and .Values.ingressAuthzOverlay.enabled .Values.ingressAuthzOverlay.authzAdapter.deploy }}
+{{- $config := .Values.ingressAuthzOverlay }}
+{{- $adapter := $config.authzAdapter }}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ $adapter.serviceName }}
+ namespace: {{ $adapter.namespace }}
+ labels:
+ {{- include "ingress-authz-overlay.labels" . | nindent 4 }}
+ app.kubernetes.io/component: authz-adapter
+ app: {{ $adapter.serviceName }}
+ annotations:
+ meta.helm.sh/release-name: {{ .Release.Name }}
+ meta.helm.sh/release-namespace: {{ .Release.Namespace }}
+spec:
+ replicas: {{ $adapter.replicas | default 2 }}
+ selector:
+ matchLabels:
+ app: {{ $adapter.serviceName }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ template:
+ metadata:
+ labels:
+ app: {{ $adapter.serviceName }}
+ {{- include "ingress-authz-overlay.selectorLabels" . | nindent 8 }}
+ app.kubernetes.io/component: authz-adapter
+ spec:
+ {{- with $adapter.securityContext }}
+ securityContext:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ containers:
+ - name: authz-adapter
+ image: {{ $adapter.image }}
+ imagePullPolicy: IfNotPresent
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
+ capabilities:
+ drop:
+ - ALL
+ ports:
+ - name: http
+ containerPort: {{ $adapter.port }}
+ protocol: TCP
+ env:
+ - name: FENCE_BASE
+ value: {{ $adapter.env.fenceBase | quote }}
+ - name: TENANT_LOGIN_PATH
+ value: {{ $adapter.env.tenantLoginPath | default "/tenants/login" | quote }}
+ - name: HTTP_TIMEOUT
+ value: {{ $adapter.env.httpTimeout | default "3.0" | quote }}
+ {{- if $adapter.env.gitappBaseUrl }}
+ - name: GITAPP_BASE_URL
+ value: {{ $adapter.env.gitappBaseUrl | quote }}
+ {{- end }}
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ timeoutSeconds: 3
+ failureThreshold: 3
+ readinessProbe:
+ httpGet:
+ path: /healthz
+ port: http
+ initialDelaySeconds: 3
+ periodSeconds: 5
+ timeoutSeconds: 2
+ failureThreshold: 2
+ {{- with $adapter.resources }}
+ resources:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ $adapter.serviceName }}
+ namespace: {{ $adapter.namespace }}
+ labels:
+ {{- include "ingress-authz-overlay.labels" . | nindent 4 }}
+ app.kubernetes.io/component: authz-adapter
+ app: {{ $adapter.serviceName }}
+ annotations:
+ meta.helm.sh/release-name: {{ .Release.Name }}
+ meta.helm.sh/release-namespace: {{ .Release.Namespace }}
+spec:
+ type: ClusterIP
+ selector:
+ app: {{ $adapter.serviceName }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ ports:
+ - name: http
+ port: {{ $adapter.port }}
+ targetPort: http
+ protocol: TCP
+{{- end }}
diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/templates/ingress-authz.yaml b/helm/argo-stack/overlays/ingress-authz-overlay/templates/ingress-authz.yaml
new file mode 100644
index 00000000..6f81fae3
--- /dev/null
+++ b/helm/argo-stack/overlays/ingress-authz-overlay/templates/ingress-authz.yaml
@@ -0,0 +1,63 @@
+{{/*
+Ingress resources for each route in the ingress-authz-overlay.
+Each route creates a separate Ingress resource in its respective namespace,
+all sharing the same host and TLS configuration.
+All routes are protected by the authz-adapter via NGINX external auth.
+*/}}
+{{- if .Values.ingressAuthzOverlay.enabled }}
+{{- $root := . }}
+{{- $config := .Values.ingressAuthzOverlay }}
+{{- range $routeName, $route := $config.routes }}
+{{- if $route.enabled }}
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-authz-{{ $routeName }}
+ namespace: {{ $route.namespace }}
+ labels:
+ {{- include "ingress-authz-overlay.labels" $root | nindent 4 }}
+ app.kubernetes.io/component: ingress
+ ingress-authz-overlay.calypr.io/route: {{ $routeName | quote }}
+ annotations:
+ # Helm release tracking
+ meta.helm.sh/release-name: {{ $root.Release.Name }}
+ meta.helm.sh/release-namespace: {{ $root.Release.Namespace }}
+ # NGINX external auth annotations
+ {{- include "ingress-authz-overlay.authAnnotations" $root | nindent 4 }}
+ {{- if $config.tls.enabled }}
+ # Let's Encrypt / cert-manager integration
+ cert-manager.io/cluster-issuer: {{ $config.tls.clusterIssuer | quote }}
+ {{- end }}
+ {{- if $route.useRegex }}
+ # Path rewriting for subpath support
+ nginx.ingress.kubernetes.io/use-regex: "true"
+ nginx.ingress.kubernetes.io/rewrite-target: {{ $route.rewriteTarget | default "/$2" }}
+ {{- end }}
+spec:
+ ingressClassName: {{ $config.ingressClassName | default "nginx" }}
+ {{- if $config.tls.enabled }}
+ tls:
+ - hosts:
+ - {{ $config.host | quote }}
+ secretName: {{ $config.tls.secretName | quote }}
+ {{- end }}
+ rules:
+ - host: {{ $config.host | quote }}
+ http:
+ paths:
+ {{- if $route.useRegex }}
+ - path: {{ $route.pathPrefix }}(/|$)(.*)
+ pathType: ImplementationSpecific
+ {{- else }}
+ - path: {{ $route.pathPrefix }}
+ pathType: Prefix
+ {{- end }}
+ backend:
+ service:
+ name: {{ $route.service }}
+ port:
+ number: {{ $route.port }}
+{{- end }}
+{{- end }}
+{{- end }}
diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/tests/authz-ingress.feature b/helm/argo-stack/overlays/ingress-authz-overlay/tests/authz-ingress.feature
new file mode 100644
index 00000000..e84c7a8c
--- /dev/null
+++ b/helm/argo-stack/overlays/ingress-authz-overlay/tests/authz-ingress.feature
@@ -0,0 +1,72 @@
+Feature: Authz ingress overlay
+
+ Background:
+ Given the ingress-authz-overlay is installed
+ And the hostname "calypr-demo.ddns.net" resolves to the ingress endpoint
+
+ Scenario: Unauthenticated user is redirected to login
+ When I send a GET request to "https://calypr-demo.ddns.net/workflows"
+ Then the response status should be 302 or 303
+ And the "Location" header should contain "/tenants/login"
+
+ Scenario: Authenticated user can access workflows
+ Given I have a valid session recognized by authz-adapter
+ When I send a GET request to "https://calypr-demo.ddns.net/workflows"
+ Then the response status should be 200
+
+ Scenario: All paths are protected by authz-adapter
+ When I send a GET request to "https://calypr-demo.ddns.net/applications" without credentials
+ Then I should be redirected to "/tenants/login"
+
+ When I send a GET request to "https://calypr-demo.ddns.net/registrations" without credentials
+ Then I should be redirected to "/tenants/login"
+
+ When I send a GET request to "https://calypr-demo.ddns.net/api" without credentials
+ Then I should be redirected to "/tenants/login"
+
+ When I send a GET request to "https://calypr-demo.ddns.net/tenants" without credentials
+ Then I should be redirected to "/tenants/login" or served only public content as configured
+
+ Scenario: TLS certificate is valid
+ When I connect to "https://calypr-demo.ddns.net"
+ Then the TLS certificate should be issued by "Let's Encrypt"
+ And the certificate subject alt name should include "calypr-demo.ddns.net"
+
+ Scenario: Routing sends requests to the correct services
+ Given I am authenticated
+ When I send a GET request to "https://calypr-demo.ddns.net/workflows"
+ Then the response should contain an HTML title for the workflows UI
+
+ When I send a GET request to "https://calypr-demo.ddns.net/applications"
+ Then the response should contain an HTML title for the applications UI
+
+ When I send a GET request to "https://calypr-demo.ddns.net/api/health"
+ Then I should receive a 200 response with a JSON health object from the API
+
+ When I send a GET request to "https://calypr-demo.ddns.net/tenants"
+ Then I should see the tenant portal landing page or login as configured
+
+ Scenario: Auth response headers are passed to backend
+ Given I am authenticated with user "test@example.com" in groups "argo-runner,argo-viewer"
+ When I send a GET request to "https://calypr-demo.ddns.net/api/whoami"
+ Then the backend should receive header "X-Auth-Request-User" with value "test@example.com"
+ And the backend should receive header "X-Auth-Request-Groups" with value "argo-runner,argo-viewer"
+
+ Scenario: Path rewriting works correctly
+ Given I am authenticated
+ When I send a GET request to "https://calypr-demo.ddns.net/workflows/workflow-details/my-workflow"
+ Then the Argo Workflows server should receive path "/workflow-details/my-workflow"
+
+ When I send a GET request to "https://calypr-demo.ddns.net/api/v1/users"
+ Then the Calypr API should receive path "/v1/users"
+
+ Scenario: Health check endpoint is accessible
+ When I send a GET request to "http://authz-adapter.argo-stack.svc.cluster.local:8080/healthz"
+ Then the response status should be 200
+ And the response body should be "ok"
+
+ Scenario: Multiple simultaneous requests are handled
+ Given I am authenticated
+ When I send 10 concurrent GET requests to "https://calypr-demo.ddns.net/workflows"
+ Then all responses should have status 200
+ And the average response time should be less than 500ms
diff --git a/helm/argo-stack/overlays/ingress-authz-overlay/values.yaml b/helm/argo-stack/overlays/ingress-authz-overlay/values.yaml
new file mode 100644
index 00000000..ed3b71a4
--- /dev/null
+++ b/helm/argo-stack/overlays/ingress-authz-overlay/values.yaml
@@ -0,0 +1,147 @@
+# ============================================================================
+# Ingress AuthZ Overlay Configuration
+# ============================================================================
+# This overlay provides a single host, path-based ingress layer for all
+# major UIs and APIs, protected by a centralized authz-adapter.
+#
+# Usage:
+# helm upgrade --install ingress-authz-overlay \
+# helm/argo-stack/overlays/ingress-authz-overlay \
+# --set ingressAuthzOverlay.enabled=true
+# ============================================================================
+
+ingressAuthzOverlay:
+ # Enable or disable the overlay
+ enabled: true
+
+ # ============================================================================
+ # Host and TLS Configuration
+ # ============================================================================
+ # Single host for all path-based routes
+ host: calypr-demo.ddns.net
+
+ # TLS configuration using cert-manager
+ tls:
+ enabled: true
+ secretName: calypr-demo-tls
+ clusterIssuer: letsencrypt-prod
+
+ # ============================================================================
+ # Ingress Controller Configuration
+ # ============================================================================
+ ingressClassName: nginx
+
+ # ============================================================================
+ # AuthZ Adapter Configuration
+ # ============================================================================
+ authzAdapter:
+ # Enable deployment of authz-adapter (set to false if deployed separately)
+ deploy: true
+
+ # Service discovery settings
+ serviceName: authz-adapter
+ namespace: argo-stack
+ port: 8080
+
+ # Auth endpoint path
+ path: /check
+
+ # Sign-in URL for unauthenticated requests
+ signinUrl: https://calypr-demo.ddns.net/tenants/login
+
+ # Headers to pass back from auth response
+ responseHeaders: "X-User,X-Email,X-Groups,X-Auth-Request-User,X-Auth-Request-Email,X-Auth-Request-Groups"
+
+ # Container image for authz-adapter
+ image: ghcr.io/calypr/argo-helm:latest
+
+ # Number of replicas
+ replicas: 2
+
+ # Environment configuration for the adapter
+ env:
+ # GitApp/Fence base URL for user info
+ fenceBase: "https://calypr-dev.ohsu.edu/user"
+ # Tenant login path
+ tenantLoginPath: "/tenants/login"
+ # HTTP timeout for auth calls
+ httpTimeout: "3.0"
+
+ # Resource limits and requests
+ resources:
+ requests:
+ cpu: 50m
+ memory: 64Mi
+ limits:
+ cpu: 200m
+ memory: 128Mi
+
+ # Pod security context
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 1000
+
+ # ============================================================================
+ # Route Definitions
+ # ============================================================================
+ # Each route creates a separate Ingress resource in the specified namespace.
+ # All routes share the same host and TLS configuration.
+ # All routes are protected by the authz-adapter via NGINX external auth.
+ routes:
+ # Argo Workflows UI
+ workflows:
+ enabled: true
+ namespace: argo-stack
+ service: argo-stack-argo-workflows-server
+ port: 2746
+ pathPrefix: /workflows
+ # Use regex path matching for subpaths
+ useRegex: true
+ # Rewrite path to remove prefix
+ rewriteTarget: /$2
+
+ # Argo CD Applications UI
+ applications:
+ enabled: true
+ namespace: argo-stack
+ service: argo-stack-argocd-server
+ port: 8080
+ pathPrefix: /applications
+ useRegex: true
+ rewriteTarget: /$2
+
+ # GitHub Repository Registrations EventSource
+ registrations:
+ enabled: true
+ namespace: argo-stack
+ service: github-repo-registrations-eventsource-svc
+ port: 12000
+ pathPrefix: /registrations
+ useRegex: true
+ rewriteTarget: /$2
+
+ # Calypr API Service
+ api:
+ enabled: true
+ namespace: calypr-api
+ service: calypr-api
+ port: 3000
+ pathPrefix: /api
+ useRegex: true
+ rewriteTarget: /$2
+
+ # Calypr Tenants Service
+ tenants:
+ enabled: true
+ namespace: calypr-tenants
+ service: calypr-tenants
+ port: 3001
+ pathPrefix: /tenants
+ useRegex: true
+ rewriteTarget: /$2
+ # Optional: Allow public access to login endpoint
+ # Set to true to skip auth for /tenants/login
+ publicPaths:
+ - /tenants/login
+ - /tenants/logout
+ - /tenants/callback
diff --git a/helm/argo-stack/values.yaml b/helm/argo-stack/values.yaml
index 4215afd3..76275e6b 100644
--- a/helm/argo-stack/values.yaml
+++ b/helm/argo-stack/values.yaml
@@ -215,6 +215,59 @@ ingressAuth:
authURL: "http://authz-adapter.security.svc.cluster.local:8080/check"
passAuthorization: true
+# ============================================================================
+# Ingress AuthZ Overlay - Unified Path-Based Routing with Centralized Auth
+# ============================================================================
+# Enable this overlay to provide a single host, path-based ingress for all
+# major UIs and APIs, protected by the authz-adapter.
+# See: helm/argo-stack/overlays/ingress-authz-overlay/docs/authz-ingress-user-guide.md
+#
+# To use the overlay, install it separately:
+# helm upgrade --install ingress-authz-overlay \
+# helm/argo-stack/overlays/ingress-authz-overlay \
+# --values helm/argo-stack/values.yaml \
+# --set ingressAuthzOverlay.enabled=true
+
+ingressAuthzOverlay:
+ enabled: false
+ host: calypr-demo.ddns.net
+ tls:
+ secretName: calypr-demo-tls
+ clusterIssuer: letsencrypt-prod
+ authzAdapter:
+ serviceName: authz-adapter
+ namespace: argo-stack
+ port: 8080
+ path: /check
+ signinUrl: https://calypr-demo.ddns.net/tenants/login
+ responseHeaders: X-User, X-Email, X-Groups
+ routes:
+ workflows:
+ namespace: argo-stack
+ service: argo-stack-argo-workflows-server
+ port: 2746
+ pathPrefix: /workflows
+ applications:
+ namespace: argo-stack
+ service: argo-stack-argocd-server
+ port: 8080
+ pathPrefix: /applications
+ registrations:
+ namespace: argo-stack
+ service: github-repo-registrations-eventsource-svc
+ port: 12000
+ pathPrefix: /registrations
+ api:
+ namespace: calypr-api
+ service: calypr-api
+ port: 3000
+ pathPrefix: /api
+ tenants:
+ namespace: calypr-tenants
+ service: calypr-tenants
+ port: 3001
+ pathPrefix: /tenants
+
# ============================================================================
# Argo CD Applications - Multi-Application Support (REMOVED)
# ============================================================================