auth-proxy: proxies only authenticated requests to upstream, redirects to login page otherwise (#103)

* auth-proxy: inspects authenticated user

* ingress: chart and use in rpuppy

* auth-proxy: make it optional in rpuppy

* kratos: whitelist env pub/priv domains for auth return_to addr

* url-shortener: put behind auth-proxy

* pihole: replace oauth2-client with auth-proxy

* auth-proxy: fix upstream uri generation

* pihole: remove old chart using oauth2

* auth-proxy: remove temporary values file

* url-shortener: check x-user header for authentication

* auth: fix allowed_return_urls list

* auth-proxy: fix current address generation logic

---------

Co-authored-by: Giorgi Lekveishvili <lekva@gl-mbp-m1-max.local>
diff --git a/core/auth/proxy/Dockerfile b/core/auth/proxy/Dockerfile
new file mode 100644
index 0000000..6e8d93c
--- /dev/null
+++ b/core/auth/proxy/Dockerfile
@@ -0,0 +1,5 @@
+FROM gcr.io/distroless/static:nonroot
+
+ARG TARGETARCH
+
+COPY server_${TARGETARCH} /usr/bin/server
diff --git a/core/auth/proxy/Makefile b/core/auth/proxy/Makefile
new file mode 100644
index 0000000..053ab05
--- /dev/null
+++ b/core/auth/proxy/Makefile
@@ -0,0 +1,39 @@
+repo_name ?= giolekva
+podman ?= docker
+ifeq ($(podman), podman)
+manifest_dest=docker://docker.io/$(repo_name)/pcloud-installer:latest
+endif
+
+clean:
+	rm -f server server_*
+
+build: clean
+	go build -o server *.go
+
+build_arm64: export CGO_ENABLED=0
+build_arm64: export GO111MODULE=on
+build_arm64: export GOOS=linux
+build_arm64: export GOARCH=arm64
+build_arm64:
+	go build -o server_arm64 *.go
+
+build_amd64: export CGO_ENABLED=0
+build_amd64: export GO111MODULE=on
+build_amd64: export GOOS=linux
+build_amd64: export GOARCH=amd64
+build_amd64:
+	go build -o server_amd64 *.go
+
+push_arm64: clean build_arm64
+	$(podman) build --platform linux/arm64 --tag=$(repo_name)/auth-proxy:arm64 .
+	$(podman) push $(repo_name)/auth-proxy:arm64
+
+push_amd64: clean build_amd64
+	$(podman) build --platform linux/amd64 --tag=$(repo_name)/auth-proxy:amd64 .
+	$(podman) push $(repo_name)/auth-proxy:amd64
+
+
+push: push_arm64 push_amd64
+	$(podman) manifest create $(repo_name)/auth-proxy:latest $(repo_name)/auth-proxy:arm64 $(repo_name)/auth-proxy:amd64
+	$(podman) manifest push $(repo_name)/auth-proxy:latest $(manifest_dest)
+	$(podman) manifest rm $(repo_name)/auth-proxy:latest
diff --git a/core/auth/proxy/main.go b/core/auth/proxy/main.go
new file mode 100644
index 0000000..8b3d837
--- /dev/null
+++ b/core/auth/proxy/main.go
@@ -0,0 +1,156 @@
+package main
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"net/http/cookiejar"
+	"net/url"
+	"strings"
+)
+
+var port = flag.Int("port", 3000, "Port to listen on")
+var whoAmIAddr = flag.String("whoami-addr", "", "Kratos whoami endpoint address")
+var loginAddr = flag.String("login-addr", "", "Login page address")
+var upstream = flag.String("upstream", "", "Upstream service address")
+
+type user struct {
+	Identity struct {
+		Traits struct {
+			Username string `json:"username"`
+		} `json:"traits"`
+	} `json:"identity"`
+}
+
+type authError struct {
+	Error struct {
+		Status string `json:"status"`
+	} `json:"error"`
+}
+
+func getAddr(r *http.Request) (*url.URL, error) {
+	return url.Parse(fmt.Sprintf(
+		"%s://%s%s",
+		r.Header["X-Forwarded-Scheme"][0],
+		r.Header["X-Forwarded-Host"][0],
+		r.URL.RequestURI()))
+}
+
+func handle(w http.ResponseWriter, r *http.Request) {
+	user, err := queryWhoAmI(r.Cookies())
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if user == nil {
+		if r.Method != http.MethodGet {
+			http.Error(w, "Unauthorized", http.StatusUnauthorized)
+			return
+		}
+		curr, err := getAddr(r)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		addr := fmt.Sprintf("%s?return_to=%s", *loginAddr, curr.String())
+		http.Redirect(w, r, addr, http.StatusSeeOther)
+		return
+	}
+	rc := r.Clone(context.Background())
+	rc.Header.Add("X-User", user.Identity.Traits.Username)
+	ru, err := url.Parse(fmt.Sprintf("http://%s%s", *upstream, r.URL.RequestURI()))
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	rc.URL = ru
+	rc.RequestURI = ""
+	client := &http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		},
+		CheckRedirect: func(req *http.Request, via []*http.Request) error {
+			return http.ErrUseLastResponse
+		},
+	}
+	resp, err := client.Do(rc)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	for name, values := range resp.Header {
+		for _, value := range values {
+			w.Header().Add(name, value)
+		}
+	}
+	w.WriteHeader(resp.StatusCode)
+	if _, err := io.Copy(w, resp.Body); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+func queryWhoAmI(cookies []*http.Cookie) (*user, error) {
+	jar, err := cookiejar.New(nil)
+	if err != nil {
+		return nil, err
+	}
+	client := &http.Client{
+		Jar: jar,
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		},
+	}
+	addr, err := url.Parse(*whoAmIAddr)
+	if err != nil {
+		return nil, err
+	}
+	client.Jar.SetCookies(addr, cookies)
+	resp, err := client.Get(*whoAmIAddr)
+	if err != nil {
+		return nil, err
+	}
+	data := make(map[string]any)
+	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+		return nil, err
+	}
+	// TODO(gio): remove debugging
+	b, err := json.MarshalIndent(data, "", "  ")
+	if err != nil {
+		return nil, err
+	}
+	fmt.Println(string(b))
+	var buf bytes.Buffer
+	if err := json.NewEncoder(&buf).Encode(data); err != nil {
+		return nil, err
+	}
+	tmp := buf.String()
+	if resp.StatusCode == http.StatusOK {
+		u := &user{}
+		if err := json.NewDecoder(strings.NewReader(tmp)).Decode(u); err != nil {
+			return nil, err
+		}
+		return u, nil
+	}
+	e := &authError{}
+	if err := json.NewDecoder(strings.NewReader(tmp)).Decode(e); err != nil {
+		return nil, err
+	}
+	if e.Error.Status == "Unauthorized" {
+		return nil, nil
+	}
+	return nil, fmt.Errorf("Unknown error: %s", tmp)
+}
+
+func main() {
+	flag.Parse()
+	http.HandleFunc("/", handle)
+	fmt.Printf("Starting HTTP server on port: %d\n", *port)
+	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
+}
diff --git a/core/auth/ui/Makefile b/core/auth/ui/Makefile
index f3b9b63..23ae76b 100644
--- a/core/auth/ui/Makefile
+++ b/core/auth/ui/Makefile
@@ -1,5 +1,8 @@
 repo_name ?= dtabidze
 podman ?= docker
