Compare commits

...

9 Commits

Author SHA1 Message Date
Jeff McCune
7e93fe4535 (#86) ArgoCD
Using the Helm chart so we can inject the istio sidecar with a kustomize
patch and tweak the configs for OIDC integration.

Login works, istio sidecar is injected.  ArgoCD can only be configured
with one domain unfortunately, it's not accessible at argocd.ois.run,
only argocd.k2.ois.run (or whatever cluster it's installed into).

Ideally it would use the Host header but it does not.

RBAC is not implemented but the User Info endpoint does have group
membership so this shouldn't be a problem to implement.
2024-04-02 15:33:47 -07:00
Jeff McCune
2e98df3572 (#86) ArgoCD in prod-platform project namespace
Deploys using the official release yaml.
2024-04-02 13:34:03 -07:00
Jeff McCune
3b561de413 (#93) Custom AuthPolicy rules for vault
This patch defines a #AuthPolicyRules struct which excludes hosts from
the blanket auth policy and includes them in specialized auth policies.
The purpose is to handle special cases like vault requests which have an
`X-Vault-Token` and `X-Vault-Request` header.

Vault does not use jwts so we cannot verify them in the mesh, have to
pass them along to the backend.

Closes: #93
2024-04-02 12:54:31 -07:00
Jeff McCune
0d0dae8742 (#89) Disable project auth proxies by default
Focus on the ingress gateway auth proxy for now and see how far it gets
us.
2024-04-01 21:48:08 -07:00
Jeff McCune
61b4b5bd17 (#89) Refactor auth proxy callbacks
The ingress gateway auth proxy callback conflicts with the project stage
auth proxy callback for the same backend Host: header value.

This patch disambiguates them by the namespace the auth proxy resides
in.
2024-04-01 21:37:52 -07:00
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
17 changed files with 12473 additions and 49 deletions

View File

@@ -0,0 +1,37 @@
package holos
import ap "security.istio.io/authorizationpolicy/v1"
// #AuthPolicyRules represents AuthorizationPolicy rules for hosts that need specialized treatment. Entries in this struct are exclused from the blank ingressauth AuthorizationPolicy governing the ingressgateway and included in a spcialized policy
#AuthPolicyRules: {
// AuthProxySpec represents the identity provider configuration
AuthProxySpec: #AuthProxySpec & #Platform.authproxy
// Hosts are hosts that need specialized treatment
hosts: {
[Name=_]: {
// name is the fully qualifed hostname, a Host: header value.
name: Name
// slug is the resource name prefix
slug: string
// Refer to https://istio.io/latest/docs/reference/config/security/authorization-policy/#Rule
spec: ap.#AuthorizationPolicySpec & {
action: "CUSTOM"
provider: name: AuthProxySpec.provider
selector: matchLabels: istio: "ingressgateway"
}
}
}
objects: #APIObjects & {
for Host in hosts {
apiObjects: {
AuthorizationPolicy: "\(Host.slug)-custom": {
metadata: namespace: "istio-ingress"
metadata: name: "\(Host.slug)-custom"
spec: Host.spec
}
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,189 @@
// Code generated by timoni. DO NOT EDIT.
//timoni:generate timoni vendor crd -f /home/jeff/workspace/holos-run/holos-infra/deploy/clusters/k2/components/prod-platform-argocd/prod-platform-argocd.gen.yaml
package v1alpha1
import "strings"
// AppProject provides a logical grouping of applications,
// providing controls for: * where the apps may deploy to
// (cluster whitelist) * what may be deployed (repository
// whitelist, resource whitelist/blacklist) * who can access
// these applications (roles, OIDC group claims bindings) * and
// what they can do (RBAC policies) * automation access to these
// roles (JWT tokens)
#AppProject: {
// APIVersion defines the versioned schema of this representation
// of an object. Servers should convert recognized schemas to the
// latest internal value, and may reject unrecognized values.
// More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
apiVersion: "argoproj.io/v1alpha1"
// Kind is a string value representing the REST resource this
// object represents. Servers may infer this from the endpoint
// the client submits requests to. Cannot be updated. In
// CamelCase. More info:
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
kind: "AppProject"
metadata: {
name!: strings.MaxRunes(253) & strings.MinRunes(1) & {
string
}
namespace!: strings.MaxRunes(63) & strings.MinRunes(1) & {
string
}
labels?: {
[string]: string
}
annotations?: {
[string]: string
}
}
// AppProjectSpec is the specification of an AppProject
spec!: #AppProjectSpec
}
// AppProjectSpec is the specification of an AppProject
#AppProjectSpec: {
// ClusterResourceBlacklist contains list of blacklisted cluster
// level resources
clusterResourceBlacklist?: [...{
group: string
kind: string
}]
// ClusterResourceWhitelist contains list of whitelisted cluster
// level resources
clusterResourceWhitelist?: [...{
group: string
kind: string
}]
// Description contains optional project description
description?: string
// Destinations contains list of destinations available for
// deployment
destinations?: [...{
// Name is an alternate way of specifying the target cluster by
// its symbolic name. This must be set if Server is not set.
name?: string
// Namespace specifies the target namespace for the application's
// resources. The namespace will only be set for namespace-scoped
// resources that have not set a value for .metadata.namespace
namespace?: string
// Server specifies the URL of the target cluster's Kubernetes
// control plane API. This must be set if Name is not set.
server?: string
}]
// NamespaceResourceBlacklist contains list of blacklisted
// namespace level resources
namespaceResourceBlacklist?: [...{
group: string
kind: string
}]
// NamespaceResourceWhitelist contains list of whitelisted
// namespace level resources
namespaceResourceWhitelist?: [...{
group: string
kind: string
}]
// OrphanedResources specifies if controller should monitor
// orphaned resources of apps in this project
orphanedResources?: {
// Ignore contains a list of resources that are to be excluded
// from orphaned resources monitoring
ignore?: [...{
group?: string
kind?: string
name?: string
}]
// Warn indicates if warning condition should be created for apps
// which have orphaned resources
warn?: bool
}
// PermitOnlyProjectScopedClusters determines whether destinations
// can only reference clusters which are project-scoped
permitOnlyProjectScopedClusters?: bool
// Roles are user defined RBAC roles associated with this project
roles?: [...{
// Description is a description of the role
description?: string
// Groups are a list of OIDC group claims bound to this role
groups?: [...string]
// JWTTokens are a list of generated JWT tokens bound to this role
jwtTokens?: [...{
exp?: int
iat: int
id?: string
}]
// Name is a name for this role
name: string
// Policies Stores a list of casbin formatted strings that define
// access policies for the role in the project
policies?: [...string]
}]
// SignatureKeys contains a list of PGP key IDs that commits in
// Git must be signed with in order to be allowed for sync
signatureKeys?: [...{
// The ID of the key in hexadecimal notation
keyID: string
}]
// SourceNamespaces defines the namespaces application resources
// are allowed to be created in
sourceNamespaces?: [...string]
// SourceRepos contains list of repository URLs which can be used
// for deployment
sourceRepos?: [...string]
// SyncWindows controls when syncs can be run for apps in this
// project
syncWindows?: [...{
// Applications contains a list of applications that the window
// will apply to
applications?: [...string]
// Clusters contains a list of clusters that the window will apply
// to
clusters?: [...string]
// Duration is the amount of time the sync window will be open
duration?: string
// Kind defines if the window allows or blocks syncs
kind?: string
// ManualSync enables manual syncs when they would otherwise be
// blocked
manualSync?: bool
// Namespaces contains a list of namespaces that the window will
// apply to
namespaces?: [...string]
// Schedule is the time the window will begin, specified in cron
// format
schedule?: string
// TimeZone of the sync that will be applied to the schedule
timeZone?: string
}]
}

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

@@ -0,0 +1,84 @@
package holos
import "encoding/yaml"
let ArgoCD = "argocd"
let Namespace = "prod-platform"
spec: components: HelmChartList: [
#HelmChart & {
_dependsOn: "prod-secrets-stores": _
namespace: Namespace
metadata: name: "\(namespace)-\(ArgoCD)"
chart: {
name: "argo-cd"
release: "argocd"
version: "6.7.8"
repository: {
name: "argocd"
url: "https://argoproj.github.io/argo-helm"
}
}
_values: #ArgoCDValues & {
kubeVersionOverride: "1.29.0"
global: domain: "argocd.\(#ClusterName).\(#Platform.org.domain)"
configs: params: "server.insecure": true
configs: cm: {
"admin.enabled": false
"oidc.config": yaml.Marshal(OIDCConfig)
}
}
// Holos overlay objects
apiObjectMap: OBJECTS.apiObjectMap
},
]
let OBJECTS = #APIObjects & {
apiObjects: {
// ExternalSecret: "deploy-key": _
VirtualService: (ArgoCD): {
metadata: name: ArgoCD
metadata: namespace: Namespace
spec: hosts: [
ArgoCD + ".\(#Platform.org.domain)",
ArgoCD + ".\(#ClusterName).\(#Platform.org.domain)",
]
spec: gateways: ["istio-ingress/\(Namespace)"]
spec: http: [{route: [{destination: {
host: "argocd-server.\(Namespace).svc.cluster.local"
port: number: 80
}}]}]
}
}
}
let IstioInject = [{op: "add", path: "/spec/template/metadata/labels/sidecar.istio.io~1inject", value: "true"}]
#Kustomize: _patches: {
mesh: {
target: {
group: "apps"
version: "v1"
kind: "Deployment"
name: "argocd-server"
}
patch: yaml.Marshal(IstioInject)
}
}
// Probably shouldn't use the authproxy struct and should instead define an identity provider struct.
let AuthProxySpec = #AuthProxySpec & #Platform.authproxy
let OIDCConfig = {
name: "Holos Platform"
issuer: AuthProxySpec.issuer
clientID: #Platform.argocd.clientID
requestedIDTokenClaims: groups: essential: true
requestedScopes: ["openid", "profile", "email", "groups", "urn:zitadel:iam:org:domain:primary:\(AuthProxySpec.orgDomain)"]
enablePKCEAuthentication: true
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,5 +1,9 @@
package holos
// Ingress Gateway default auth proxy
let Provider = _IngressAuthProxy.AuthProxySpec.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,323 @@
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"
AuthProxySpec: #AuthProxySpec & #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:\(AuthProxySpec.orgDomain)"
clientID: AuthProxySpec.clientID
clientSecretFile: "/dev/null"
code_challenge_method: "S256"
loginURLParameters: [{
default: ["force"]
name: "approval_prompt"
}]
oidcConfig: {
issuerURL: AuthProxySpec.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=" + AuthProxySpec.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: AuthProxySpec.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: ["\(AuthProxySpec.projectID)"]
forwardOriginalToken: true
fromHeaders: [{name: AuthProxySpec.idTokenHeader}]
issuer: AuthProxySpec.issuer
}]
spec: selector: matchLabels: istio: "ingressgateway"
}
AuthorizationPolicy: "\(Name)-custom": {
_description: "Route all requests through the auth proxy by default"
metadata: Metadata & {name: "\(Name)-custom"}
spec: {
action: "CUSTOM"
provider: name: AuthProxySpec.provider
rules: [
{
to: [{
operation: notHosts: [
// Never send requests for the login service through the authorizer, would block login.
AuthProxySpec.issuerHost,
// Exclude hosts with specialized rules from the catch-all.
for x in _AuthPolicyRules.hosts {x.name},
]
}]
when: [
{
// bypass the external authorizer when the id token is already in the request.
// the RequestAuthentication rule will verify the token.
key: "request.headers[\(AuthProxySpec.idTokenHeader)]"
notValues: ["*"]
},
]
},
]
selector: matchLabels: istio: "ingressgateway"
}
}
}
}
}
_AuthPolicyRules: #AuthPolicyRules & {
hosts: {
let Vault = "vault.core.ois.run"
(Vault): {
slug: "vault"
// Rules for when to route requests through the auth proxy
spec: rules: [
{
to: [{
operation: hosts: [Vault]
operation: paths: ["/ui", "/ui/*"]
}]
},
{
to: [{
operation: hosts: [Vault]
}]
when: [{
key: "request.headers[x-vault-request]"
notValues: ["true"]
}]
},
]
}
}
}

