Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/on-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,28 @@ permissions:

jobs:
validate:
uses: unbounded-tech/workflows-crossplane/.github/workflows/validate.yaml@v2.20.0
uses: hops-ops/workflows-crossplane/.github/workflows/validate.yaml@v3.0.0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/on-pr.yaml

Repository: hops-ops/auth-stack

Length of output: 1729


Pin reusable workflow references to immutable commit SHAs.

Using @v3.0.0 leaves these jobs vulnerable to tag retargeting. Pin to full commit SHAs and optionally annotate the intended tag in a comment.

🔒 Suggested hardening
-    uses: hops-ops/workflows-crossplane/.github/workflows/validate.yaml@v3.0.0
+    uses: hops-ops/workflows-crossplane/.github/workflows/validate.yaml@<sha-for-v3.0.0> # v3.0.0
...
-    uses: hops-ops/workflows-crossplane/.github/workflows/test.yaml@v3.0.0
+    uses: hops-ops/workflows-crossplane/.github/workflows/test.yaml@<sha-for-v3.0.0> # v3.0.0
...
-    uses: hops-ops/workflows-crossplane/.github/workflows/e2e.yaml@v3.0.0
+    uses: hops-ops/workflows-crossplane/.github/workflows/e2e.yaml@<sha-for-v3.0.0> # v3.0.0
...
-    uses: hops-ops/workflows-crossplane/.github/workflows/publish.yaml@v3.0.0
+    uses: hops-ops/workflows-crossplane/.github/workflows/publish.yaml@<sha-for-v3.0.0> # v3.0.0

Also applies to: 41-41, 44-44, 50-50

🧰 Tools
🪛 zizmor (1.25.2)

