Compare commits

...

4 Commits

Author SHA1 Message Date
Jeff McCune
0060740b76 (#82) ingress gateway AuthorizationPolicy
This patch adds a `RequestAuthentication` and `AuthorizationPolicy` rule
to protect all requests flowing through the default ingress gateway.

Consider a browser request for httpbin.k2.example.com representing any
arbitrary host with a valid destination inside the service mesh.  The
default ingress gateway will check if there is already an
x-oidc-id-token header, and if so validate the token is issued by
ZITADEL and the aud value contains the ZITADEL project number.

If the header is not present, the request is forwarded to oauth2-proxy
in the istio-ingress namespace.  This auth proxy is configured to start
the oidc auth flow with a redirect back to /holos/oidc/callback of the
Host: value originally provided in the browser request.

Closes: #82
2024-04-01 20:37:34 -07:00
Jeff McCune
bf8a4af579 (#82) ingressgateway ExtAuthzHttp provider
This patch adds an ingress gateway extauthz provider.  Because ZITADEL
returns all applications associated with a ZITADEL project in the aud
claim, it makes sense to have one ingress auth proxy at the initial
ingress gateway so we can get the ID token in the request header for
backend namespaces to match using `RequestAuthentication` and
`AuthorizationPolicy`.

This change likely makes the additional per-stage auth proxies
unnecessary and over-engineered.  Backend namespaces will have access to
the ID token.
2024-04-01 16:53:11 -07:00
Jeff McCune
dc057fe39d (#89) Add platform project hosts for argocd, grafana, and prometheus
Certificates are issued by the provisioner and synced to the workload
clusters.
2024-04-01 13:09:46 -07:00
Jeff McCune
9877ab131a (#89) Platform Project
This patch manages a platform project to host platform level services
like ArgoCD, Kube Prom Stack, Kiali, etc...
2024-04-01 11:46:02 -07:00
9 changed files with 329 additions and 12 deletions

View File

@@ -6,9 +6,10 @@ package holos
// clusterName is the value of the --cluster-name flag, the cluster currently being manged / rendered.
clusterName: string | *#ClusterName
extensionProviderMap: [Name=_]: {
name: Name
}
// for extAuthzHttp extension providers
extensionProviderMap: [Name=_]: #ExtAuthzProxy & {name: Name}
// for other extension providers like zipkin
extensionProviderExtraMap: [Name=_]: {name: Name}
config: {
accessLogEncoding: string | *"JSON"
@@ -21,7 +22,10 @@ package holos
enablePrometheusMerge: false | *true
rootNamespace: string | *"istio-system"
trustDomain: string | *"cluster.local"
extensionProviders: [for x in extensionProviderMap {x}]
extensionProviders: [
for x in extensionProviderMap {x},
for y in extensionProviderExtraMap {y},
]
}
}

View File

@@ -52,6 +52,10 @@ spec: components: HelmChartList: [
}
}
apiObjectMap: OBJECTS.apiObjectMap
// Auth Proxy
apiObjectMap: _IngressAuthProxy.Deployment.apiObjectMap
// Auth Policy
apiObjectMap: _IngressAuthProxy.Policy.apiObjectMap
},
]

View File

