Skip to content

Commit 503b798

Browse files
authored
Merge pull request #2 from jinnatar/push-knqpoqnvzpnw
Updates to get a release out
2 parents f1c318d + 5790850 commit 503b798

File tree

6 files changed

+139
-63
lines changed

6 files changed

+139
-63
lines changed

.github/workflows/publish-image.yml

+21-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
---
22
name: Docker image
33

4-
on: push
4+
on:
5+
pull_request:
6+
release:
7+
types: [published]
58

69
env:
710
REGISTRY: ghcr.io
@@ -18,17 +21,22 @@ jobs:
1821
- name: Checkout repository
1922
uses: actions/checkout@v4
2023

21-
- name: Get the release channel
22-
id: get_channel
24+
- name: Infer image tags
25+
id: get_tags
2326
shell: bash
2427
run: |
25-
if [[ "$GITHUB_REF" == 'refs/heads/main' ]]; then
26-
echo "channel=latest" >> $GITHUB_OUTPUT
27-
echo "version=main_${GITHUB_SHA::6}" >> $GITHUB_OUTPUT
28-
elif [[ "$GITHUB_REF" == "refs/heads/"* ]]; then
29-
echo "version=${GITHUB_REF/refs\/heads\//}_${GITHUB_SHA::6}" >> $GITHUB_OUTPUT
30-
elif [[ "$GITHUB_REF" == "refs/tags/"* ]]; then
31-
echo "channel=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
28+
if [[ "$GITHUB_EVENT_NAME" == "release" ]]; then
29+
# We believe tags from releases
30+
echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
31+
# But also give a more stable git relevant tag
32+
echo "hash=main_${GITHUB_SHA::6}" >> $GITHUB_OUTPUT
33+
if [[ "$GITHUB_REF" != "refs/tags/"*"-pre"* ]]; then
34+
# Only tag latest for non-prereleases
35+
echo "channel=latest" >> $GITHUB_OUTPUT
36+
fi
37+
elif [[ "$GITHUB_REF" == "refs/pull/"* ]]; then
38+
# Tag PRs only by PR number & branch to pollute registry less on force pushes
39+
echo "version=pr${GITHUB_REF_NAME/\/merge}_${GITHUB_HEAD_REF}" >> $GITHUB_OUTPUT
3240
fi
3341
3442
- name: Extract metadata (tags, labels) for Docker
@@ -37,8 +45,9 @@ jobs:
3745
with:
3846
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
3947
tags: |
40-
type=raw,value=${{ steps.get_channel.outputs.channel }}
41-
type=raw,value=${{ steps.get_channel.outputs.version }}
48+
type=raw,value=${{ steps.get_tags.outputs.hash }}
49+
type=raw,value=${{ steps.get_tags.outputs.channel }}
50+
type=raw,value=${{ steps.get_tags.outputs.version }}
4251
4352
- name: Log in to the Container registry
4453
uses: docker/login-action@v2

Dockerfile

+22-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
FROM python:3.12-alpine
22

3-
# runtime dependencies
3+
ENV SATOSA_VERSION="8.5.1"
4+
5+
# While SATOSA now has a release with modern OIDC support, it's dependency idpyoidc has not yet
6+
# made a release that allows ES256. Ergo, we pull that dep in from the branch that most
7+
# closely matches where they usually cut releases from.
8+
# More context: https://github.com/IdentityPython/idpy-oidc/issues/114
9+
ENV IDPYOIDC_REF="git+https://github.com/IdentityPython/idpy-oidc@issuer_metadata"
10+
411
# Run as uid:gid 999:999 to avoid conferring default UID 1000 permissions to key material
512
RUN set -eux; \
613
delgroup ping ; \
@@ -11,29 +18,27 @@ RUN set -eux; \
1118
jq \
1219
libxml2-utils \
1320
xmlsec \
14-
git \
15-
; \
16-
pip install --no-cache-dir \
17-
yq \
18-
;
21+
git # Only needed until we have a non-git idpyoidc ref
1922