View File

@@ -2,15 +2,27 @@ package holos
#Project: authProxyOrgDomain: "openinfrastructure.co"
let ZitadelProjectID = 257713952794870157
_Projects: #Projects & {
// The platform project is required and where platform services reside. ArgoCD, Grafana, Prometheus, etc...
platform: {
resourceId: ZitadelProjectID
// platform level services typically run in the core cluster pair.
clusters: core1: _
clusters: core2: _
// for development, probably wouldn't run these services in the workload clusters.
clusters: k2: _
// Services hosted in the platform project
hosts: argocd: _
hosts: grafana: _
hosts: prometheus: _
}
holos: {
resourceId: 260446255245690199
clusters: {
k1: _
k2: _
}
stages: dev: authProxyClientID: "260505543108527218@holos"
stages: prod: authProxyClientID: "260506079325128023@holos"
resourceId: ZitadelProjectID
clusters: k1: _
clusters: k2: _
environments: {
prod: stage: "prod"
dev: stage: "dev"
@@ -21,13 +33,11 @@ _Projects: #Projects & {
}
iam: {
resourceId: 260582480954787159
resourceId: ZitadelProjectID
clusters: {
core1: _
core2: _
}
stages: dev: authProxyClientID: "260582521186616432@iam"
stages: prod: authProxyClientID: "260582633862399090@iam"
}
}