@@ -1,5 +1,9 @@
package holos
// Ingress Gateway default auth proxy
let Provider = _IngressAuthProxy.authproxy.provider
let Service = _IngressAuthProxy.service
#MeshConfig: extensionProviderMap: (Provider): envoyExtAuthzHttp: service: Service
// Istio meshconfig
// TODO: Generate per-project extauthz providers.
_MeshConfig: (#MeshConfig & {projects: _Projects}).config

View File

@@ -1,5 +1,277 @@
package holos
import "encoding/yaml"
#InstancePrefix: "prod-mesh"
#IstioVersion: "1.21.0"
// The ingress gateway auth proxy is used by multiple cue instances.
// AUTHPROXY configures one oauth2-proxy deployment for each host in each stage of a project. Multiple deployments per stage are used to narrow down the cookie domain.
_IngressAuthProxy: {
Name: "authproxy"
Namespace: "istio-ingress"
service: "\(Name).\(Namespace).svc.cluster.local"
authproxy: #IngressAuthProxySpec | *#Platform.authproxy
Domains: [DOMAIN=string]: {name: DOMAIN}
Domains: (#Platform.org.domain): _
Domains: "\(#ClusterName).\(#Platform.org.domain)": _
let Metadata = {
name: string
namespace: Namespace
labels: "app.kubernetes.io/name": name
labels: "app.kubernetes.io/part-of": "istio-ingressgateway"
...
}
let ProxyMetadata = Metadata & {name: Name}
let RedisMetadata = Metadata & {name: Name + "-redis"}
// Deployment represents the oauth2-proxy deployment
Deployment: #APIObjects & {
apiObjects: {
// oauth2-proxy
ExternalSecret: (Name): metadata: ProxyMetadata
// Place the ID token in a header that does not conflict with the Authorization header.
// Refer to: https://github.com/oauth2-proxy/oauth2-proxy/issues/1877#issuecomment-1364033723
ConfigMap: (Name): {
metadata: ProxyMetadata
data: "config.yaml": yaml.Marshal(AuthProxyConfig)
let AuthProxyConfig = {
injectResponseHeaders: [{
name: "x-oidc-id-token"
values: [{claim: "id_token"}]
}]
providers: [{
id: "Holos Platform"
name: "Holos Platform"
provider: "oidc"
scope: "openid profile email groups offline_access urn:zitadel:iam:org:domain:primary:\(authproxy.orgDomain)"
clientID: authproxy.clientID
clientSecretFile: "/dev/null"
code_challenge_method: "S256"
loginURLParameters: [{
default: ["force"]
name: "approval_prompt"
}]
oidcConfig: {
issuerURL: authproxy.issuer
audienceClaims: ["aud"]
emailClaim: "email"
groupsClaim: "groups"
userIDClaim: "sub"
}
}]
server: BindAddress: ":4180"
upstreamConfig: upstreams: [{
id: "static://200"
path: "/"
static: true
staticCode: 200
}]
}
}
Deployment: (Name): #Deployment & {
metadata: ProxyMetadata
spec: {
replicas: 1
selector: matchLabels: ProxyMetadata.labels
template: {
metadata: labels: ProxyMetadata.labels
metadata: labels: #IstioSidecar
spec: {
securityContext: seccompProfile: type: "RuntimeDefault"
containers: [{
image: "quay.io/oauth2-proxy/oauth2-proxy:v7.6.0"
imagePullPolicy: "IfNotPresent"
name: "oauth2-proxy"
volumeMounts: [{
name: "config"
mountPath: "/config"
readOnly: true
}]
args: [
// callback url is proxy prefix + /callback
"--proxy-prefix=" + authproxy.proxyPrefix,
"--email-domain=*",
"--session-store-type=redis",
"--redis-connection-url=redis://\(RedisMetadata.name):6379",
"--cookie-refresh=12h",
"--cookie-expire=2160h",
"--cookie-secure=true",
"--cookie-name=__Secure-\(#ClusterName)-ingress-\(Name)",
"--cookie-samesite=lax",
for domain in Domains {"--cookie-domain=.\(domain.name)"},
for domain in Domains {"--cookie-domain=\(domain.name)"},
for domain in Domains {"--whitelist-domain=.\(domain.name)"},
for domain in Domains {"--whitelist-domain=\(domain.name)"},
"--cookie-csrf-per-request=true",
"--cookie-csrf-expire=120s",
// will skip authentication for OPTIONS requests
"--skip-auth-preflight=true",
"--real-client-ip-header=X-Forwarded-For",
"--skip-provider-button=true",
"--auth-logging",
"--alpha-config=/config/config.yaml",
]
env: [{
name: "OAUTH2_PROXY_COOKIE_SECRET"
// echo '{"cookiesecret":"'$(LC_ALL=C tr -dc "[:alpha:]" </dev/random | tr '[:upper:]' '[:lower:]' | head -c 32)'"}' | holos create secret -n istio-ingress --append-hash=false --data-stdin authproxy
valueFrom: secretKeyRef: {
key: "cookiesecret"
name: Name
}
}]
ports: [{
containerPort: 4180
protocol: "TCP"
}]
securityContext: {
seccompProfile: type: "RuntimeDefault"
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 8192
runAsGroup: 8192
capabilities: drop: ["ALL"]
}
}]
volumes: [{name: "config", configMap: name: Name}]
}
}
}
}
Service: (Name): #Service & {
metadata: ProxyMetadata
spec: selector: ProxyMetadata.labels
spec: ports: [
{port: 4180, targetPort: 4180, protocol: "TCP", name: "http"},
]
}
VirtualService: (Name): #VirtualService & {
metadata: ProxyMetadata
spec: hosts: ["*"]
spec: gateways: ["istio-ingress/default"]
spec: http: [{
match: [{uri: prefix: authproxy.proxyPrefix}]
route: [{
destination: host: Name
destination: port: number: 4180
}]
}]
}
// redis
ConfigMap: (RedisMetadata.name): {
metadata: RedisMetadata
data: "redis.conf": """
maxmemory 128mb
maxmemory-policy allkeys-lru
"""
}
Deployment: (RedisMetadata.name): {
metadata: RedisMetadata
spec: {
selector: matchLabels: RedisMetadata.labels
template: {
metadata: labels: RedisMetadata.labels
metadata: labels: #IstioSidecar
spec: securityContext: seccompProfile: type: "RuntimeDefault"
spec: {
containers: [{
command: [
"redis-server",
"/redis-master/redis.conf",
]
env: [{
name: "MASTER"
value: "true"
}]
image: "quay.io/holos/redis:7.2.4"
livenessProbe: {
initialDelaySeconds: 15
tcpSocket: port: "redis"
}
name: "redis"
ports: [{
containerPort: 6379
name: "redis"
}]
readinessProbe: {
exec: command: [
"redis-cli",
"ping",
]
initialDelaySeconds: 5
}
resources: limits: cpu: "0.5"
securityContext: {
seccompProfile: type: "RuntimeDefault"
allowPrivilegeEscalation: false
capabilities: drop: ["ALL"]
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
}
volumeMounts: [{
mountPath: "/redis-master-data"
name: "data"
}, {
mountPath: "/redis-master"
name: "config"
}]
}]
volumes: [{
emptyDir: {}
name: "data"
}, {
configMap: name: RedisMetadata.name
name: "config"
}]
}
}
}
}
Service: (RedisMetadata.name): #Service & {
metadata: RedisMetadata
spec: selector: RedisMetadata.labels
spec: type: "ClusterIP"
spec: ports: [{
name: "redis"
port: 6379
protocol: "TCP"
targetPort: 6379
}]
}
}
}
// Policy represents the AuthorizationPolicy and RequestAuthentication policy
Policy: #APIObjects & {
apiObjects: {
RequestAuthentication: (Name): #RequestAuthentication & {
metadata: Metadata & {name: Name}
spec: jwtRules: [{
audiences: ["\(authproxy.projectID)"]
forwardOriginalToken: true
fromHeaders: [{name: authproxy.idTokenHeader}]
issuer: authproxy.issuer
}]
spec: selector: matchLabels: istio: "ingressgateway"
}
AuthorizationPolicy: "\(Name)-custom": {
metadata: Metadata & {name: "\(Name)-custom"}
spec: {
action: "CUSTOM"
provider: name: authproxy.provider
// bypass the external authorizer when the id token is already in the request.
// the RequestAuthentication rule will verify the token.
rules: [{when: [{key: "request.headers[\(authproxy.idTokenHeader)]", notValues: ["*"]}]}]
selector: matchLabels: istio: "ingressgateway"
}
}
}
}
}