2023

21-
# Install SATOSA from git latest since the latest release 8.4.0 lacks idpy_oidc_backend which is required for PKCE
22-
# Also install ES256 compatible idpyoidc from fork while not fixed upstream: https://github.com/IdentityPython/idpy-oidc/issues/110
23-
RUN set -eux; \
24-
pip install --no-cache-dir \
25-
'satosa[idpy_oidc_backend] @ git+https://github.com/IdentityPython/SATOSA' \
26-
'idpyoidc @ git+https://github.com/jinnatar/idpy-oidc@sign-algo-verify' \
27-
; \
28-
mkdir /etc/satosa; \
29-
chown -R satosa:satosa /etc/satosa
24+
RUN pip install --no-cache-dir \
25+
yq \
26+
"satosa[idpy_oidc_backend]==${SATOSA_VERSION}" \
27+
"idpyoidc @ ${IDPYOIDC_REF}"
3028

29+
RUN mkdir /etc/satosa && chown -R satosa:satosa /etc/satosa
3130
WORKDIR /etc/satosa
3231

3332
# Preload bespoke ENV configurable config
3433
COPY *.yaml /etc/satosa
34+
# Preload dummy XML metadata
35+
COPY dummy-metadata.xml /etc/satosa
36+
37+
# Add an entrypoint so we can use ENV for gunicorn args
38+
ENV LOG_LEVEL="INFO"
39+
ENV LISTEN_ADDR="0.0.0.0:80"
40+
COPY entrypoint.sh /entrypoint.sh
41+
ENTRYPOINT ["/entrypoint.sh"]
3542

36-
ENTRYPOINT ["gunicorn"]
3743
EXPOSE 80
3844
USER satosa:satosa
39-
CMD ["-b0.0.0.0:80","satosa.wsgi:app"]

README.md

+75-27
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
# SATOSA based SAML to Kanidm OIDC proxy
22

33
i.e. How to connect legacy web apps that only support SAML to be backed by Kanidm OIDC. While the configs in this repo can be educational for rolling your own SATOSA setup, an opinionated ENV configurable container image is also provided.
4+
This example on purpose only supports a 1:1 proxy config where a single SAML supporting web service auths via a single OIDC endpoint. To limit blast radius, just deploy multiple if you have multiple SAML-only services.
45

