@@ -69,11 +69,20 @@ export interface TraefikIngressConstructProps {
6969 * @default false
7070 */
7171 enableSmtp ?: boolean ;
72+
73+ /**
74+ * Reference to the webmail auth proxy service (handles auth + redirect)
75+ * When provided, webmail ingress will route through this proxy instead of
76+ * using ForwardAuth middleware, enabling proper redirect on auth failure.
77+ */
78+ webmailAuthProxyService ?: kplus . Service ;
7279}
7380
7481export class TraefikIngressConstruct extends Construct {
7582 public readonly httpIngress : k8s . KubeIngress ;
83+ public readonly webmailIngress : k8s . KubeIngress ;
7684 public readonly antispamIngress : k8s . KubeIngress ;
85+ public readonly ssoPhpIngress : k8s . KubeIngress ;
7786 public readonly tcpRoutes : traefik . IngressRouteTcp [ ] ;
7887
7988 constructor ( scope : Construct , id : string , props : TraefikIngressConstructProps ) {
@@ -131,6 +140,8 @@ export class TraefikIngressConstruct extends Construct {
131140 } ,
132141 } ,
133142 } ,
143+ // SSO path (handled by Flask at /sso/login)
144+ // Note: /sso.php routes are handled by separate ssoPhpIngress with redirect middleware
134145 {
135146 path : '/sso' ,
136147 pathType : 'Prefix' ,
@@ -167,19 +178,7 @@ export class TraefikIngressConstruct extends Construct {
167178 } ,
168179 } ,
169180 } ,
170- // Webmail path
171- {
172- path : '/webmail' ,
173- pathType : 'Prefix' ,
174- backend : {
175- service : {
176- name : props . webmailService . name ,
177- port : {
178- number : 80 ,
179- } ,
180- } ,
181- } ,
182- } ,
181+ // NOTE: /webmail path is handled by separate webmailIngress with ForwardAuth middleware
183182 // Health check endpoint on front service
184183 {
185184 path : '/health' ,
@@ -241,6 +240,41 @@ export class TraefikIngressConstruct extends Construct {
241240 } ,
242241 } ) ;
243242
243+ // Create ForwardAuth Middleware for webmail authentication
244+ // This middleware checks if user is authenticated and passes user credentials to webmail
245+ // The auth endpoint returns X-User and X-User-Token headers that Roundcube uses for authentication
246+ new traefik . Middleware ( this , 'webmail-auth-middleware' , {
247+ metadata : {
248+ name : 'mailu-webmail-auth' ,
249+ namespace : props . namespace ,
250+ } ,
251+ spec : {
252+ forwardAuth : {
253+ // Point to user auth endpoint that verifies session and returns user info headers
254+ address : `http://${ props . adminService . name } .${ props . namespace } .svc.cluster.local:8080/internal/auth/user` ,
255+ // Pass through X-User and X-User-Token headers from auth response to webmail
256+ // The Roundcube plugin (patched) expects these headers for authentication
257+ authResponseHeaders : [ 'X-User' , 'X-User-Token' ] ,
258+ // Don't trust X-Forwarded-* headers from the authentication request
259+ trustForwardHeader : false ,
260+ } ,
261+ } ,
262+ } ) ;
263+
264+ // Create StripPrefix Middleware to remove /webmail before forwarding to webmail service
265+ // Webmail nginx expects requests at root /, but ingress sends /webmail/*
266+ new traefik . Middleware ( this , 'webmail-strip-prefix' , {
267+ metadata : {
268+ name : 'mailu-webmail-strip-prefix' ,
269+ namespace : props . namespace ,
270+ } ,
271+ spec : {
272+ stripPrefix : {
273+ prefixes : [ '/webmail' ] ,
274+ } ,
275+ } ,
276+ } ) ;
277+
244278 // Create StripPrefix Middleware to remove /admin/antispam before forwarding to Rspamd
245279 // Rspamd's web interface is at the root path /, not /admin/antispam/
246280 new traefik . Middleware ( this , 'antispam-strip-prefix' , {
@@ -255,6 +289,140 @@ export class TraefikIngressConstruct extends Construct {
255289 } ,
256290 } ) ;
257291
292+ // Create RedirectRegex Middleware to redirect sso.php to /sso/login
293+ // Roundcube's mailu plugin expects sso.php but Flask serves /sso/login
294+ new traefik . Middleware ( this , 'sso-php-redirect' , {
295+ metadata : {
296+ name : 'mailu-sso-php-redirect' ,
297+ namespace : props . namespace ,
298+ } ,
299+ spec : {
300+ redirectRegex : {
301+ regex : '^https?://([^/]+)(/webmail)?/sso\\.php(.*)$' ,
302+ replacement : 'https://${1}/sso/login${3}' ,
303+ permanent : false ,
304+ } ,
305+ } ,
306+ } ) ;
307+
308+ // Separate Ingress for sso.php redirect compatibility
309+ // Roundcube's mailu plugin redirects to sso.php but Flask serves /sso/login
310+ // This ingress catches sso.php requests and redirects to /sso/login
311+ this . ssoPhpIngress = new k8s . KubeIngress ( this , 'sso-php-ingress' , {
312+ metadata : {
313+ name : 'mailu-sso-php' ,
314+ namespace : props . namespace ,
315+ annotations : {
316+ 'cert-manager.io/cluster-issuer' : certIssuer ,
317+ // Apply redirect middleware to rewrite sso.php to /sso/login
318+ 'traefik.ingress.kubernetes.io/router.middlewares' : `${ props . namespace } -mailu-sso-php-redirect@kubernetescrd` ,
319+ // Higher priority to ensure this ingress is checked before mailu-webmail ingress
320+ 'traefik.ingress.kubernetes.io/router.priority' : '100' ,
321+ } ,
322+ } ,
323+ spec : {
324+ ingressClassName : 'traefik' ,
325+ tls : [
326+ {
327+ hosts : [ props . hostname ] ,
328+ secretName : 'mailu-tls' ,
329+ } ,
330+ ] ,
331+ rules : [
332+ {
333+ host : props . hostname ,
334+ http : {
335+ paths : [
336+ {
337+ path : '/sso.php' ,
338+ pathType : 'Exact' ,
339+ backend : {
340+ service : {
341+ name : props . adminService . name ,
342+ port : {
343+ number : 8080 ,
344+ } ,
345+ } ,
346+ } ,
347+ } ,
348+ {
349+ path : '/webmail/sso.php' ,
350+ pathType : 'Exact' ,
351+ backend : {
352+ service : {
353+ name : props . adminService . name ,
354+ port : {
355+ number : 8080 ,
356+ } ,
357+ } ,
358+ } ,
359+ } ,
360+ ] ,
361+ } ,
362+ } ,
363+ ] ,
364+ } ,
365+ } ) ;
366+
367+ // Separate Ingress for webmail with authentication
368+ // When auth proxy is provided, route through it (handles auth + redirect on failure)
369+ // Otherwise, use ForwardAuth middleware (returns 403 on auth failure - legacy behavior)
370+ // NOTE: We do NOT strip /webmail prefix - the webmail nginx is configured (via WEB_WEBMAIL env)
371+ // to handle requests at /webmail and generate correct asset paths
372+ const useAuthProxy = ! ! props . webmailAuthProxyService ;
373+ const webmailBackendService = useAuthProxy
374+ ? props . webmailAuthProxyService !
375+ : props . webmailService ;
376+
377+ // Build annotations - only include ForwardAuth when not using auth proxy
378+ const webmailIngressAnnotations : Record < string , string > = {
379+ // Use cert-manager to provision Let's Encrypt certificate
380+ 'cert-manager.io/cluster-issuer' : certIssuer ,
381+ } ;
382+ if ( ! useAuthProxy ) {
383+ // Apply ForwardAuth middleware when not using auth proxy (legacy behavior)
384+ webmailIngressAnnotations [ 'traefik.ingress.kubernetes.io/router.middlewares' ] =
385+ `${ props . namespace } -mailu-webmail-auth@kubernetescrd` ;
386+ }
387+
388+ this . webmailIngress = new k8s . KubeIngress ( this , 'webmail-auth-ingress' , {
389+ metadata : {
390+ name : 'mailu-webmail-auth' ,
391+ namespace : props . namespace ,
392+ annotations : webmailIngressAnnotations ,
393+ } ,
394+ spec : {
395+ ingressClassName : 'traefik' ,
396+ tls : [
397+ {
398+ hosts : [ props . hostname ] ,
399+ secretName : 'mailu-tls' ,
400+ } ,
401+ ] ,
402+ rules : [
403+ {
404+ host : props . hostname ,
405+ http : {
406+ paths : [
407+ {
408+ path : '/webmail' ,
409+ pathType : 'Prefix' ,
410+ backend : {
411+ service : {
412+ name : webmailBackendService . name ,
413+ port : {
414+ number : 80 ,
415+ } ,
416+ } ,
417+ } ,
418+ } ,
419+ ] ,
420+ } ,
421+ } ,
422+ ] ,
423+ } ,
424+ } ) ;
425+
258426 // Separate Ingress for Rspamd antispam web UI with ForwardAuth middleware
259427 // This proxies /admin/antispam/* to Rspamd's web interface on port 11334
260428 // and ensures only authenticated global admins can access it
0 commit comments