View File

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

View File

@@ -75,24 +75,32 @@ import "encoding/yaml"
}
// Manage auth-proxy in each stage
"\(stage.slug)-authproxy": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
apiObjects: (AUTHPROXY & {stage: Stage, project: Project, servers: GatewayServers[stage.name]}).apiObjects
}).apiObjectMap
if project.features.authproxy.enabled {
"\(stage.slug)-authproxy": #KubernetesObjects & {
apiObjectMap: (#APIObjects & {
apiObjects: (AUTHPROXY & {stage: Stage, project: Project, servers: GatewayServers[stage.name]}).apiObjects
}).apiObjectMap
}
for Env in project.environments if Env.stage == stage.name {
"\(Env.slug)-authpolicy": #KubernetesObjects & {
// Manage auth policy in each env
apiObjectMap: (#APIObjects & {
apiObjects: (AUTHPOLICY & {env: Env, project: Project, servers: GatewayServers[stage.name]}).apiObjects
}).apiObjectMap
}
}
}
// Manage httpbin in each environment
for Env in project.environments if Env.stage == stage.name {
"\(Env.slug)-httpbin": #KubernetesObjects & {
let Project = project
apiObjectMap: (#APIObjects & {
apiObjects: (HTTPBIN & {env: Env, project: Project}).apiObjects
}).apiObjectMap
// Manage auth policy in each env
apiObjectMap: (#APIObjects & {
apiObjects: (AUTHPOLICY & {env: Env, project: Project, servers: GatewayServers[stage.name]}).apiObjects
}).apiObjectMap
if project.features.httpbin.enabled {
for Env in project.environments if Env.stage == stage.name {
"\(Env.slug)-httpbin": #KubernetesObjects & {
let Project = project
apiObjectMap: (#APIObjects & {
apiObjects: (HTTPBIN & {env: Env, project: Project}).apiObjects
}).apiObjectMap
}
}
}
}
@@ -194,6 +202,14 @@ let AUTHPROXY = {
let Project = project
let Stage = stage
let AuthProxySpec = #AuthProxySpec & {
namespace: stage.namespace
projectID: project.resourceId
clientID: stage.authProxyClientID
orgDomain: project.authProxyOrgDomain
provider: stage.extAuthzProviderName
}
let Metadata = {
name: Name
namespace: stage.namespace
@@ -224,15 +240,15 @@ let AUTHPROXY = {
data: "config.yaml": yaml.Marshal(AuthProxyConfig)
let AuthProxyConfig = {
injectResponseHeaders: [{
name: "x-oidc-id-token"
name: AuthProxySpec.idTokenHeader
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:\(project.authProxyOrgDomain)"
clientID: stage.authProxyClientID
scope: "openid profile email groups offline_access urn:zitadel:iam:org:domain:primary:\(AuthProxySpec.orgDomain)"
clientID: AuthProxySpec.clientID
clientSecretFile: "/dev/null"
code_challenge_method: "S256"
loginURLParameters: [{
@@ -240,7 +256,7 @@ let AUTHPROXY = {
name: "approval_prompt"
}]
oidcConfig: {
issuerURL: project.authProxyIssuer
issuerURL: AuthProxySpec.issuer
audienceClaims: ["aud"]
emailClaim: "email"
groupsClaim: "groups"
@@ -285,7 +301,7 @@ let AUTHPROXY = {
}]
args: [
// callback url is proxy prefix + /callback
"--proxy-prefix=" + project.authProxyPrefix,
"--proxy-prefix=" + AuthProxySpec.proxyPrefix,
"--email-domain=*",
"--session-store-type=redis",
"--redis-connection-url=redis://\(RedisMetadata.name):6379",
@@ -345,7 +361,7 @@ let AUTHPROXY = {
spec: hosts: ["*"]
spec: gateways: ["istio-ingress/\(stage.slug)"]
spec: http: [{
match: [{uri: prefix: project.authProxyPrefix}]
match: [{uri: prefix: AuthProxySpec.proxyPrefix}]
route: [{
destination: host: Name
destination: port: number: 4180
@@ -447,6 +463,14 @@ let AUTHPOLICY = {
let stage = project.stages[env.stage]
let Env = env
let AuthProxySpec = #AuthProxySpec & {
namespace: stage.namespace
projectID: project.resourceId
clientID: stage.authProxyClientID
orgDomain: project.authProxyOrgDomain
provider: stage.extAuthzProviderName
}
let Metadata = {
name: string
namespace: env.namespace
@@ -469,16 +493,16 @@ let AUTHPOLICY = {
for host in Hosts {host.name},
for host in Hosts {host.name + ":*"},
]
let MatchLabels = {"security.holos.run/authproxy": stage.extAuthzProviderName}
let MatchLabels = {"security.holos.run/authproxy": AuthProxySpec.provider}
apiObjects: {
RequestAuthentication: (Name): #RequestAuthentication & {
metadata: Metadata & {name: Name}
spec: jwtRules: [{
audiences: [stage.authProxyClientID]
audiences: [AuthProxySpec.clientID]
forwardOriginalToken: true
fromHeaders: [{name: "x-oidc-id-token"}]
issuer: project.authProxyIssuer
fromHeaders: [{name: AuthProxySpec.idTokenHeader}]
issuer: AuthProxySpec.issuer
}]
spec: selector: matchLabels: MatchLabels
}
@@ -487,8 +511,19 @@ let AUTHPOLICY = {
spec: {
action: "CUSTOM"
// send the request to the auth proxy
provider: name: stage.extAuthzProviderName
rules: [{to: [{operation: hosts: HostList}]}]
provider: name: AuthProxySpec.provider
rules: [{
to: [{operation: hosts: HostList}]
when: [
{
key: "request.headers[\(AuthProxySpec.idTokenHeader)]"
notValues: ["*"]
},
{
key: "request.headers[host]"
notValues: [AuthProxySpec.issuerHost]
},
]}]
selector: matchLabels: MatchLabels
}
}

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
@@ -23,8 +26,6 @@ import "strings"
}
domain: string | *#Platform.org.domain
// authProxyPrefix is the path routed to the ext auth proxy.
authProxyPrefix: string | *"/holos/oidc"
// authProxyOrgDomain is the primary org domain for zitadel.
authProxyOrgDomain: string | *#Platform.org.domain
// authProxyIssuer is the issuer url
@@ -60,6 +61,8 @@ import "strings"
// features is YAGNI maybe?
features: [Name=string]: #Feature & {name: Name}
features: authproxy: _
features: httpbin: _
}
// #Cluster defines a cluster
@@ -124,7 +127,7 @@ import "strings"
#Feature: {
name: string
description: string
enabled: *true | false
enabled: true | *false
}
#ProjectTemplate: {

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,32 @@ _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: #AuthProxySpec & {
namespace: "istio-ingress"
provider: "ingressauth"
}
}
#AuthProxySpec: {
// projectID is the zitadel project resource id.
projectID: number
// clientID is the zitadel application client id.
clientID: string
// namespace is the namespace
namespace: string
// provider is the istio extension provider name in the mesh config.
provider: string
// orgDomain is the zitadel organization domain for logins.
orgDomain: string | *#Platform.org.domain
// issuerHost is the Host: header value of the oidc issuer
issuerHost: string | *"login.\(#Platform.org.domain)"
// issuer is the oidc identity provider issuer url
issuer: string | *"https://\(issuerHost)"
// 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/authproxy/\(namespace)"
// idTokenHeader represents the header where the id token is placed
idTokenHeader: string | *"x-oidc-id-token"
}
// ManagedNamespace is a namespace to manage across all clusters in the holos platform.

View File

@@ -1 +1 @@
61
62

View File

@@ -1 +1 @@
5
0