fix(auth): harden public infra routes
This commit is contained in:
@@ -201,6 +201,8 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: andrew-web
|
app: andrew-web
|
||||||
|
annotations:
|
||||||
|
flowercore.io/healthz-auth-policy: "allow-anonymous"
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: nginx
|
- name: nginx
|
||||||
@@ -225,12 +227,18 @@ spec:
|
|||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 3
|
initialDelaySeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
volumes:
|
volumes:
|
||||||
@@ -265,7 +273,7 @@ spec:
|
|||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
routes:
|
routes:
|
||||||
- match: Host(`bluejay.dev`) || Host(`www.bluejay.dev`)
|
- match: (Host(`bluejay.dev`) || Host(`www.bluejay.dev`)) && (Method(`GET`) || Method(`HEAD`))
|
||||||
kind: Rule
|
kind: Rule
|
||||||
services:
|
services:
|
||||||
- name: andrew-web
|
- name: andrew-web
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: dustin-web
|
app: dustin-web
|
||||||
|
annotations:
|
||||||
|
flowercore.io/healthz-auth-policy: "allow-anonymous"
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: nginx
|
- name: nginx
|
||||||
@@ -225,12 +227,18 @@ spec:
|
|||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 3
|
initialDelaySeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
volumes:
|
volumes:
|
||||||
@@ -265,7 +273,7 @@ spec:
|
|||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
routes:
|
routes:
|
||||||
- match: Host(`timeforta.co`) || Host(`www.timeforta.co`)
|
- match: (Host(`timeforta.co`) || Host(`www.timeforta.co`)) && (Method(`GET`) || Method(`HEAD`))
|
||||||
kind: Rule
|
kind: Rule
|
||||||
services:
|
services:
|
||||||
- name: dustin-web
|
- name: dustin-web
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: erik-web
|
app: erik-web
|
||||||
|
annotations:
|
||||||
|
flowercore.io/healthz-auth-policy: "allow-anonymous"
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: nginx
|
- name: nginx
|
||||||
@@ -225,12 +227,18 @@ spec:
|
|||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 3
|
initialDelaySeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
volumes:
|
volumes:
|
||||||
@@ -265,7 +273,7 @@ spec:
|
|||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
routes:
|
routes:
|
||||||
- match: Host(`erckak.dev`) || Host(`www.erckak.dev`)
|
- match: (Host(`erckak.dev`) || Host(`www.erckak.dev`)) && (Method(`GET`) || Method(`HEAD`))
|
||||||
kind: Rule
|
kind: Rule
|
||||||
services:
|
services:
|
||||||
- name: erik-web
|
- name: erik-web
|
||||||
|
|||||||
@@ -203,6 +203,8 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: fc-landing
|
app: fc-landing
|
||||||
|
annotations:
|
||||||
|
flowercore.io/healthz-auth-policy: "allow-anonymous"
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: nginx
|
- name: nginx
|
||||||
@@ -227,12 +229,18 @@ spec:
|
|||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 3
|
initialDelaySeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
volumes:
|
volumes:
|
||||||
@@ -298,7 +306,7 @@ spec:
|
|||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
routes:
|
routes:
|
||||||
- match: Host(`flowercore.io`) || Host(`www.flowercore.io`)
|
- match: (Host(`flowercore.io`) || Host(`www.flowercore.io`)) && (Method(`GET`) || Method(`HEAD`))
|
||||||
kind: Rule
|
kind: Rule
|
||||||
services:
|
services:
|
||||||
- name: fc-landing
|
- name: fc-landing
|
||||||
@@ -316,7 +324,7 @@ spec:
|
|||||||
entryPoints:
|
entryPoints:
|
||||||
- web
|
- web
|
||||||
routes:
|
routes:
|
||||||
- match: Host(`flowercore.io`) || Host(`www.flowercore.io`)
|
- match: (Host(`flowercore.io`) || Host(`www.flowercore.io`)) && (Method(`GET`) || Method(`HEAD`))
|
||||||
kind: Rule
|
kind: Rule
|
||||||
services:
|
services:
|
||||||
- name: fc-landing
|
- name: fc-landing
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: fit-web
|
app: fit-web
|
||||||
|
annotations:
|
||||||
|
flowercore.io/healthz-auth-policy: "allow-anonymous"
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: nginx
|
- name: nginx
|
||||||
@@ -225,12 +227,18 @@ spec:
|
|||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 3
|
initialDelaySeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
volumes:
|
volumes:
|
||||||
@@ -265,7 +273,7 @@ spec:
|
|||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
routes:
|
routes:
|
||||||
- match: Host(`flowerinsider.xyz`) || Host(`www.flowerinsider.xyz`)
|
- match: (Host(`flowerinsider.xyz`) || Host(`www.flowerinsider.xyz`)) && (Method(`GET`) || Method(`HEAD`))
|
||||||
kind: Rule
|
kind: Rule
|
||||||
services:
|
services:
|
||||||
- name: fit-web
|
- name: fit-web
|
||||||
|
|||||||
@@ -257,6 +257,8 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: flowercore-web
|
app: flowercore-web
|
||||||
|
annotations:
|
||||||
|
flowercore.io/healthz-auth-policy: "allow-anonymous"
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: nginx
|
- name: nginx
|
||||||
@@ -281,12 +283,18 @@ spec:
|
|||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 3
|
initialDelaySeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ spec:
|
|||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
routes:
|
routes:
|
||||||
- match: Host(`gitea.flowercore.io`)
|
- match: Host(`gitea.flowercore.io`) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`))
|
||||||
kind: Rule
|
kind: Rule
|
||||||
services:
|
services:
|
||||||
- name: gitea-http
|
- name: gitea-http
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ spec:
|
|||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
routes:
|
routes:
|
||||||
- match: Host(`webmail.flowercore.io`)
|
- match: Host(`webmail.flowercore.io`) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`))
|
||||||
kind: Rule
|
kind: Rule
|
||||||
services:
|
services:
|
||||||
- name: mail-webmail
|
- name: mail-webmail
|
||||||
|
|||||||
@@ -479,7 +479,7 @@ spec:
|
|||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
routes:
|
routes:
|
||||||
- match: Host(`element.flowercore.io`)
|
- match: Host(`element.flowercore.io`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
kind: Rule
|
kind: Rule
|
||||||
services:
|
services:
|
||||||
- name: element-web
|
- name: element-web
|
||||||
@@ -497,7 +497,7 @@ spec:
|
|||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
routes:
|
routes:
|
||||||
- match: Host(`matrix.flowercore.io`)
|
- match: Host(`matrix.flowercore.io`) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`))
|
||||||
kind: Rule
|
kind: Rule
|
||||||
services:
|
services:
|
||||||
- name: synapse
|
- name: synapse
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ spec:
|
|||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: pki-web
|
app: pki-web
|
||||||
|
annotations:
|
||||||
|
flowercore.io/healthz-auth-policy: "allow-anonymous"
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: nginx
|
- name: nginx
|
||||||
@@ -158,12 +160,18 @@ spec:
|
|||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz
|
path: /healthz
|
||||||
port: 80
|
port: 80
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 3
|
initialDelaySeconds: 3
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
volumes:
|
volumes:
|
||||||
@@ -201,6 +209,7 @@ spec:
|
|||||||
dnsNames:
|
dnsNames:
|
||||||
- pki.iamworkin.lan
|
- pki.iamworkin.lan
|
||||||
---
|
---
|
||||||
|
# Internal-only route: if a public twin is ever operator-approved, gate it with Host(`<public-host>`) && (Method(`GET`) || Method(`HEAD`)).
|
||||||
# Traefik IngressRoute
|
# Traefik IngressRoute
|
||||||
apiVersion: traefik.io/v1alpha1
|
apiVersion: traefik.io/v1alpha1
|
||||||
kind: IngressRoute
|
kind: IngressRoute
|
||||||
|
|||||||
@@ -207,12 +207,18 @@ spec:
|
|||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health
|
||||||
port: 5100
|
port: 5100
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health
|
||||||
port: 5100
|
port: 5100
|
||||||
|
httpHeaders:
|
||||||
|
- name: X-Forwarded-Proto
|
||||||
|
value: https
|
||||||
initialDelaySeconds: 10
|
initialDelaySeconds: 10
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
volumes:
|
volumes:
|
||||||
@@ -256,12 +262,12 @@ spec:
|
|||||||
- websecure
|
- websecure
|
||||||
routes:
|
routes:
|
||||||
- kind: Rule
|
- kind: Rule
|
||||||
match: Host(`telephony.flowercore.io`)
|
match: Host(`telephony.flowercore.io`) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`))
|
||||||
services:
|
services:
|
||||||
- name: telephony-web
|
- name: telephony-web
|
||||||
port: 5100
|
port: 5100
|
||||||
- kind: Rule
|
- kind: Rule
|
||||||
match: Host(`telephony.iamwork.in`)
|
match: Host(`telephony.iamwork.in`) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`))
|
||||||
services:
|
services:
|
||||||
- name: telephony-web
|
- name: telephony-web
|
||||||
port: 5100
|
port: 5100
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ spec:
|
|||||||
basicAuth:
|
basicAuth:
|
||||||
secret: traefik-dashboard-auth
|
secret: traefik-dashboard-auth
|
||||||
---
|
---
|
||||||
|
# Internal-only route: if a public twin is ever operator-approved, gate it with Host(`<public-host>`) && (Method(`GET`) || Method(`HEAD`)).
|
||||||
# Dashboard IngressRoute
|
# Dashboard IngressRoute
|
||||||
apiVersion: traefik.io/v1alpha1
|
apiVersion: traefik.io/v1alpha1
|
||||||
kind: IngressRoute
|
kind: IngressRoute
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ spec:
|
|||||||
- websecure
|
- websecure
|
||||||
routes:
|
routes:
|
||||||
- kind: Rule
|
- kind: Rule
|
||||||
match: Host(`voice.bluejay.dev`)
|
match: Host(`voice.bluejay.dev`) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`))
|
||||||
services:
|
services:
|
||||||
- name: voice-bridge
|
- name: voice-bridge
|
||||||
port: 8766
|
port: 8766
|
||||||
@@ -84,7 +84,7 @@ spec:
|
|||||||
- websecure
|
- websecure
|
||||||
routes:
|
routes:
|
||||||
- kind: Rule
|
- kind: Rule
|
||||||
match: Host(`voice-ws.bluejay.dev`)
|
match: Host(`voice-ws.bluejay.dev`) && (Method(`GET`) || Method(`HEAD`))
|
||||||
services:
|
services:
|
||||||
- name: voice-bridge
|
- name: voice-bridge
|
||||||
port: 8765
|
port: 8765
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ spec:
|
|||||||
dnsNames:
|
dnsNames:
|
||||||
- zabbix.iamworkin.lan
|
- zabbix.iamworkin.lan
|
||||||
---
|
---
|
||||||
|
# Internal-only route: if a public twin is ever operator-approved, gate it with Host(`<public-host>`) && (Method(`GET`) || Method(`HEAD`)).
|
||||||
# Traefik IngressRoute
|
# Traefik IngressRoute
|
||||||
apiVersion: traefik.io/v1alpha1
|
apiVersion: traefik.io/v1alpha1
|
||||||
kind: IngressRoute
|
kind: IngressRoute
|
||||||
|
|||||||
@@ -13,8 +13,20 @@ public sealed class FleetManifestLintTests
|
|||||||
|
|
||||||
private static readonly HashSet<string> PublicReadOnlyHosts = new(StringComparer.Ordinal)
|
private static readonly HashSet<string> PublicReadOnlyHosts = new(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
|
"bluejay.dev",
|
||||||
"brochure.flowercore.io",
|
"brochure.flowercore.io",
|
||||||
"dist.flowercore.io",
|
"dist.flowercore.io",
|
||||||
|
"element.flowercore.io",
|
||||||
|
"erckak.dev",
|
||||||
|
"flowercore.io",
|
||||||
|
"flowerinsider.xyz",
|
||||||
|
"timeforta.co",
|
||||||
|
"voice-ws.bluejay.dev",
|
||||||
|
"www.bluejay.dev",
|
||||||
|
"www.erckak.dev",
|
||||||
|
"www.flowercore.io",
|
||||||
|
"www.flowerinsider.xyz",
|
||||||
|
"www.timeforta.co",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Public hosts that allow a tightly bounded write surface in addition to
|
// Public hosts that allow a tightly bounded write surface in addition to
|
||||||
@@ -28,10 +40,40 @@ public sealed class FleetManifestLintTests
|
|||||||
// same bounded read-write allowlist as the LAN pair.
|
// same bounded read-write allowlist as the LAN pair.
|
||||||
private static readonly HashSet<string> PublicReadWriteAllowlistHosts = new(StringComparer.Ordinal)
|
private static readonly HashSet<string> PublicReadWriteAllowlistHosts = new(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
|
"chat.flowercore.io",
|
||||||
|
"gitea.flowercore.io",
|
||||||
|
"matrix.flowercore.io",
|
||||||
|
"telephony.flowercore.io",
|
||||||
|
"telephony.iamwork.in",
|
||||||
"updatecenter.iamworkin.lan",
|
"updatecenter.iamworkin.lan",
|
||||||
"updates.iamworkin.lan",
|
"updates.iamworkin.lan",
|
||||||
"update.flowercore.io",
|
"update.flowercore.io",
|
||||||
"updates.flowercore.io",
|
"updates.flowercore.io",
|
||||||
|
"voice.bluejay.dev",
|
||||||
|
"webmail.flowercore.io",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> InfraHealthzProbeDeployments = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["andrew"] = "andrew-web",
|
||||||
|
["dustin"] = "dustin-web",
|
||||||
|
["erik"] = "erik-web",
|
||||||
|
["fc-landing"] = "fc-landing",
|
||||||
|
["fit"] = "fit-web",
|
||||||
|
["flowercore"] = "flowercore-web",
|
||||||
|
["pki-web"] = "pki-web",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> InfraForwardedProtoProbeDeployments = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["andrew"] = "andrew-web",
|
||||||
|
["dustin"] = "dustin-web",
|
||||||
|
["erik"] = "erik-web",
|
||||||
|
["fc-landing"] = "fc-landing",
|
||||||
|
["fit"] = "fit-web",
|
||||||
|
["flowercore"] = "flowercore-web",
|
||||||
|
["pki-web"] = "pki-web",
|
||||||
|
["telephony"] = "telephony-web",
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly HashSet<string> ApiKeyProtectedDeployments = new(StringComparer.Ordinal)
|
private static readonly HashSet<string> ApiKeyProtectedDeployments = new(StringComparer.Ordinal)
|
||||||
@@ -131,8 +173,13 @@ public sealed class FleetManifestLintTests
|
|||||||
}))
|
}))
|
||||||
.Where(entry => PublicReadOnlyHosts.Any(host => entry.Match.Contains($"Host(`{host}`)", StringComparison.Ordinal)))
|
.Where(entry => PublicReadOnlyHosts.Any(host => entry.Match.Contains($"Host(`{host}`)", StringComparison.Ordinal)))
|
||||||
.Where(entry => !entry.Match.Contains("Method(`GET`)", StringComparison.Ordinal)
|
.Where(entry => !entry.Match.Contains("Method(`GET`)", StringComparison.Ordinal)
|
||||||
|| !entry.Match.Contains("Method(`HEAD`)", StringComparison.Ordinal))
|
|| !entry.Match.Contains("Method(`HEAD`)", StringComparison.Ordinal)
|
||||||
.Select(entry => $"{entry.Document.Descriptor} is missing an explicit GET/HEAD method allowlist.")
|
|| entry.Match.Contains("Method(`POST`)", StringComparison.Ordinal)
|
||||||
|
|| entry.Match.Contains("Method(`PUT`)", StringComparison.Ordinal)
|
||||||
|
|| entry.Match.Contains("Method(`PATCH`)", StringComparison.Ordinal)
|
||||||
|
|| entry.Match.Contains("Method(`DELETE`)", StringComparison.Ordinal)
|
||||||
|
|| entry.Match.Contains("Method(`OPTIONS`)", StringComparison.Ordinal))
|
||||||
|
.Select(entry => $"{entry.Document.Descriptor} must explicitly allow GET/HEAD only on a public read-only host.")
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
violations.Should().BeEmpty();
|
violations.Should().BeEmpty();
|
||||||
@@ -473,6 +520,49 @@ public sealed class FleetManifestLintTests
|
|||||||
violations.Should().BeEmpty();
|
violations.Should().BeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuthSafeInfraHealthzProbes_MustDeclareAnonymousHealthzContract()
|
||||||
|
{
|
||||||
|
var violations = InfraHealthzProbeDeployments.SelectMany(expected =>
|
||||||
|
{
|
||||||
|
var deployment = AppDocuments(expected.Key)
|
||||||
|
.Single(document => document.Kind == "Deployment" && document.Name == expected.Value);
|
||||||
|
var hasHealthzProbe = deployment.MainContainerMappings()
|
||||||
|
.Any(container => ProbeHttpGetPath(container, "readinessProbe") == "/healthz"
|
||||||
|
|| ProbeHttpGetPath(container, "startupProbe") == "/healthz"
|
||||||
|
|| ProbeHttpGetPath(container, "livenessProbe") == "/healthz");
|
||||||
|
|
||||||
|
return hasHealthzProbe
|
||||||
|
&& !string.Equals(PodAnnotation(deployment, "flowercore.io/healthz-auth-policy"), "allow-anonymous", StringComparison.Ordinal)
|
||||||
|
? new[] { $"{deployment.Descriptor} probes /healthz but lacks flowercore.io/healthz-auth-policy: allow-anonymous." }
|
||||||
|
: Array.Empty<string>();
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
violations.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuthSafeInfraHttpProbes_MustSendForwardedProtoHttpsHeader()
|
||||||
|
{
|
||||||
|
var violations = InfraForwardedProtoProbeDeployments.SelectMany(expected =>
|
||||||
|
{
|
||||||
|
var deployment = AppDocuments(expected.Key)
|
||||||
|
.Single(document => document.Kind == "Deployment" && document.Name == expected.Value);
|
||||||
|
|
||||||
|
return deployment.MainContainerMappings()
|
||||||
|
.SelectMany(container => new[] { "startupProbe", "readinessProbe", "livenessProbe" }
|
||||||
|
.Where(probeKey => ProbeHttpGetPath(container, probeKey) is "/healthz" or "/health")
|
||||||
|
.Where(probeKey => !string.Equals(ProbeHttpGetHeaderValue(container, probeKey, "X-Forwarded-Proto"), "https", StringComparison.Ordinal))
|
||||||
|
.Select(probeKey =>
|
||||||
|
{
|
||||||
|
var containerName = ManifestNodeExtensions.Scalar(container, "name") ?? "<unnamed>";
|
||||||
|
return $"{deployment.Descriptor} container '{containerName}' {probeKey} is missing X-Forwarded-Proto=https.";
|
||||||
|
}));
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
violations.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Knowledge_OidcEnforcement_MustKeepHealthzAnonymousContractVisibleInManifest()
|
public void Knowledge_OidcEnforcement_MustKeepHealthzAnonymousContractVisibleInManifest()
|
||||||
{
|
{
|
||||||
@@ -1015,6 +1105,20 @@ public sealed class FleetManifestLintTests
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? ProbeHttpGetHeaderValue(YamlMappingNode container, string probeKey, string name)
|
||||||
|
{
|
||||||
|
if (!ManifestNodeExtensions.TryGetMapping(container, probeKey, out var probe)
|
||||||
|
|| !ManifestNodeExtensions.TryGetMapping(probe, "httpGet", out var httpGet))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ManifestNodeExtensions.MappingSequence(httpGet, "httpHeaders")
|
||||||
|
.Where(header => string.Equals(ManifestNodeExtensions.Scalar(header, "name"), name, StringComparison.Ordinal))
|
||||||
|
.Select(header => ManifestNodeExtensions.Scalar(header, "value"))
|
||||||
|
.SingleOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<ManifestDocument> FcDeviceManagementDocuments()
|
private static IReadOnlyList<ManifestDocument> FcDeviceManagementDocuments()
|
||||||
{
|
{
|
||||||
return Inventory.Documents
|
return Inventory.Documents
|
||||||
|
|||||||
Reference in New Issue
Block a user