[error] 30-30: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/on-pr.yaml at line 30, Replace the floating tag references
to the reusable workflows with immutable commit SHAs: for each uses:
hops-ops/workflows-crossplane/.github/workflows/validate.yaml@v3.0.0 (and the
other occurrences at the same file), update the `@v3.0.0` suffix to the specific
commit SHA of the target repository; optionally add a trailing comment
indicating the intended tag (e.g., // intended-tag: v3.0.0) for clarity. Locate
the three uses lines in .github/workflows/on-pr.yaml and swap the tag for the
full commit SHA so the workflow points to an immutable ref.

with:
examples: |
[
{ "example": "examples/authstacks/minimal.yaml" },
{ "example": "examples/authstacks/standard.yaml" },
{ "example": "examples/authstacks/local-colima.yaml" }
{ "example": "examples/authstacks/minimal.yaml", "api_path": "apis/authstacks" },
{ "example": "examples/authstacks/standard.yaml", "api_path": "apis/authstacks" },
{ "example": "examples/authstacks/local-colima.yaml", "api_path": "apis/authstacks" }
]
api_path: apis/authstacks
error_on_missing_schemas: true

test:
uses: unbounded-tech/workflows-crossplane/.github/workflows/test.yaml@v2.20.0
uses: hops-ops/workflows-crossplane/.github/workflows/test.yaml@v3.0.0

e2e:
uses: unbounded-tech/workflows-crossplane/.github/workflows/e2e.yaml@v2.20.0
uses: hops-ops/workflows-crossplane/.github/workflows/e2e.yaml@v3.0.0

publish:
needs:
- validate
- test
uses: unbounded-tech/workflows-crossplane/.github/workflows/publish.yaml@v2.20.0
secrets: inherit
uses: hops-ops/workflows-crossplane/.github/workflows/publish.yaml@v3.0.0
secrets:
GH_PAT: ${{ secrets.GH_PAT }}
with:
tag: pr-${{ github.event.pull_request.number }}-${{ github.sha }}
13 changes: 6 additions & 7 deletions .github/workflows/on-push-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,21 @@ permissions:

jobs:
validate:
uses: unbounded-tech/workflows-crossplane/.github/workflows/validate.yaml@v2.20.0
uses: hops-ops/workflows-crossplane/.github/workflows/validate.yaml@v3.0.0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

git ls-files ".github/workflows/*.yaml" | head -20

Repository: hops-ops/auth-stack

Length of output: 169


🏁 Script executed:

cat -n .github/workflows/on-push-main.yaml

Repository: hops-ops/auth-stack

Length of output: 1741


🏁 Script executed:

# Check if hops-ops/workflows-crossplane is a real repo and what tags/commits are available
web_search
GitHub Actions best practices: commit SHA vs tag pinning

Repository: hops-ops/auth-stack

Length of output: 159


🌐 Web query:

GitHub Actions security best practices tag vs commit SHA pinning 2024 2025

💡 Result:

In the context of GitHub Actions security, pinning to a full-length commit SHA is the industry-standard best practice for ensuring immutability and mitigating supply chain attacks [1][2][3]. Key differences and security implications: Commit SHA Pinning (Recommended) - Immutability: A commit SHA uniquely identifies a specific state of the repository, ensuring that the exact same code runs every time [2][4][3]. - Security: It protects against tag hijacking, where an attacker compromises a repository or maintainer account to move a version tag (e.g., v1) to a malicious commit [5][6][3]. - Implementation: Use the full-length commit SHA (e.g., actions/checkout@b4ffde6) [1][2]. - Maintenance: To maintain readability and facilitate updates, it is best practice to include the version tag as a comment (e.g., uses: actions/checkout@b4ffde6 # v4.1.1) [5][7][4]. Tag-Based Pinning (Convenient but Risky) - Mutability: Version tags (e.g., v1, v1.2) are mutable references in Git [3][8]. They can be deleted and re-created or force-pushed to point to different commits, making them vulnerable to supply chain compromise [6][3][9]. - Trust: Pinning to a tag should only be done if you fully trust the creator [1]. Even then, it remains susceptible to account takeovers or repository compromises [5][1]. Best Practices for 2025-2026 - Enforce SHA Pinning: Organizations can now configure policies at the repository or organization level to require that all actions be pinned to a full-length commit SHA [1][4][10]. - Automated Updates: Use tools like Dependabot or Renovate to manage updates for pinned actions [5][7][11]. These tools can automatically open pull requests to update SHAs when new versions are released, balancing security with maintenance [4][3]. - Blocklist Compromised Actions: GitHub allows organizations to block specific actions or versions using the! prefix (e.g.,!compromised-org/action) to respond rapidly to security incidents [4][10]. - Defense in Depth: Pinning is one layer of security. Other critical practices include minimizing secret exposure, using OIDC for authentication, auditing high-privilege workflow triggers (e.g., pull_request_target), and restricting self-hosted runners [12][13][10].

Citations:


Pin reusable workflows by commit SHA.

These uses: refs are tag-pinned, not commit-pinned. Switch to immutable SHAs to prevent tag hijacking and supply chain attacks.

Also applies to: 37-37, 40-40

🧰 Tools
🪛 zizmor (1.25.2)

[error] 26-26: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/on-push-main.yaml at line 26, The workflow uses:
references are tag-pinned
("hops-ops/workflows-crossplane/.github/workflows/validate.yaml@v3.0.0") and
must be locked to immutable commit SHAs; for each occurrence (the three uses:
the one shown and the ones at the other reported lines) replace the tag ref
(`@v3.0.0`) with the exact commit SHA from the upstream repository (e.g.,
`@abcdef1234567890`...), fetching the canonical commit ID from the
hops-ops/workflows-crossplane repo and updating the three uses: entries so they
reference that SHA instead of the tag.

with:
examples: |
[
{ "example": "examples/authstacks/minimal.yaml" },
{ "example": "examples/authstacks/standard.yaml" },
{ "example": "examples/authstacks/local-colima.yaml" }
{ "example": "examples/authstacks/minimal.yaml", "api_path": "apis/authstacks" },
{ "example": "examples/authstacks/standard.yaml", "api_path": "apis/authstacks" },
{ "example": "examples/authstacks/local-colima.yaml", "api_path": "apis/authstacks" }
]
api_path: apis/authstacks
error_on_missing_schemas: true

test:
uses: unbounded-tech/workflows-crossplane/.github/workflows/test.yaml@v2.20.0
uses: hops-ops/workflows-crossplane/.github/workflows/test.yaml@v3.0.0

e2e:
uses: unbounded-tech/workflows-crossplane/.github/workflows/e2e.yaml@v2.20.0
uses: hops-ops/workflows-crossplane/.github/workflows/e2e.yaml@v3.0.0

version-and-tag:
name: Version and Tag
Expand Down
48 changes: 29 additions & 19 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
SHELL := /bin/bash

PACKAGE ?= auth-stack
# Default XRD_DIR for legacy single-API targets; multi-API targets derive per-example.
XRD_DIR := apis/authstacks
COMPOSITION := $(XRD_DIR)/composition.yaml
DEFINITION := $(XRD_DIR)/definition.yaml
CONFIGURATION := $(XRD_DIR)/configuration.yaml
EXAMPLE_DEFAULT := examples/authstacks/standard.yaml
RENDER_TESTS := $(wildcard tests/test-*)
E2E_TESTS := $(wildcard tests/e2etest-*)

# Multi-API support: examples/<apiplural>/<example>.yaml maps to apis/<apiplural>/.
# Helper macro: api_dir_for(example_path) → apis/<dirname>
api-dir = apis/$(word 2,$(subst /, ,$(1)))

clean:
rm -rf _output
rm -rf .up
rm -f $(CONFIGURATION)

build:
up project build

generate-configuration:
@set -euo pipefail; \
hops validate generate-configuration --path . --api-path "$(XRD_DIR)"

# Examples list - mirrors GitHub Actions workflow
# Format: example_path::observed_resources_path (observed_resources_path is optional)
# api_path is derived from example_path via the api-dir macro (examples/<x>/... → apis/<x>/).
EXAMPLES := \
examples/authstacks/minimal.yaml:: \
examples/authstacks/standard.yaml:: \
Expand All @@ -35,14 +35,17 @@ render\:all:
for entry in $(EXAMPLES); do \
example=$${entry%%::*}; \
observed=$${entry#*::}; \
api_dir=$$(echo "$$example" | awk -F/ '{print "apis/" $$2}'); \
composition="$$api_dir/composition.yaml"; \
definition="$$api_dir/definition.yaml"; \
outfile="$$tmpdir/$$(echo $$entry | tr '/:' '__')"; \
( \
if [ -n "$$observed" ]; then \
echo "=== Rendering $$example with observed-resources $$observed ==="; \
up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example --observed-resources=$$observed; \
up composition render --xrd=$$definition $$composition $$example --observed-resources=$$observed; \
else \
echo "=== Rendering $$example ==="; \
up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example; \
echo "=== Rendering $$example (api=$$api_dir) ==="; \
up composition render --xrd=$$definition $$composition $$example; \
fi; \
echo "" \
) > "$$outfile" 2>&1 & \
Expand All @@ -59,24 +62,27 @@ render\:all:
exit $$failed

# Validate all examples (parallel execution, output shown per-job when complete)
validate\:all: generate-configuration
validate\:all:
@tmpdir=$$(mktemp -d); \
pids=""; \
for entry in $(EXAMPLES); do \
example=$${entry%%::*}; \
observed=$${entry#*::}; \
api_dir=$$(echo "$$example" | awk -F/ '{print "apis/" $$2}'); \
composition="$$api_dir/composition.yaml"; \
definition="$$api_dir/definition.yaml"; \
outfile="$$tmpdir/$$(echo $$entry | tr '/:' '__')"; \
( \
if [ -n "$$observed" ]; then \
echo "=== Validating $$example with observed-resources $$observed ==="; \
up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example \
up composition render --xrd=$$definition $$composition $$example \
--observed-resources=$$observed --include-full-xr --quiet | \
crossplane beta validate $(XRD_DIR) --error-on-missing-schemas -; \
crossplane beta validate $$api_dir --error-on-missing-schemas -; \
else \
echo "=== Validating $$example ==="; \
up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example \
echo "=== Validating $$example (api=$$api_dir) ==="; \
up composition render --xrd=$$definition $$composition $$example \
--include-full-xr --quiet | \
crossplane beta validate $(XRD_DIR) --error-on-missing-schemas -; \
crossplane beta validate $$api_dir --error-on-missing-schemas -; \
fi; \
echo "" \
) > "$$outfile" 2>&1 & \
Expand All @@ -93,16 +99,16 @@ validate\:all: generate-configuration
exit $$failed

# Shorthand aliases
.PHONY: render validate generate-configuration
.PHONY: clean build test e2e publish render validate
render: ; @$(MAKE) 'render:all'
validate: ; @$(MAKE) generate-configuration 'validate:all'
validate: ; @$(MAKE) 'validate:all'

# Single example targets
# Single example targets (legacy — uses default XRD_DIR for examples/authstacks/<name>.yaml)
render\:%:
@example="examples/authstacks/$*.yaml"; \
up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example

validate\:%: generate-configuration
validate\:%:
@example="examples/authstacks/$*.yaml"; \
up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example \
--include-full-xr --quiet | \
Expand All @@ -113,3 +119,7 @@ test:

e2e:
up test run $(E2E_TESTS) --e2e

publish:
@if [ -z "$(tag)" ]; then echo "Error: tag is not set. Usage: make publish tag=<version>"; exit 1; fi
up project build --push --tag $(tag)
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# auth-stack

Installs Zitadel into a Kubernetes cluster as the platform identity provider. Wraps the upstream `zitadel/zitadel` Helm chart with a typed XRD surface that handles the namespace, the database wiring, Gateway API routing, and re-projects the chart-managed bootstrap secrets (admin PAT + login-client PAT) into XR status for downstream consumers.
Installs Zitadel into a Kubernetes cluster as the platform identity provider, and hosts a focused set of `auth.hops.ops.com.ai` primitive XRDs (`HumanUser`, `MachineUser`, `Grant`, `IDP`) that compose against the installed Zitadel.

The stack XRD (`AuthStack`) wraps the upstream `zitadel/zitadel` Helm chart — handling the namespace, database wiring, Gateway API routing, and re-projecting chart-managed bootstrap secrets (admin PAT + login-client PAT) into XR status for downstream consumers.

The primitive XRDs let operators declaratively manage the identity model inside a running Zitadel — see [Auth-group primitives](#auth-group-primitives) below.

## Quick Start

Expand Down Expand Up @@ -76,6 +80,21 @@ status:
loginClientPatSecretRef: { name: login-client, namespace: zitadel, key: pat }
```

## Auth-group primitives

Per [[specs/identity-architecture]], the auth-group primitive XRDs that have substantive composition value-add — `HumanUser`, `MachineUser`, `Grant`, `IDP` — live in this repo alongside `AuthStack` under the `auth.hops.ops.com.ai` group.

Status:

| Kind | Plural | Composes | Status |
|---|---|---|---|
| `HumanUser` | `humanusers` | `HumanUser` + optional `UserIDPLink` MRs | TO WRITE |
| `MachineUser` | `machineusers` | `MachineUser` + `PAT` + AWS SM push pipeline | TO WRITE |
| `Grant` | `grants` | `ProjectMember` (same-Org) or `ProjectGrant + GrantMember` (cross-Org) | TO WRITE |
| `IDP` | `idps` | polymorphic over `GoogleIDP` / `GitHubIDP` / `OIDCIDP` / `SAMLIDP` | TO WRITE |

Operators apply raw Zitadel `Org` / `Project` / `Role` MRs (`org.zitadel.m.crossplane.io`, `project.zitadel.m.crossplane.io`) directly when they need them — the `Tenant` business kind in [[tenant-stack]] composes the initial set during Tenant scaffolding.

## Cross-Stack Integration

The intent is for consumer stacks (gitops/ArgoCD, observe/Grafana, the-website) to wire to AuthStack's status surface rather than configuring OIDC manually. Today, those consumers still need a Zitadel OIDC application created out-of-band (via the Zitadel UI/API) and a client ID/secret provided to them. Once the Zitadel Crossplane provider lands, consumer stacks can declaratively create OIDC applications by referencing `status.bootstrap.iamAdminPatSecretRef`.
Expand Down
16 changes: 0 additions & 16 deletions apis/authstacks/configuration.yaml

This file was deleted.

11 changes: 7 additions & 4 deletions examples/authstacks/local-colima.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Local-dev manifest for colima.
# Pairs with: a `PSQLCluster` named `zitadel-pg` whose `app.externalSecret`
# pulls credentials from AWS SM at `zitadel/app`, and a masterkey at
# `zitadel/masterkey` synced via `hops secrets sync aws`.
# pulls credentials from AWS SM at `zitadel/app`, and a masterkey +
# admin-password under a single AWS SM entry at `zitadel/credentials`
# synced via `hops secrets sync aws`.
apiVersion: hops.ops.com.ai/v1alpha1
kind: AuthStack
metadata:
Expand All @@ -12,11 +13,13 @@ spec:
domain: auth.localtest.me
externalSecure: false

# PULL flow: AuthStack ExternalSecret pulls masterkey + admin-password
# from a single AWS SM JSON blob keyed by secretPath. Both values are
# seeded together by `hops auth bootstrap <cluster>`.
externalSecrets:
enabled: true
secretStoreName: hops-aws-secrets-manager
masterkey:
secretPath: zitadel/masterkey
secretPath: zitadel/credentials

firstInstance:
org: hops-ops
Expand Down
Loading
Loading