5-
> [!CAUTION]
6-
> This is an early version that only supports a 1:1 proxy config where a single SAML supporting web service auths via a single OIDC endpoint.
7-
> The intent is to morph into a "v2" that allows a dynamic mapping of multiple systems to multiple OIDC endpoints via a single SAML proxy. The simpler version will be preserved for educational purposes but is intended to become "legacy".
6+
> [!NOTE]
7+
> If you want to just skip to the part where we use this with Kanidm, you could jump straight to the practical example: [Ceph SSO via Kanidm](#practical-example-ceph-sso-via-kanidm)
88
99
## TODO items on the roadmap
10-
1. Add log level configuration via ENV. It's now hardcoded to debug.
11-
2. Rewrite env config & the SATOSA configs for dynamic routing so that multiple apps can be routed to different OIDC clients. In the meanwhile you can configure multiple apps to use the same proxy, but then you can't control via claim maps on the Kanidm side who is eligible for what app.
12-
3. Get rid of the idpyoidc PR fork use once they no longer force RS256.
10+
1. Get rid of the idpyoidc git build once there's a release that contains ES256 support.
11+
2. Get rid of any manual jiggery with idpyoidc once SATOSA requires a sufficiently high version to support ES256.
1312

1413
## The container
1514

1615
The container built at `ghcr.io/jinnatar/satosa-saml-proxy:latest` is a proof of concept using the SATOSA configs in the repo. The guides below will assume you are using it, but nothing prevents you from using the same configs and ENV config with any other supported SATOSA installation method. I am using the container myself in my environment and have a vested interest in keeping it going and tested.
1716

18-
The caveats with the container and/or trying to go without it:
19-
- The currently released version of SATOSA, 8.4.0 is over a year old and does not include their new and improved OIDC module. The better module is required for PKCE support, which is why the container is built from their git HEAD instead of a release.
20-
- The main dependency for their new OIDC module is idpy-oidc. Unfortunately it has an issue that prevents using ES256 for signing. The container has a patch that instead enforces ES256, but it's unsuitable to upstream as a proper fix. This patching will be removed once https://github.com/IdentityPython/idpy-oidc/issues/110 is resolved. You could use `ES256.patch` to replicate this bubblegum fix outside the container.
21-
- The containers are not version tagged, since there is no upstream version of SATOSA that fulfills the requirements. They are however tagged to specific commits for your convenience if you do not wish to follow `:latest`.
17+
### The caveats with the container and/or trying to go without it:
18+
- While recent releases of SATOSA support PKCE, they depend on the Python library `idpyoidc` for this. Unfortunately it has an issue that prevents using `ES256` for signing with released versions. The container thus uses [a branch from git](https://github.com/IdentityPython/idpy-oidc/tree/issuer_metadata) that contains the fix for this. Once a full release is made with said fix that will be used specifically. Once SATOSA requires a high enough release of `idpyoidc` that contains a fix, we can stop with this nonsense altogether.
19+
- The containers are now version tagged as per SATOSA upstream versions. However, due to the above nonsense those tags will be updated later when better build provenance is available.
20+
21+
### Container config options
22+
The container contains minimal config options via environment variables for ease of use.
23+
- `LOG_LEVEL`: defaults to `INFO`. You may want to raise this to `DEBUG` for troubleshooting, but be aware logs will then leak tokens. Affects gunicorn and all SATOSA modules (if using the env based default config).
24+
- `LISTEN_ADDR`: defaults to `0.0.0.0:80`. You may need to alter this depending on your container orchestration and proxying needs.
25+
- Any other gunicorn flags can be passed as arguments.
2226

2327
## Step by step guides for usage
2428

@@ -35,14 +39,35 @@ SAML is a bit *involved* so we need to prep a persistent certificate and provide
3539
1. Once you have your metadata XML file, make it available to your container, for example via a volume. The dummy data is already available.
3640
2. Configure the ENV variables that will tweak the provided SATOSA configs. You can edit the provided `example.env` file and feed it to Docker via the `--env-file` flag. Make sure to **not** quote values if using that flag. Explanations below:
3741
```shell
38-
ENCRYPTION_KEY=0xDEADBEEF # Key used to encrypt state in transit. Could generate with `openssl rand -base64 32`
39-
OIDC_CLIENT_ID=your-client-id # The OIDC client id in Kanidm is the name of the integration, for example `ceph`
42+
# Enables debug logging for troubleshooting.
43+
# Change this to "INFO" when everything works!
44+
LOG_LEVEL=DEBUG # Enables debug logging for troubleshooting. Change this to "INFO" when everything works!
45+
46+
# Key used to encrypt state in transit. Use a key of your own choosing!
47+
# For example, generate one with `openssl rand -base64 32`
48+
ENCRYPTION_KEY=0xDEADBEEF
49+
# The OIDC client id in Kanidm is the name of the integration, for example `ceph`.
50+
OIDC_CLIENT_ID=your-client-id
4051
OIDC_CLIENT_SECRET=your-oidc-client-secret
41-
OIDC_ISSUER_URL=https://idm.example.com/oauth2/openid/your-client-id # Full URL to the discovery endpoint
42-
OIDC_NAME=unique_oidc_name # A unique id used for this OIDC backend in SATOSA. Uniqueness becomes relevant if you configure multiple on the same proxy.
43-
PROXY_BASE_URL=https://saml.example.com # Where your proxy lives. **must** be https, must be the root of a host, must match the CN in your cert from step 1.
44-
SAML_METADATA="dummy-metadata.xml" # A path to your app SAML metadata file. The working directory of the provided image is `/etc/satosa` so the relative path example here would expect the file to be on the container at `/etc/satosa/dummy-metadata.xml`. If you can't get this until the proxy is running and you've registered it in the app, use dummy-metadata.xml as a workaround to boot the proxy without it.
45-
SAML_NAME=unique_saml_name # A unique id used for this SAML frontend in SATOSA. Uniqueness becomes relevant if you configure multiple on the same proxy.
52+
53+
# Full URL to the discovery endpoint
54+
OIDC_ISSUER_URL=https://idm.example.com/oauth2/openid/your-client-id
55+
56+
# A unique id used for the OIDC side in SATOSA, used in the callback URL: `https://ceph.example.com/unique_oidc_name`
57+
OIDC_NAME=unique_oidc_name
58+
# A unique id used for the SAML side in SATOSA, used in URLs.
59+
SAML_NAME=unique_saml_name
60+
61+
# Where your proxy lives. **must** be https, must be the root of a host with no subdir,
62+
# must match the CN in your cert from step 1, must be unique per app you're integrating.
63+
PROXY_BASE_URL=https://saml.example.com
64+
65+
# A path to your app SAML metadata file.
66+
# The working directory of the provided image is `/etc/satosa`,
67+
# so the relative path example here would expect the file to be on the container at `/etc/satosa/dummy-metadata.xml`.
68+
# If you can't get this until the proxy is running and you've registered it in the app, use dummy-metadata.xml as a workaround to boot the proxy without it.
69+
SAML_METADATA=dummy-metadata.xml
70+
4671
```
4772
3. Launch the proxy. This depends on your container orchestration, but a simple testing example is provided below. **This is not enough, you need to get https working which is outside the scope of this guide.
4873
```shell
@@ -62,22 +87,42 @@ SAML is a bit *involved* so we need to prep a persistent certificate and provide
6287
### Practical example: Ceph SSO via Kanidm
6388
1. Pre-create your users in Ceph to give them the correct authz. In this example we'll use short usernames for simplicity so that needs to match.
6489
1. Create your Kanidm OIDC configuration the usual way, no need to disable PKCE!
90+
```shell
91+
# **Important** give the upstream Ceph landing page URL here:
92+
kanidm system oauth2 create ceph Ceph https://ceph.example.com
93+
94+
# **Important** give the proxy callback URL here. The full value depends on $OIDC_NAME:
95+
kanidm system oauth2 add-redirect-url ceph https://ceph-saml.example.com/oidc_ceph
96+
97+
# Use short usernames for convenience
98+
kanidm system oauth2 prefer-short-username ceph
99+
100+
# Create the scope map, don't forget to create the group and add your Ceph admins to it.
101+
kanidm system oauth2 update-scope-map ceph ceph_admins openid profile email
102+
103+
# Get your client_secret for use later on:
104+
kanidm system oauth2 show-basic-secret ceph
65105
```
66-
kanidm system oauth2 create ceph Ceph https://saml.example.com # **Important**, give the proxy URL here.
67-
kanidm system oauth2 prefer-short-username ceph # Use short usernames for convenience
68-
kanidm system oauth2 update-scope-map ceph ceph_admins openid profile email # Create the scope map, don't forget to create the group and add your Ceph admins to it.
69-
kanidm system oauth2 show-basic-secret ceph # Get your client_secret for use later on.
106+
1. Create your SAML2 certs and set their permissions, remember to set the correct `SN`:
107+
```shell
108+
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 \
109+
-keyout saml.key -out saml.crt -subj "/SN=ceph-saml.example.com/"
110+
chown :999 saml.key
111+
chmod g+r saml.key
70112
```
71-
1. Create your SAML2 certs and set their permissions as per the generic steps above, nothing special here.
72113
1. We can't get Ceph to spit out it's metadata XML before the proxy is functioning so we skip ahead.
73114
1. Config your ENV variables into a new env file, `ceph.env`. If you don't change the ENCRYPTION_KEY value you deserve everything you get as a result.
74115
```shell
75-
ENCRYPTION_KEY=+OSDGTYdWxesiUwcMEzaGzwCx81YHhzOFgsitMn9A/c=
116+
# Enables debug logging for troubleshooting. Change this to "INFO" when everything works!
117+
LOG_LEVEL=DEBUG
118+
# Generate this for example with: `openssl rand -base64 32`
119+
ENCRYPTION_KEY=
76120
OIDC_CLIENT_ID=ceph
77-
OIDC_CLIENT_SECRET=# You got this above from kanidm
121+
# The client secret you got in a previous step:
122+
OIDC_CLIENT_SECRET=
78123
OIDC_ISSUER_URL=https://idm.example.com/oauth2/openid/ceph
79124
OIDC_NAME=oidc_ceph
80-
PROXY_BASE_URL=https://saml.example.com
125+
PROXY_BASE_URL=https://ceph-saml.example.com
81126
SAML_METADATA=dummy-metadata.xml
82127
SAML_NAME=saml_ceph
83128
```
@@ -90,7 +135,10 @@ SAML is a bit *involved* so we need to prep a persistent certificate and provide
90135
```
91136
1. Register the proxy with Ceph, giving it the Ceph URL, SAML metadata endpoint and an attribute field name to expect for the username.
92137
```shell
93-
ceph dashboard sso setup saml2 https://ceph.example.com https://saml.example.com/saml_ceph/metadata.xml urn:oid:0.9.2342.19200300.100.1.1
138+
ceph dashboard sso setup saml2 \
139+
https://ceph.example.com \
140+
https://ceph-saml.example.com/saml_ceph/metadata.xml \
141+
urn:oid:0.9.2342.19200300.100.1.1
94142
```
95143
1. Assuming registration was succesful, we can now get the Ceph side SAML metadata:
96144
```shell
@@ -105,4 +153,4 @@ SAML is a bit *involved* so we need to prep a persistent certificate and provide
105153
ghcr.io/jinnatar/satosa-saml-proxy:latest
106154
```
107155

108-
1. Restart the proxy and go test Ceph SSO!
156+
1. Restart the proxy and go test Ceph SSO! Once it's all working, amend your env one more time to set `LOG_LEVEL=INFO`!

entrypoint.sh

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/bin/sh
2+
3+
set -eu
4+
5+
# This entrypoint exists to set gunicorn flags from ENV.
6+
# Any arguments are interpreted as gunicorn flags to allow fine tuning, say for adding TLS.
7+
8+
# SATOSA is particular about log levels being uppercase, gunicorn doesn't care
9+
LOG_LEVEL="$(echo "$LOG_LEVEL" | tr [:lower:] [:upper:])"
10+
export LOG_LEVEL
11+
12+
exec gunicorn --log-level="$LOG_LEVEL" --bind="$LISTEN_ADDR" "$@" "satosa.wsgi:app"

example.env

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
LOG_LEVEL=debug
2+
LISTEN_ADDR=0.0.0.0:80
13
ENCRYPTION_KEY=0xDEADBEEF
24
OIDC_CLIENT_ID=your-client-id
35
OIDC_CLIENT_SECRET=your-oidc-client-secret

proxy_conf.yaml

+7-7
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@ LOGGING:
2525
stdout:
2626
class: logging.StreamHandler
2727
stream: "ext://sys.stdout"
28-
level: DEBUG
28+
level: !ENV LOG_LEVEL
2929
formatter: simple
3030
loggers:
3131
satosa:
32-
level: DEBUG
32+
level: !ENV LOG_LEVEL
3333
saml2:
34-
level: DEBUG
34+
level: !ENV LOG_LEVEL
3535
oidcendpoint:
36-
level: DEBUG
36+
level: !ENV LOG_LEVEL
3737
pyop:
38-
level: DEBUG
38+
level: !ENV LOG_LEVEL
3939
oic:
40-
level: DEBUG
40+
level: !ENV LOG_LEVEL
4141
root:
42-
level: DEBUG
42+
level: !ENV LOG_LEVEL
4343
handlers:
4444
- stdout

0 commit comments

Comments
 (0)