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
14 changes: 14 additions & 0 deletions .buzzvil/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Buzzvil patched authentik image (overlay on the official server image).
#
# Replaces two SAML provider files so that IdP-initiated SSO forwards the
# SP-supplied RelayState (AWS WorkSpaces appends a per-session state code via
# ?RelayState= to the init URL) instead of only using the static
# default_relay_state. Without this, the WorkSpaces native client loops back to
# sign-in because euc-sso cannot bind the auth code to the client session.
#
# Patch source of truth: this repo's authentik/ tree (see PR #1). File paths below
# match the official image layout (/authentik/...).
FROM ghcr.io/goauthentik/server:2026.2.1

COPY --chown=authentik:authentik authentik/providers/saml/processors/authn_request_parser.py /authentik/providers/saml/processors/authn_request_parser.py
COPY --chown=authentik:authentik authentik/providers/saml/views/sso.py /authentik/providers/saml/views/sso.py
74 changes: 74 additions & 0 deletions .github/workflows/build-patched-ecr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: build-patched-ghcr

# authentik 패치 이미지 빌드 + GitHub Release 를 함께 생성.
#
# 트리거: '<authentik version>-relaystate' 형태의 태그 push (예: 2026.2.1-relaystate).
# 태그명 = 이미지 태그 = release 태그 → 빌드와 release 가 항상 한 쌍으로 생성된다.
#
# 새 authentik 버전 릴리스 절차:
# 1) 새 브랜치(buzzvil/workspaces-relaystate-<ver>)에서 .buzzvil/Dockerfile 의 FROM 을
# 그 버전으로 바꾸고 SAML 2 파일 패치를 재적용.
# 2) 그 커밋에 태그 '<ver>-relaystate' 를 push → 이 워크플로우가 이미지 빌드 + release 생성.
# 3) ops(buzz-k8s-resources) 의 global.image tag 를 갱신하고 WorkSpaces 로그인 재검증.
on:
push:
tags:
- '*-relaystate'
workflow_dispatch:
inputs:
image_tag:
description: "이미지 태그 (예: 2026.2.1-relaystate). 이미지만 rebuild (release 미생성)."
required: true

permissions:
contents: write # release 생성
packages: write # GHCR push

jobs:
build:
runs-on: ubuntu-latest
env:
IMAGE_TAG: ${{ github.event.inputs.image_tag || github.ref_name }}
steps:
- name: Checkout (patch files only)
uses: actions/checkout@v4
with:
sparse-checkout: |
.buzzvil
authentik/providers/saml/processors/authn_request_parser.py
authentik/providers/saml/views/sso.py
sparse-checkout-cone-mode: false

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: .buzzvil/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/buzzvil/authentik:${{ env.IMAGE_TAG }}

- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${{ github.ref_name }}" \
--repo "${{ github.repository }}" \
--title "${{ github.ref_name }} — WorkSpaces SAML RelayState patch" \
--notes "패치 이미지: \`ghcr.io/buzzvil/authentik:${{ github.ref_name }}\` (multi-arch amd64/arm64, public). 패치 내용·빌드·제거 조건은 저장소 README 참고." \
--verify-tag \
|| echo "release for ${{ github.ref_name }} already exists — skipping"
Comment on lines +64 to +74

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

run: 블록의 템플릿 보간이 셸 인젝션 위험(zizmor error) — env 변수 경유로 전달 권장.

${{ github.ref_name }}${{ github.repository }}run: 셸 스크립트에 직접 보간됩니다. 태그 push 권한이 있는 사용자만 ref_name을 제어할 수 있어 공격 표면은 제한적이지만, 특수문자(백틱, $(), ; 등)가 포함된 태그명이 생성되면 셸 인젝션으로 이어질 수 있습니다. GitHub Actions 표준 완화책은 표현식을 env:로 넘기고 셸에서는 환경변수로만 참조하는 것입니다.

🛡️ 제안
       - name: Create GitHub Release
         if: startsWith(github.ref, 'refs/tags/')
         env:
           GH_TOKEN: ${{ github.token }}
+          TAG_NAME: ${{ github.ref_name }}
+          REPO_NAME: ${{ github.repository }}
         run: |
-          gh release create "${{ github.ref_name }}" \
-            --repo "${{ github.repository }}" \
-            --title "${{ github.ref_name }} — WorkSpaces SAML RelayState patch" \
-            --notes "패치 이미지: \`ghcr.io/buzzvil/authentik:${{ github.ref_name }}\` (multi-arch amd64/arm64, public). 패치 내용·빌드·제거 조건은 저장소 README 참고." \
+          gh release create "$TAG_NAME" \
+            --repo "$REPO_NAME" \
+            --title "$TAG_NAME — WorkSpaces SAML RelayState patch" \
+            --notes "패치 이미지: \`ghcr.io/buzzvil/authentik:$TAG_NAME\` (multi-arch amd64/arm64, public). 패치 내용·빌드·제거 조건은 저장소 README 참고." \
             --verify-tag \
-            || echo "release for ${{ github.ref_name }} already exists — skipping"
+            || echo "release for $TAG_NAME already exists — skipping"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "${{ github.ref_name }}" \
--repo "${{ github.repository }}" \
--title "${{ github.ref_name }} — WorkSpaces SAML RelayState patch" \
--notes "패치 이미지: \`ghcr.io/buzzvil/authentik:${{ github.ref_name }}\` (multi-arch amd64/arm64, public). 패치 내용·빌드·제거 조건은 저장소 README 참고." \
--verify-tag \
|| echo "release for ${{ github.ref_name }} already exists — skipping"
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ github.ref_name }}
REPO_NAME: ${{ github.repository }}
run: |
gh release create "$TAG_NAME" \
--repo "$REPO_NAME" \
--title "$TAG_NAME — WorkSpaces SAML RelayState patch" \
--notes "패치 이미지: \`ghcr.io/buzzvil/authentik:$TAG_NAME\` (multi-arch amd64/arm64, public). 패치 내용·빌드·제거 조건은 저장소 README 참고." \
--verify-tag \
|| echo "release for $TAG_NAME already exists — skipping"
🧰 Tools
🪛 zizmor (1.26.1)

[error] 69-69: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 71-71: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 72-72: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 74-74: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 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/build-patched-ecr.yaml around lines 64 - 74, The Create
GitHub Release step is interpolating github.ref_name and github.repository
directly inside the run script, which creates a shell-injection risk. Update the
workflow so the release name and repo are passed through env variables on the
Create GitHub Release step, then reference only those environment variables
inside the gh release create command. Keep the change localized to the Create
GitHub Release block and avoid direct expression expansion in the shell script.

Source: Linters/SAST tools

57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,60 @@
# ⚠️ Buzzvil fork of goauthentik/authentik

이 저장소는 **upstream [goauthentik/authentik](https://github.com/goauthentik/authentik) 의 포크**이며,
AWS WorkSpaces SAML SSO 를 위한 **2 파일 패치**만 얹은 것이다. 그 외 코드는 upstream 과 동일하다.

빌드 이미지: **`ghcr.io/buzzvil/authentik:2026.2.1-relaystate`** (multi-arch amd64/arm64, public)
소비처: `Buzzvil/buzz-k8s-resources` → `argo-cd/buzzvil-eks-ops/apps/authentik.yaml` 의 `global.image` override
(ops 클러스터 `ops-k8s-authentik`). base: `ghcr.io/goauthentik/server:2026.2.1`.

## 무슨 패치인가

**IdP-initiated SAML 로그인이 SP 가 넘긴 `RelayState` 를 그대로 forward** 하도록 수정.

| 파일 | 변경 |
|------|------|
| `authentik/providers/saml/views/sso.py` | `SAMLSSOBindingInitView` 가 `request.GET["RelayState"]` 를 `idp_initiated()` 로 전달 |
| `authentik/providers/saml/processors/authn_request_parser.py` | `idp_initiated(relay_state=None)` — 전달된 relay_state 를 provider 의 정적 `default_relay_state` 보다 우선 사용 |

### 왜 필요한가
AWS WorkSpaces native client 로그인은 euc-sso 가 IdP init URL 에 **per-session state code 를
`?RelayState=` 로 붙여** 보내고, IdP 가 그 값을 SAML Response 에 echo 해야 euc-sso 가 auth code 를
그 client 세션에 바인딩한다. **upstream Authentik 의 IdP-initiated init 엔드포인트는 요청의 `RelayState`
쿼리를 무시하고 provider 의 정적 `default_relay_state` 만** 쓰기 때문에(SP-initiated redirect/POST
뷰와 달리), state code 가 유실되어 native client 가 sign-in 으로 loop-back 한다.
(upstream 동일 이슈: goauthentik/authentik#17542)

## 빌드 / 릴리스

`.buzzvil/Dockerfile` 이 공식 이미지 위에 위 2 파일만 COPY 하는 overlay 이고, 빌드 워크플로우
(`build-patched-ghcr`)가 **`<ver>-relaystate` 태그 push** 시 GitHub-hosted 러너에서 multi-arch(amd64/arm64)
빌드해 GHCR 로 push 하고 **동일 태그로 GitHub Release 를 함께 생성**한다.
(별도 self-hosted 러너·AWS·org secret 불필요. 이미지만 재빌드하려면 workflow_dispatch.)

## 언제 내릴 수 있나 (이 포크 제거 조건)

이 포크는 **임시**이며, 아래 중 하나가 충족되면 제거하고 공식 이미지로 되돌린다.

1. **(우선) upstream 이 고칠 때** — goauthentik/authentik 가 IdP-initiated init 뷰에서 요청 `RelayState`
를 forward 하도록 반영한 릴리스가 나오면(이슈 #17542 에 upstream PR 기여 권장), ops 의 `global.image`
override 를 제거해 `ghcr.io/goauthentik/server` 정식 이미지로 복귀하고 이 포크를 archive.
2. **WorkSpaces SAML 경로가 사라질 때** — adfit VDI 폐기 또는 인증 방식 변경 시.

**제거 전 검증**: 대상 버전으로 올린 뒤 WorkSpaces native client 로그인을 end-to-end(auth code 발급 →
데스크톱 연결)로 확인하고 나서 포크를 내린다.

## 유지보수 (upstream 이 고치기 전까지)

authentik 을 새 버전으로 올릴 때마다 이 2 파일 패치를 새 버전 기준으로 다시 적용한다:
1. upstream 태그(`version/<X.Y.Z>`)에서 브랜치 `buzzvil/workspaces-relaystate-<X.Y.Z>` 생성
2. 위 2 파일 패치 재적용(작고 self-contained), `.buzzvil/Dockerfile` 의 `FROM ...:<X.Y.Z>` 갱신
3. 그 커밋에 태그 **`<X.Y.Z>-relaystate`** push → 워크플로우가 멀티아치 이미지 빌드 + 동일 태그 Release 생성
4. ops(buzz-k8s-resources) 의 `global.image` tag 를 `<X.Y.Z>-relaystate` 로 갱신 → WorkSpaces 로그인 재검증

---

> 아래는 upstream authentik README 원문.

<p align="center">
<img src="https://goauthentik.io/img/icon_top_brand_colour.svg" height="150" alt="authentik logo">
</p>
Expand Down
10 changes: 8 additions & 2 deletions authentik/providers/saml/processors/authn_request_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,16 @@ def parse_detached(
except ParseError as exc:
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc

def idp_initiated(self) -> AuthNRequest:
def idp_initiated(self, relay_state: str | None = None) -> AuthNRequest:
"""Create IdP Initiated AuthNRequest"""
request = AuthNRequest(relay_state=None)
if self.provider.default_relay_state != "":
# Buzzvil patch: prefer a RelayState supplied by the SP over the static
# default_relay_state. AWS WorkSpaces IdP-initiated SSO appends a per-session
# state code via ?RelayState= to the init URL; it must round-trip back to AWS
# or the euc-sso auth code cannot be bound to the client session (loop-back).
if relay_state:
request.relay_state = relay_state
elif self.provider.default_relay_state != "":
request.relay_state = self.provider.default_relay_state
if self.provider.default_name_id_policy != SAMLNameIDPolicy.UNSPECIFIED:
request.name_id_policy = self.provider.default_name_id_policy
Expand Down
6 changes: 5 additions & 1 deletion authentik/providers/saml/views/sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,5 +155,9 @@ class SAMLSSOBindingInitView(SAMLSSOView):
def check_saml_request(self) -> HttpRequest | None:
"""Create SAML Response from scratch"""
LOGGER.debug("No SAML Request, using IdP-initiated flow.")
auth_n_request = AuthNRequestParser(self.provider).idp_initiated()
# Buzzvil patch: forward the SP-supplied RelayState (AWS WorkSpaces euc-sso
# per-session state code) into the IdP-initiated AuthNRequest so it round-trips.
auth_n_request = AuthNRequestParser(self.provider).idp_initiated(
relay_state=self.request.GET.get(REQUEST_KEY_RELAY_STATE),
)
self.plan_context[PLAN_CONTEXT_SAML_AUTH_N_REQUEST] = auth_n_request