-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Goal
Allow users to password-protect their static sites via the StaticSite CRD.
Decisions
- MVP: BasicAuth
- Future phases: ForwardAuth reference, IP AllowList
- Secret location: User namespace (operator copies to system namespace)
Quick Comparison
| Method | Complexity | Dependencies | Use Case |
|---|---|---|---|
| BasicAuth | Low | None | Simple password protection |
| ForwardAuth | Low | Pre-existing middleware | SSO via oauth2-proxy |
| IP AllowList | Low | None | Office/VPN access |
CRD Design (All Methods)
apiVersion: pages.kup6s.com/v1alpha1
kind: StaticSite
spec:
repo: https://github.com/user/site
auth: # Only ONE method can be active
# Option 1: BasicAuth
basicAuth:
secretRef:
name: my-site-auth # htpasswd secret in user namespace
realm: "Restricted" # Optional, shown in browser dialog
removeHeader: true # Optional, strip Authorization header
# Option 2: ForwardAuth (reference existing middleware)
forwardAuth:
middlewareRef: oauth2-proxy # "name" or "namespace/name"
# Option 3: IP AllowList
ipAllowList:
sourceRange:
- "192.168.1.0/24"
- "10.0.0.1"
ipStrategy: # Optional
depth: 1 # X-Forwarded-For positionMiddleware Chain Order
IngressRoute -> [auth? -> ipAllowList? -> stripPrefix? -> addPrefix] -> nginx
Authentication middlewares run FIRST, before path manipulation.
Phase 1: BasicAuth (MVP)
Challenge: Cross-Namespace Secret Reference
Traefik middlewares can only reference secrets in their own namespace.
Solution: Operator copies user's secret to system namespace with name {namespace}--{site}-basicauth.
Go Types (pkg/apis/v1alpha1/types.go)
// Add to StaticSiteSpec
Auth *AuthConfig `json:"auth,omitempty"`
type AuthConfig struct {
BasicAuth *BasicAuthConfig `json:"basicAuth,omitempty"`
ForwardAuth *ForwardAuthConfig `json:"forwardAuth,omitempty"`
IPAllowList *IPAllowListConfig `json:"ipAllowList,omitempty"`
}
type BasicAuthConfig struct {
SecretRef LocalSecretReference `json:"secretRef"`
Realm string `json:"realm,omitempty"` // default: "Restricted"
RemoveHeader *bool `json:"removeHeader,omitempty"` // default: true
}
type LocalSecretReference struct {
Name string `json:"name"`
}CRD Schema (Helm chart CRD YAML)
auth:
type: object
properties:
basicAuth:
type: object
required: [secretRef]
properties:
secretRef:
type: object
required: [name]
properties:
name:
type: string
realm:
type: string
default: "Restricted"
removeHeader:
type: boolean
default: trueController Logic (pkg/controller/staticsite.go)
Add new functions:
reconcileAuth()- dispatch to appropriate auth methodreconcileBasicAuth()- copy secret, create middlewarecleanupAuthResources()- remove auth resources on deletion/change
Update reconcileIngressRoute() to insert auth middleware first in chain.
RBAC Changes
ClusterRole (clusterrole-operator.yaml):
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "watch", "list"] # Read user secretsRole (role-operator.yaml):
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "create", "update", "delete"] # Manage copied secretsSecret Format
Traefik BasicAuth requires htpasswd format:
# Create htpasswd file
htpasswd -nB myuser > users
# Create secret (Opaque with 'users' key)
kubectl create secret generic my-auth --from-file=usersThe secret must have a users key with htpasswd-formatted content.
Phase 2: ForwardAuth Reference
Go Types
type ForwardAuthConfig struct {
MiddlewareRef string `json:"middlewareRef"` // "name" or "namespace/name"
}Controller Logic
- Parse
middlewareRefinto namespace/name - Validate middleware exists (optional)
- Add reference to IngressRoute middlewares array
- No middleware creation - just reference
Cross-Namespace Note
Referencing middlewares in other namespaces requires Traefik allowCrossNamespace=true.
Phase 3: IP AllowList
Go Types
type IPAllowListConfig struct {
SourceRange []string `json:"sourceRange"`
IPStrategy *IPStrategy `json:"ipStrategy,omitempty"`
}
type IPStrategy struct {
Depth int `json:"depth,omitempty"`
ExcludedIPs []string `json:"excludedIPs,omitempty"`
}Controller Logic
Creates Traefik ipAllowList middleware in system namespace.
Files to Modify
| File | Changes |
|---|---|
pkg/apis/v1alpha1/types.go |
Add AuthConfig structs |
pkg/controller/staticsite.go |
Add reconcileAuth, update middleware chain |
charts/kup6s-pages/crds/staticsites.pages.kup6s.com.yaml |
Add auth schema |
charts/kup6s-pages/templates/clusterrole-operator.yaml |
Add secret read |
charts/kup6s-pages/templates/role-operator.yaml |
Add secret CRUD |
Status Tracking
Update ManagedResources in types.go:
type ManagedResources struct {
// ... existing ...
BasicAuthMiddleware string `json:"basicAuthMiddleware,omitempty"`
BasicAuthSecret string `json:"basicAuthSecret,omitempty"`
IPAllowMiddleware string `json:"ipAllowMiddleware,omitempty"`
}Mutual Exclusion
Only ONE auth method can be active. Add validation in controller:
func validateAuthConfig(auth *AuthConfig) error {
count := 0
if auth.BasicAuth != nil { count++ }
if auth.ForwardAuth != nil { count++ }
if auth.IPAllowList != nil { count++ }
if count > 1 {
return fmt.Errorf("only one auth method allowed")
}
return nil
}Test Scenarios
BasicAuth
- Valid secret -> middleware created, auth works
- Secret not found -> Error phase
- Secret updates -> copied secret updates
- Auth removed -> middleware + copied secret deleted
- Site deleted -> all auth resources cleaned up
ForwardAuth
- Valid reference -> added to IngressRoute
- Cross-namespace reference -> works with allowCrossNamespace
- Invalid format -> CRD validation error
IP AllowList
- Single IP -> access allowed
- CIDR range -> range allowed
- Invalid CIDR -> validation error
Verification
- Create StaticSite with
auth.basicAuth.secretRef - Verify middleware:
kubectl get middleware -n kup6s-pages - Verify copied secret:
kubectl get secret -n kup6s-pages - Access site -> browser prompts for credentials
- Remove auth -> resources cleaned up