+ifeq ($(podman), podman)
+manifest_dest=docker://docker.io/$(repo_name)/pcloud-installer:latest
+endif
 
 clean:
 	rm -f server server_*
@@ -32,5 +35,5 @@
 
 push: push_arm64 push_amd64
 	$(podman) manifest create $(repo_name)/auth-ui:latest $(repo_name)/auth-ui:arm64 $(repo_name)/auth-ui:amd64
-	$(podman) manifest push $(repo_name)/auth-ui:latest
+	$(podman) manifest push $(repo_name)/auth-ui:latest $(manifest_dest)
 	$(podman) manifest rm $(repo_name)/auth-ui:latest
diff --git a/core/auth/ui/main.go b/core/auth/ui/main.go
index 7cb1f4d..3de264c 100644
--- a/core/auth/ui/main.go
+++ b/core/auth/ui/main.go
@@ -239,9 +239,14 @@
 		// 	HttpOnly: true,
 		// })
 	}
+	returnTo := r.Form.Get("return_to")
 	flow, ok := r.Form["flow"]
 	if !ok {
-		http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
+		addr := s.kratos + "/self-service/login/browser"
+		if returnTo != "" {
+			addr += fmt.Sprintf("?return_to=%s", returnTo)
+		}
+		http.Redirect(w, r, addr, http.StatusSeeOther)
 		return
 	}
 	csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