View File

@@ -3,12 +3,23 @@ package holos
#Project: authProxyOrgDomain: "openinfrastructure.co"
_Projects: #Projects & {
// The platform project is required and where platform services reside. ArgoCD, Grafana, Prometheus, etc...
platform: {
resourceId: 257713952794870157
clusters: k1: _
clusters: k2: _
stages: dev: authProxyClientID: "260887327029658738@holos_platform"
stages: prod: authProxyClientID: "260887404288738416@holos_platform"
// Services hosted in the platform project
hosts: argocd: _
hosts: grafana: _
hosts: prometheus: _
}
holos: {
resourceId: 260446255245690199
clusters: {
k1: _
k2: _
}
clusters: k1: _
clusters: k2: _
stages: dev: authProxyClientID: "260505543108527218@holos"
stages: prod: authProxyClientID: "260506079325128023@holos"
environments: {

View File

@@ -4,7 +4,7 @@ package holos
projects: _
clusterName: _
extensionProviderMap: {
extensionProviderExtraMap: {
"cluster-trace": {
zipkin: {
maxTagLength: 56

View File

@@ -7,6 +7,9 @@ import "strings"
// #Projects is a map of all the projects in the platform.
#Projects: [Name=_]: #Project & {name: Name}
// The platform project is required and where platform services reside. ArgoCD, Grafana, Prometheus, etc...
#Projects: platform: _
#Project: {
name: string
// resourceId is the zitadel project Resource ID

View File

@@ -112,7 +112,7 @@ _apiVersion: "holos.run/v1alpha1"
_name: string
metadata: {
name: _name
namespace: #TargetNamespace
namespace: string | *#TargetNamespace
}
spec: {
refreshInterval: string | *"1h"
@@ -225,6 +225,25 @@ _apiVersion: "holos.run/v1alpha1"
services: [ID=_]: {
name: string & ID
}
// authproxy configures the auth proxy attached to the default ingress gateway in the istio-ingress namespace.
authproxy: #IngressAuthProxySpec
}
#IngressAuthProxySpec: {
// projectID is the zitadel project resource id.
projectID: number
// clientID is the zitadel application client id.
clientID: string
// orgDomain is the zitadel organization domain for logins.
orgDomain: string | *#Platform.org.domain
// issuer is the oidc identity provider issuer url
issuer: string | *"https://login.\(#Platform.org.domain)"
// path is the oauth2-proxy --proxy-prefix value. The default callback url is the Host: value with a path of /holos/oidc/callback
proxyPrefix: string | *"/holos/oidc"
// provider is the istio extension provider name in the mesh config.
provider: "ingressauth"
// idTokenHeader represents the header where the id token is placed
idTokenHeader: "x-oidc-id-token"
}
// ManagedNamespace is a namespace to manage across all clusters in the holos platform.

View File

@@ -1 +1 @@
5
6