@@ -289,6 +294,32 @@
 	return resp, nil
 }
 
+func postFormToKratos(flowType, flow string, cookies []*http.Cookie, data url.Values) (*http.Response, error) {
+	jar, err := cookiejar.New(nil)
+	if err != nil {
+		return nil, err
+	}
+	client := &http.Client{
+		Jar: jar,
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		},
+		CheckRedirect: func(req *http.Request, via []*http.Request) error {
+			return http.ErrUseLastResponse
+		},
+	}
+	b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
+	if err != nil {
+		return nil, err
+	}
+	client.Jar.SetCookies(b, cookies)
+	resp, err := client.PostForm(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), data)
+	if err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
 type logoutResp struct {
 	LogoutURL string `json:"logout_url"`
 }
@@ -360,6 +391,7 @@
 	if err != nil {
 		return err
 	}
+	fmt.Printf("++ %s\n", respBody)
 	t, err := regogo.Get(string(respBody), "input.ui.messages[0].type")
 	if err != nil {
 		return err
@@ -384,21 +416,17 @@
 		http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
 		return
 	}
-	req := loginReq{
-		CSRFToken: r.FormValue("csrf_token"),
-		Method:    "password",
-		Password:  r.FormValue("password"),
-		Username:  r.FormValue("username"),
+	req := url.Values{
+		"csrf_token": []string{r.FormValue("csrf_token")},
+		"method":     []string{"password"},
+		"password":   []string{r.FormValue("password")},
+		"identifier": []string{r.FormValue("username")},
 	}
-	var reqBody bytes.Buffer
-	if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	resp, err := postToKratos("login", flow[0], r.Cookies(), &reqBody)
-	if err == nil {
-		err = extractError(resp.Body)
-	}
+	resp, err := postFormToKratos("login", flow[0], r.Cookies(), req)
+	fmt.Printf("--- %d\n", resp.StatusCode)
+	var vv bytes.Buffer
+	io.Copy(&vv, resp.Body)
+	fmt.Println(vv.String())
 	if err != nil {
 		if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
 			redirectTo, err := s.hydra.LoginRejectChallenge(challenge.Value, err.Error())
@@ -429,7 +457,11 @@
 		http.Redirect(w, r, redirectTo, http.StatusSeeOther)
 		return
 	}
-	http.Redirect(w, r, "/", http.StatusSeeOther)
+	if resp.StatusCode == http.StatusSeeOther {
+		http.Redirect(w, r, resp.Header.Get("Location"), http.StatusSeeOther)
+	} else {
+		http.Redirect(w, r, "/", http.StatusSeeOther)
+	}
 }
 
 func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
diff --git a/core/installer/app.go b/core/installer/app.go
index 6c83376..8cfb2b5 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -147,7 +147,7 @@
 	...
 }
 
-helm: {
+helmValidate: {
 	for key, value in helm {
 		"\(key)": #Helm & value & {
 			name: key
@@ -184,7 +184,7 @@
 }
 
 output: {
-	for name, r in helm {
+	for name, r in helmValidate {
 		"\(name)": #HelmRelease & {
 			_name: name
 			_chart: r.chart
diff --git a/core/installer/repoio.go b/core/installer/repoio.go
index 374f75f..5b90327 100644
--- a/core/installer/repoio.go
+++ b/core/installer/repoio.go
@@ -388,6 +388,7 @@
 		}
 		switch def.Kind() {
 		case KindBoolean:
+			ret[k] = v
 		case KindString:
 			ret[k] = v
 		case KindNetwork:
diff --git a/core/installer/values-tmpl/core-auth.cue b/core/installer/values-tmpl/core-auth.cue
index 99840ab..192f806 100644
--- a/core/installer/values-tmpl/core-auth.cue
+++ b/core/installer/values-tmpl/core-auth.cue
@@ -225,6 +225,10 @@
 						}
 						selfservice: {
 							default_browser_return_url: "https://accounts-ui.\(global.domain)"
+							allowed_return_urls: [
+								"https://*.\(global.domain)/",
+								"https://*.\(global.privateDomain)",
+						    ]
 							methods: {
 								password: {
 									enabled: true
diff --git a/core/installer/values-tmpl/pihole.cue b/core/installer/values-tmpl/pihole.cue
index a1ec66a..35d4c51 100644
--- a/core/installer/values-tmpl/pihole.cue
+++ b/core/installer/values-tmpl/pihole.cue
@@ -1,6 +1,7 @@
 input: {
 	network: #Network
 	subdomain: string
+	requireAuth: bool
 }
 
 _domain: "\(input.subdomain).\(input.network.domain)"
@@ -18,17 +19,15 @@
 		tag: "v5.8.1"
 		pullPolicy: "IfNotPresent"
 	}
+	authProxy: {
+		repository: "giolekva"
+		name: "auth-proxy"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
 }
 
 charts: {
-	oauth2Client: {
-		chart: "charts/oauth2-client"
-		sourceRef: {
-			kind: "GitRepository"
-			name: "pcloud"
-			namespace: global.id
-		}
-	}
 	pihole: {
 		chart: "charts/pihole"
 		sourceRef: {
@@ -37,80 +36,110 @@
 			namespace: global.id
 		}
 	}
-}
-
-_oauth2ClientSecretName: "oauth2-client"
-
-helm: {
-	"oauth2-client": {
-		chart: charts.oauth2Client
-		values: {
-			name: "oauth2-client"
-			secretName: _oauth2ClientSecretName
-			grantTypes: ["authorization_code"]
-			responseTypes: ["code"]
-			scope: "openid profile email"
-			redirectUris: ["https://\(_domain)/oauth2/callback"]
-			hydraAdmin: "http://hydra-admin.\(global.namespacePrefix)core-auth.svc.cluster.local"
+	ingress: {
+		chart: "charts/ingress"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
 		}
 	}
+	authProxy: {
+		chart: "charts/auth-proxy"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+}
+
+_piholeServiceName: "pihole-web"
+_authProxyServiceName: "auth-proxy"
+_httpPortName: "http"
+_serviceWebPort: 80
+
+helm: {
 	pihole: {
 		chart: charts.pihole
 		values: {
-			domain: _domain
-			pihole: {
-				fullnameOverride: "pihole"
-				persistentVolumeClaim: { // TODO(gio): create volume separately as a dependency
+			fullnameOverride: "pihole"
+			persistentVolumeClaim: { // TODO(gio): create volume separately as a dependency
+				enabled: true
+				size: "5Gi"
+			}
+			admin: {
+				enabled: false
+			}
+			ingress: {
+				enabled: false
+			}
+			serviceDhcp: {
+				enabled: false
+			}
+			serviceDns: {
+				type: "ClusterIP"
+			}
+			serviceWeb: {
+				type: "ClusterIP"
+				http: {
 					enabled: true
-					size: "5Gi"
+					port: _serviceWebPort
 				}
-				admin: {
+				https: {
 					enabled: false
 				}
-				ingress: {
-					enabled: false
+			}
+			virtualHost: _domain
+			resources: {
+				requests: {
+					cpu: "250m"
+					memory: "100M"
 				}
-				serviceDhcp: {
-					enabled: false
+				limits: {
+					cpu: "500m"
+					memory: "250M"
 				}
-				serviceDns: {
-					type: "ClusterIP"
-				}
-				serviceWeb: {
-					type: "ClusterIP"
-					http: {
-						enabled: true
-					}
-					https: {
-						enabled: false
-					}
-				}
-				virtualHost: _domain
-				resources: {
-					requests: {
-						cpu: "250m"
-						memory: "100M"
-					}
-					limits: {
-						cpu: "500m"
-						memory: "250M"
-					}
-				}
+			}
+			image: {
+				repository: images.pihole.fullName
+				tag: images.pihole.tag
+				pullPolicy: images.pihole.pullPolicy
+			}
+		}
+	}
+	if input.requireAuth {
+		"auth-proxy": {
+			chart: charts.authProxy
+			values: {
 				image: {
-					repository: images.pihole.fullName
-					tag: images.pihole.tag
-					pullPolicy: images.pihole.pullPolicy
+					repository: images.authProxy.fullName
+					tag: images.authProxy.tag
+					pullPolicy: images.authProxy.pullPolicy
 				}
+				upstream: "\(_piholeServiceName).\(release.namespace).svc.cluster.local"
+				whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
+				loginAddr: "https://accounts-ui.\(global.domain)/login"
+				portName: _httpPortName
 			}
-			oauth2: {
-				cookieSecret: "1234123443214321"
-				secretName: _oauth2ClientSecretName
-				issuer: "https://hydra.\(global.domain)"
-			}
-			configName: "oauth2-proxy"
-			profileUrl: "https://accounts-ui.\(global.domain)"
+		}
+	}
+	ingress: {
+		chart: charts.ingress
+		values: {
+			domain: _domain
 			ingressClassName: input.network.ingressClass
 			certificateIssuer: input.network.certificateIssuer
+			service: {
+				if input.requireAuth {
+					name: _authProxyServiceName
+					port: name: _httpPortName
+				}
+				if !input.requireAuth {
+					name: _piholeServiceName
+					port: number: _serviceWebPort
+				}
+			}
 		}
 	}
 }
diff --git a/core/installer/values-tmpl/rpuppy.cue b/core/installer/values-tmpl/rpuppy.cue
index 8950d6c..ff316a0 100644
--- a/core/installer/values-tmpl/rpuppy.cue
+++ b/core/installer/values-tmpl/rpuppy.cue
@@ -1,6 +1,7 @@
 input: {
 	network: #Network
 	subdomain: string
+	requireAuth: bool
 }
 
 _domain: "\(input.subdomain).\(input.network.domain)"
@@ -18,6 +19,12 @@
 		tag: "latest"
 		pullPolicy: "Always"
 	}
+	authProxy: {
+		repository: "giolekva"
+		name: "auth-proxy"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
 }
 
 charts: {
@@ -29,20 +36,71 @@
 			namespace: global.id
 		}
 	}
+	ingress: {
+		chart: "charts/ingress"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+	authProxy: {
+		chart: "charts/auth-proxy"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
 }
 
+_rpuppyServiceName: "rpuppy"
+_authProxyServiceName: "auth-proxy"
+_httpPortName: "http"
+
 helm: {
 	rpuppy: {
 		chart: charts.rpuppy
 		values: {
-			ingressClassName: input.network.ingressClass
-			certificateIssuer: input.network.certificateIssuer
-			domain: _domain
 			image: {
 				repository: images.rpuppy.fullName
 				tag: images.rpuppy.tag
 				pullPolicy: images.rpuppy.pullPolicy
 			}
+			portName: _httpPortName
+		}
+	}
+	if input.requireAuth {
+		"auth-proxy": {
+			chart: charts.authProxy
+			values: {
+				image: {
+					repository: images.authProxy.fullName
+					tag: images.authProxy.tag
+					pullPolicy: images.authProxy.pullPolicy
+				}
+				upstream: "\(_rpuppyServiceName).\(release.namespace).svc.cluster.local"
+				whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
+				loginAddr: "https://accounts-ui.\(global.domain)/login"
+				portName: _httpPortName
+			}
+		}
+	}
+	ingress: {
+		chart: charts.ingress
+		values: {
+			domain: _domain
+			ingressClassName: input.network.ingressClass
+			certificateIssuer: input.network.certificateIssuer
+			service: {
+				if input.requireAuth {
+					name: _authProxyServiceName
+				}
+				if !input.requireAuth {
+					name: _rpuppyServiceName
+				}
+				port: name: _httpPortName
+			}
 		}
 	}
 }
diff --git a/core/installer/values-tmpl/url-shortener.cue b/core/installer/values-tmpl/url-shortener.cue
index a3f6d3b..7d854e8 100644
--- a/core/installer/values-tmpl/url-shortener.cue
+++ b/core/installer/values-tmpl/url-shortener.cue
@@ -1,6 +1,7 @@
 input: {
     network: #Network
     subdomain: string
+	requireAuth: bool
 }
 
 _domain: "\(input.subdomain).\(input.network.domain)"
@@ -18,6 +19,12 @@
 		tag: "latest"
 		pullPolicy: "Always"
 	}
+	authProxy: {
+		repository: "giolekva"
+		name: "auth-proxy"
+		tag: "latest"
+		pullPolicy: "Always"
+	}
 }
 
 charts: {
@@ -29,15 +36,32 @@
             namespace: global.id
         }
     }
+	ingress: {
+		chart: "charts/ingress"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
+	authProxy: {
+		chart: "charts/auth-proxy"
+		sourceRef: {
+			kind: "GitRepository"
+			name: "pcloud"
+			namespace: global.id
+		}
+	}
 }
 
+_urlShortenerServiceName: "url-shortener"
+_authProxyServiceName: "auth-proxy"
+_httpPortName: "http"
+
 helm: {
     "url-shortener": {
         chart: charts.urlShortener
         values: {
-            ingressClassName: input.network.ingressClass
-            certificateIssuer: input.network.certificateIssuer
-            domain: _domain
             storage: {
                 size: "1Gi"
             }
@@ -46,7 +70,40 @@
 				tag: images.urlShortener.tag
 				pullPolicy: images.urlShortener.pullPolicy
 			}
-            port: 8080
+            portName: _httpPortName
         }
     }
+	if input.requireAuth {
+		"auth-proxy": {
+			chart: charts.authProxy
+			values: {
+				image: {
+					repository: images.authProxy.fullName
+					tag: images.authProxy.tag
+					pullPolicy: images.authProxy.pullPolicy
+				}
+				upstream: "\(_urlShortenerServiceName).\(release.namespace).svc.cluster.local"
+				whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
+				loginAddr: "https://accounts-ui.\(global.domain)/login"
+				portName: _httpPortName
+			}
+		}
+	}
+	ingress: {
+		chart: charts.ingress
+		values: {
+			domain: _domain
+			ingressClassName: input.network.ingressClass
+			certificateIssuer: input.network.certificateIssuer
+			service: {
+				if input.requireAuth {
+					name: _authProxyServiceName
+				}
+				if !input.requireAuth {
+					name: _urlShortenerServiceName
+				}
+				port: name: _httpPortName
+			}
+		}
+	}
 }
diff --git a/core/installer/welcome/appmanager-tmpl/app.html b/core/installer/welcome/appmanager-tmpl/app.html
index 4fa4766..aebbd39 100644
--- a/core/installer/welcome/appmanager-tmpl/app.html
+++ b/core/installer/welcome/appmanager-tmpl/app.html
@@ -7,7 +7,7 @@
       <label for="{{ $name }}">
         <span>{{ $name }}</span>
       </label>
-	  <input type="checkbox" role="swtich" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.value)" {{ if $readonly }}disabled{{ end }} {{ if index $data $name }}checked{{ end }}/>
+	  <input type="checkbox" role="swtich" name="{{ $name }}" oninput="valueChanged({{ $name }}, this.checked)" {{ if $readonly }}disabled{{ end }} {{ if index $data $name }}checked{{ end }}/>
     {{ else if eq $schema.Kind 1 }}
       <label for="{{ $name }}">
         <span>{{ $name }}</span>