feat: Bootstrapping admin password and API key, and API commands #16

Merged
dannygroenewegen merged 1 commits from eCommons/rauthy:automation into main 2026-05-14 14:18:50 +00:00
10 changed files with 396 additions and 1 deletions

View File

@ -28,3 +28,24 @@ SECRET_HQL_API_VERSION=v1
#SMTP_USERNAME=
#SECRET_SMTP_PASSWORD_VERSION=v1
#SMTP_STARTTLS_ONLY=true
# Bootstrap admin account (only on first deploy)
# Generate admin bootstrap hash: abra app cmd --local generate_bootstrap_admin_password
#COMPOSE_FILE="$COMPOSE_FILE:compose.bootstrapadmin.yml"
#SECRET_ADMIN_PWHASH_VERSION=v1 # generate=false
# API key
# When set before first deploy, rauthy will bootstrap an API key with the Base64 encoded JSON access rights.
decentral1se marked this conversation as resolved Outdated

We generally have a convention of X_ENABLED=1/0 to turn stuff on/off. See other recipes for how that is handled. It is a more explicit way of configuring the recipe which I appreciate. Up to you if you want to go that route.

We generally have a convention of `X_ENABLED=1/0` to turn stuff on/off. See other recipes for how that is handled. It is a more explicit way of configuring the recipe which I appreciate. Up to you if you want to go that route.

I've seen that, but it's not applied everywhere. I feel that's a bit redundant with uncommenting compose.x.yml (also because uncommenting .yml and setting X_ENABLED=0 doesn't always fully disable in every implementation), but no strong preference. It makes sense to at least be consistent within a recipe and compose.smtp.yml in this recipe also uses it. So I add an X_ENABLED env for every compose.x.yml, right?

I've seen that, but it's not applied everywhere. I feel that's a bit redundant with uncommenting compose.x.yml (also because uncommenting .yml and setting X_ENABLED=0 doesn't always fully disable in every implementation), but no strong preference. It makes sense to at least be consistent within a recipe and compose.smtp.yml in this recipe also uses it. So I add an X_ENABLED env for every compose.x.yml, right?

Ah, in my mind the X_ENABLED was per-feature, not per-compose variant 😖

Maybe we can just slowly back away from this and leave it as-is 🙃

Ah, in my mind the `X_ENABLED` was per-feature, not per-compose variant 😖 Maybe we can just slowly back away from this and leave it as-is 🙃

I feel that's a bit redundant with uncommenting compose.x.yml (also because uncommenting .yml and setting X_ENABLED=0 doesn't always fully disable in every implementation)

Agreed, fine to drop X_ENABLED in general, certainly fine to not do it with this change.

> I feel that's a bit redundant with uncommenting compose.x.yml (also because uncommenting .yml and setting X_ENABLED=0 doesn't always fully disable in every implementation) Agreed, fine to drop `X_ENABLED` in general, certainly fine to not do it with this change.
# After first deploy, api_secret is also used by abra.sh functions (create_clients, create_groups)
#COMPOSE_FILE="$COMPOSE_FILE:compose.api.yml"
#SECRET_API_SECRET_VERSION=v1 # length=64
dannygroenewegen marked this conversation as resolved Outdated

I would do BOOTSTRAP_API_KEY= to drop the example which just needs to be deleted by every operator in the .env who uses it? If you want to include an example of a encoded key, that can be done in the README.md?

I would do `BOOTSTRAP_API_KEY=` to drop the example which just needs to be deleted by every operator in the `.env` who uses it? If you want to include an example of a encoded key, that can be done in the `README.md`?

I used the same naming as Rauthy internally uses and API_KEY is a bit confusing since it's not really a key. From your comment, I'm not sure if you might also be assuming it's a secret, so ignore the following if you got that part: The value in API_KEY is a Base64 encoded JSON with the access rights that should be given to the API key that will be bootstrapped.

The value I put here is Base64 of api.key.example.json, which is read and create rights for clients, groups and roles. Basically, the minimal access rights you need for using the provided abra.sh functions. I feel that's a fair value to put as default? But also happy to put it in the readme if it's confusing to have something here that looks like a secret?

Another option would be to rename this to e.g. API_BASE64_ACCESS_RIGHTS. Maybe that avoids the confusion of thinking it's a default key? Then it's named differently from Rauthy's own naming, but if you get to that layer, you can probably unconfuse yourself with the docs.

I used the same naming as Rauthy internally uses and API_KEY is a bit confusing since it's not really a key. From your comment, I'm not sure if you might also be assuming it's a secret, so ignore the following if you got that part: The value in API_KEY is a Base64 encoded JSON with the access rights that should be given to the API key that will be bootstrapped. The value I put here is Base64 of api.key.example.json, which is read and create rights for clients, groups and roles. Basically, the minimal access rights you need for using the provided abra.sh functions. I feel that's a fair value to put as default? But also happy to put it in the readme if it's confusing to have something here that looks like a secret? Another option would be to rename this to e.g. API_BASE64_ACCESS_RIGHTS. Maybe that avoids the confusion of thinking it's a default key? Then it's named differently from Rauthy's own naming, but if you get to that layer, you can probably unconfuse yourself with the docs.

@dannygroenewegen I think moving the explanatory comment one line down could help clarify that ewog... is the "read and create rights…" default.

Another option would be to rename this to e.g. API_BASE64_ACCESS_RIGHTS.

This also seems fine; maybe putting a comment that it's set to BOOTSTRAP_API_KEY could help avoid the naming-change confusion?

Fine with whichever solution though really.

@dannygroenewegen I think moving the explanatory comment one line down could help clarify that `ewog...` _is_ the "read and create rights…" default. > Another option would be to rename this to e.g. API_BASE64_ACCESS_RIGHTS. This also seems fine; maybe putting a comment that it's set to `BOOTSTRAP_API_KEY` could help avoid the naming-change confusion? Fine with whichever solution though really.

Good ideas. I will move the comment and rename the env. And want to test again afterwards, might take one or two weeks because of holidays.

Good ideas. I will move the comment and rename the env. And want to test again afterwards, might take one or two weeks because of holidays.
# Default value for API_BASE64_ACCESS (ewog...) is read and create rights on Clients, Roles and Groups. See file api.key.example.json and https://sebadob.github.io/rauthy/config/bootstrap.html#api-key
# In rauthy this value is stored in the config as API_KEY
#API_BASE64_ACCESS_RIGHTS="ewogICJuYW1lIjoiYm9vdHN0cmFwIiwKICAiZXhwIjpudWxsLAogICJhY2Nlc3MiOlt7CiAgICAgICJncm91cCI6IkNsaWVudHMiLAogICAgICAiYWNjZXNzX3JpZ2h0cyI6WyJyZWFkIiwiY3JlYXRlIl0KICAgIH0sewogICAgICAiZ3JvdXAiOiJSb2xlcyIsCiAgICAgICJhY2Nlc3NfcmlnaHRzIjpbInJlYWQiLCJjcmVhdGUiXQogICAgfSx7CiAgICAgICJncm91cCI6ICJHcm91cHMiLAogICAgICAiYWNjZXNzX3JpZ2h0cyI6WyJyZWFkIiwiY3JlYXRlIl0KICAgIH0sewogICAgICAiZ3JvdXAiOiAiU2VjcmV0cyIsCiAgICAgICJhY2Nlc3NfcmlnaHRzIjpbInJlYWQiXQogICAgfQogIF0KfQo="
# Nextcloud OIDC integration
#COMPOSE_FILE="$COMPOSE_FILE:compose.nextcloud.yml"
#SECRET_NEXTCLOUD_SEC_VERSION=v1 # length=5 prefix=Empty- This needs a value before deployment, but it will be set to the actual secret after the OIDC client is created.
#NEXTCLOUD_CLIENT_NAME="Nextcloud"
#NEXTCLOUD_REDIRECT_URI="https://nextcloud.example.com/apps/user_oidc/code"

View File

@ -38,6 +38,86 @@ mistakenly rate limited based on internal ipv4 addresses (e.g. `10.0.0.6`).
COMPOSE_FILE="$COMPOSE_FILE:compose.host.yml"
```
### Bootstrap admin password
By default, rauthy generates a random admin password and prints it to the logs on first deploy. If you want to set a known password upfront, you can bootstrap it before the first deploy.
Requires `argon2` on your local machine.
1. With `abra app config <app>`, configure the following envs:
```
COMPOSE_FILE="$COMPOSE_FILE:compose.bootstrapadmin.yml"
SECRET_ADMIN_PWHASH_VERSION=v1
```
2. Generate and insert the admin password hash:
```
abra app cmd --local <app> generate_bootstrap_admin_password
```
3. Deploy: `abra app deploy <app>`
Rauthy will use the bootstrapped hash instead of generating a password.
### API key
The API key allows access to the Rauthy API, used for creating OIDC clients, groups, and roles.
#### Setup
1. With `abra app config <app>`, configure the following envs:
```
COMPOSE_FILE="$COMPOSE_FILE:compose.api.yml"
SECRET_API_SECRET_VERSION=v1
```
2. Generate the secret:
```
abra app secret generate <app> api_secret v1
```
3. When `API_BASE64_ACCESS_RIGHTS` and `api_secret` are set before first deployment, Rauthy will bootstrap an API key with the access rights as configured in `API_BASE64_ACCESS_RIGHTS`. The default value in `.env.sample` grants read and create rights on Clients, Roles, and Groups. See the [rauthy bootstrap docs](https://sebadob.github.io/rauthy/config/bootstrap.html#api-key) for the JSON schema. If `API_BASE64_ACCESS_RIGHTS` is empty or set after first deployment, no API key is bootstrapped and you'll need to create one manually in the admin UI with secret `api_secret` to be used by the abra.sh functions.
#### Available commands
All commands require the API key to be set up and the app to be running.
**`create_client <client_id> [insertsecret]`** — Creates a confidential OIDC client. Reads configuration from env vars prefixed with the uppercased client ID:
| Variable | Required | Default |
|---|---|---|
| `<ID>_CLIENT_NAME` | yes | — |
| `<ID>_REDIRECT_URI` | yes | — |
| `<ID>_ALLOWED_SCOPES` | no | `email openid profile groups` |
Without `insertsecret`, prints the generated client secret. With `insertsecret`, it inserts Rauthy's client secret in the app secret `<ID>_sec` (undeploying and redeploying the app automatically).
**`create_groups <group> [<group> ...]`** — Creates one or more groups.
**`create_roles <role> [<role> ...]`** — Creates one or more roles.
#### Example: Nextcloud OIDC integration
This sets up rauthy as an OIDC provider for a Nextcloud app. Requires the API key to be set up first.
1. With `abra app config <app>`, configure the following envs:
```
COMPOSE_FILE="$COMPOSE_FILE:compose.nextcloud.yml"
SECRET_NEXTCLOUD_SEC_VERSION=v1
NEXTCLOUD_CLIENT_NAME="Nextcloud"
NEXTCLOUD_REDIRECT_URI="https://nextcloud.example.com/apps/user_oidc/code"
```
2. Generate a placeholder secret (required before deploy; it will be replaced after client creation):
```
abra app secret generate <app> nextcloud_sec v1
```
3. Deploy: `abra app deploy <app>`
4. Create the OIDC client in rauthy and insert the generated client secret:
```
abra app cmd <app> create_client nextcloud insertsecret
```
This undeploys the app, replaces the `nextcloud_sec` Docker secret with the real client secret, and redeploys.
5. Configure Nextcloud's OIDC provider (via the `user_oidc` app, see [Nextcloud user_oidc docs](https://git.coopcloud.tech/coop-cloud/nextcloud#how-do-i-enable-openid-connect-oidc-providers)) with:
- **Discovery endpoint**: `https://<rauthy-domain>/.well-known/openid-configuration`
- **Client ID**: `nextcloud`
- **Client secret**: the value inserted above (can also be view in Rauthy Admin UI)
### Encryption key rotation
This recipe supports encryption key rotation as described in [the docs](https://sebadob.github.io/rauthy/config/encryption.html). To rotate keys the first time:

232
abra.sh
View File

@ -1,6 +1,26 @@
set -e
export CONFIG_TOML_VERSION=v4
export CONFIG_TOML_VERSION=v5
generate_bootstrap_admin_password() {
if ! command -v argon2 &> /dev/null; then
echo "ERROR: 'argon2' CLI not found. Install it (e.g. 'apt install argon2')"
exit 1
fi
PASSWORD="$(openssl rand -base64 24)"
SALT="$(openssl rand -base64 24)"
HASH="$(echo -n "$PASSWORD" | argon2 "$SALT" -id -t 3 -m 16 -p 2 -l 32 -e)"
if abra app secret insert -C "$APP_NAME" admin_pwhash v1 "$HASH"; then
echo "Generated admin password:"
echo "$PASSWORD"
echo "WARNING: password is NOT shown again, please save it NOW"
else
echo "Failed to insert admin hash."
exit 1
fi
}
generate_enc_keys() {
KEY_A="$(openssl rand -base64 32)"
@ -11,3 +31,213 @@ generate_enc_keys() {
echo " enc_keys_a $KEY_A"
echo " enc_keys_b $KEY_B"
}
# Reads a Docker Swarm secret value from the running container
# Requires jq locally and SSH access to the server.
# Usage: get_secret <secret_name>
get_secret() {
decentral1se marked this conversation as resolved Outdated

It's crazy how much work we have to do to work around swarm secrets 🤯

(Feel free to resolve this, just a passing reflection 🙃)

It's crazy how much work we have to do to work around swarm secrets 🤯 (Feel free to resolve this, just a passing reflection 🙃)
local SECRET_NAME="$1"
if ! command -v jq &>/dev/null; then
echo "ERROR: jq is required. Install with: apt install jq" >&2
exit 1
fi
local SERVER
SERVER=$(abra app ls -m | jq -r --arg domain "$APP_NAME" '[.[].apps[] | select(.domain == $domain) | .server] | first' 2>/dev/null)
if [ -z "$SERVER" ] || [ "$SERVER" = "null" ]; then
echo "ERROR: could not determine server for app '$APP_NAME'" >&2
exit 1
fi
local MATCH
MATCH=$(ssh "$SERVER" "
docker stack services ${STACK_NAME} --format '{{.Name}}' | while read svc; do
CID=\$(docker ps --no-trunc -q --filter \"name=\${svc}\" | head -1)
[ -z \"\$CID\" ] && continue
docker service inspect \"\$svc\" --format '{{json .Spec.TaskTemplate.ContainerSpec.Secrets}}' | jq -r --arg cid \"\$CID\" '.[]? | .SecretID + \" \" + \$cid + \" \" + .SecretName'
done
" 2>/dev/null | grep " ${STACK_NAME}_${SECRET_NAME}_" | head -1)
if [ -z "$MATCH" ]; then
echo "ERROR: secret '$SECRET_NAME' not found in stack '$STACK_NAME'" >&2
exit 1
fi
local SECRET_ID CID
SECRET_ID=$(echo "$MATCH" | awk '{print $1}')
CID=$(echo "$MATCH" | awk '{print $2}')
local VALUE
VALUE=$(ssh "$SERVER" "cat /var/lib/docker/containers/${CID}/mounts/secrets/${SECRET_ID} 2>/dev/null || sudo cat /var/lib/docker/containers/${CID}/mounts/secrets/${SECRET_ID} 2>/dev/null")
if [ -z "$VALUE" ]; then
echo "ERROR: could not read value for secret '$SECRET_NAME'" >&2
exit 1
fi
printf '%s' "$VALUE"
}
# Usage: rauthy_api_request <method> <path> [json_body]
# Sets globals API_HTTP_STATUS and API_BODY.
rauthy_api_request() {
local METHOD="$1" ENDPOINT="$2" PAYLOAD="${3:-}"
if [ -z "$API_SECRET" ]; then
API_SECRET=$(get_secret api_secret)
fi
local AUTH_HEADER
AUTH_HEADER=$(printf 'Authorization: API-Key bootstrap$%s' "$API_SECRET")
local ARGS=(-s -w "\n%{http_code}" -X "$METHOD" -H "$AUTH_HEADER")
[ -n "$PAYLOAD" ] && ARGS+=(-H "Content-Type: application/json" -d "$PAYLOAD")
local RESPONSE
RESPONSE=$(curl "${ARGS[@]}" "https://${DOMAIN}/auth/v1${ENDPOINT}")
API_HTTP_STATUS=$(echo "$RESPONSE" | tail -1)
API_BODY=$(echo "$RESPONSE" | sed '$d')
}
# Creates an OIDC client in Rauthy and prints the client secret.
# Usage: create_client <client_id> [insertsecret]
# Reads config from env vars prefixed with uppercased client_id:
# <ID>_CLIENT_NAME (required)
# <ID>_REDIRECT_URI (required)
# <ID>_ALLOWED_SCOPES (optional, default: 'email openid profile groups')
# With 'insertsecret': undeploys APP_NAME, replaces the Docker secret, then redeploys.
# Example: NEXTCLOUD_CLIENT_NAME="Nextcloud" NEXTCLOUD_REDIRECT_URI="https://..." create_client nextcloud
create_client() {
local CLIENT_ID="$1"
local MODE="$2"
if [ -z "$CLIENT_ID" ]; then
echo "ERROR: no client_id; Usage: create_client <client_id> [insertsecret]" >&2
exit 1
fi
local PREFIX
PREFIX=$(echo "$CLIENT_ID" | tr '[:lower:]' '[:upper:]')
local CLIENT_NAME REDIRECT_URI ALLOWED_SCOPES
CLIENT_NAME=$(eval "echo \"\${${PREFIX}_CLIENT_NAME}\"")
REDIRECT_URI=$(eval "echo \"\${${PREFIX}_REDIRECT_URI}\"")
ALLOWED_SCOPES=$(eval "echo \"\${${PREFIX}_ALLOWED_SCOPES:-email openid profile groups}\"")
if [ -z "$CLIENT_NAME" ] || [ -z "$REDIRECT_URI" ]; then
echo "ERROR: ${PREFIX}_CLIENT_NAME and ${PREFIX}_REDIRECT_URI must be set" >&2
exit 1
fi
if ! command -v jq &>/dev/null; then
echo "ERROR: jq is required. Install with: apt install jq" >&2
exit 1
fi
rauthy_api_request GET "/clients/${CLIENT_ID}"
if [ "$API_HTTP_STATUS" = "200" ]; then
echo "Client '${CLIENT_ID}' already exists, skipping creation."
else
local PAYLOAD
PAYLOAD=$(jq -n \
--arg id "$CLIENT_ID" \
--arg name "$CLIENT_NAME" \
--arg redirect_uris "$REDIRECT_URI" \
--arg allowed_scopes "$ALLOWED_SCOPES" \
'$ARGS.named | .redirect_uris = [.redirect_uris] | .allowed_scopes = (.allowed_scopes | split(" ")) | .confidential = true')
rauthy_api_request POST "/clients" "$PAYLOAD"
if [ "$API_HTTP_STATUS" != "200" ] && [ "$API_HTTP_STATUS" != "201" ]; then
echo "ERROR: failed to create client '${CLIENT_ID}' (HTTP ${API_HTTP_STATUS}): ${API_BODY}" >&2
exit 1
fi
fi
rauthy_api_request POST "/clients/${CLIENT_ID}/secret"
if [ "$API_HTTP_STATUS" != "200" ] && [ "$API_HTTP_STATUS" != "201" ]; then
echo "ERROR: failed to fetch secret for client '${CLIENT_ID}' (HTTP ${API_HTTP_STATUS}): ${API_BODY}" >&2
exit 1
fi
local CLIENT_SECRET
CLIENT_SECRET=$(echo "$API_BODY" | jq -r '.secret // empty')
if [ -z "$CLIENT_SECRET" ]; then
echo "ERROR: no secret in API response for '${CLIENT_ID}'" >&2
echo "Response was: ${API_BODY}" >&2
exit 1
fi
if [ "$MODE" = "insertsecret" ]; then
echo "Undeploying '${APP_NAME}' to replace secret '${CLIENT_ID}_sec'"
abra --no-input app undeploy "$APP_NAME" || true
abra app secret remove -C "$APP_NAME" "${CLIENT_ID}_sec" || true
if printf '%s' "$CLIENT_SECRET" | abra app secret insert -C "$APP_NAME" "${CLIENT_ID}_sec" v1; then
echo "Secret '${CLIENT_ID}_sec' inserted, redeploying '${APP_NAME}'..."
else
echo "ERROR: failed to insert secret '${CLIENT_ID}_sec'; redeploying app" >&2
fi
abra --no-input app deploy -C "$APP_NAME" || true
#rauthy doesn't have a healthcheck, wait 5 seconds for startup
sleep 5
else
echo "Client '${CLIENT_ID}' created. Secret: ${CLIENT_SECRET}"
fi
}
# Creates one or more groups in Rauthy.
# Usage: create_groups <group_name> [<group_name> ...]
create_groups() {
if ! command -v jq &>/dev/null; then
echo "ERROR: jq is required. Install with: apt install jq" >&2
exit 1
fi
rauthy_api_request GET "/groups"
local EXISTING_GROUPS
if [ "$API_HTTP_STATUS" = "200" ]; then
EXISTING_GROUPS=$(echo "$API_BODY" | jq -r '.[].name // empty')
fi
for GROUP_NAME in "$@"; do
if echo "$EXISTING_GROUPS" | grep -qx "$GROUP_NAME"; then
echo "Group '${GROUP_NAME}' already exists, skipping"
continue
fi
rauthy_api_request POST "/groups" "$(jq -n --arg group "$GROUP_NAME" '$ARGS.named')"
if [ "$API_HTTP_STATUS" != "200" ] && [ "$API_HTTP_STATUS" != "201" ]; then
echo "ERROR: failed to create group '${GROUP_NAME}' (HTTP ${API_HTTP_STATUS}): ${API_BODY}" >&2
exit 1
fi
echo "Created group '${GROUP_NAME}'"
done
}
# Creates one or more roles in Rauthy.
# Usage: create_roles <role_name> [<role_name> ...]
create_roles() {
if ! command -v jq &>/dev/null; then
echo "ERROR: jq is required. Install with: apt install jq" >&2
exit 1
fi
rauthy_api_request GET "/roles"
local EXISTING_ROLES
EXISTING_ROLES=$(echo "$API_BODY" | jq -r '.[].name // empty')
for ROLE_NAME in "$@"; do
if echo "$EXISTING_ROLES" | grep -qx "$ROLE_NAME"; then
echo "Role '${ROLE_NAME}' already exists, skipping"
continue
fi
rauthy_api_request POST "/roles" "$(jq -n --arg role "$ROLE_NAME" '$ARGS.named')"
if [ "$API_HTTP_STATUS" != "200" ] && [ "$API_HTTP_STATUS" != "201" ]; then
echo "ERROR: failed to create role '${ROLE_NAME}' (HTTP ${API_HTTP_STATUS}): ${API_BODY}" >&2
exit 1
fi
echo "Created role '${ROLE_NAME}'"
done
}

10
alaconnect.yml Normal file
View File

@ -0,0 +1,10 @@
nextcloud:
uncomment:
- compose.nextcloud.yml
- SECRET_NEXTCLOUD_SEC_VERSION
- NEXTCLOUD_REDIRECT_URI
- NEXTCLOUD_CLIENT_NAME
initial-hooks:
- local create_client nextcloud insertsecret
shared_secrets:
user_oidc_secret: nextcloud_sec

18
api.key.example.json Normal file
View File

@ -0,0 +1,18 @@
{
"name":"bootstrap",
"exp":null,
"access":[{
"group":"Clients",
"access_rights":["read","create"]
},{
"group":"Roles",
"access_rights":["read","create"]
},{
"group": "Groups",
"access_rights":["read","create"]
},{
"group": "Secrets",
"access_rights":["read"]
}
]
}

11
compose.api.yml Normal file
View File

@ -0,0 +1,11 @@
services:
app:
environment:
- API_KEY
secrets:
- api_secret
secrets:
api_secret:
name: ${STACK_NAME}_api_secret_${SECRET_API_SECRET_VERSION}
external: true

View File

@ -0,0 +1,9 @@
services:
app:
secrets:
- admin_pwhash
secrets:
admin_pwhash:
name: ${STACK_NAME}_admin_pwhash_${SECRET_ADMIN_PWHASH_VERSION}
external: true

9
compose.nextcloud.yml Normal file
View File

@ -0,0 +1,9 @@
services:
app:
secrets:
- nextcloud_sec
secrets:
nextcloud_sec:
name: ${STACK_NAME}_nextcloud_sec_${SECRET_NEXTCLOUD_SEC_VERSION}
external: true

View File

@ -3,6 +3,9 @@
[bootstrap]
admin_email = '{{ env "ADMIN_EMAIL" }}'
pasword_argon2id = '{{ secret "admin_pwhash" }}'
api_key = '{{ env "API_BASE64_ACCESS_RIGHTS" }}'
api_key_secret = '{{ secret "api_secret" }}'
[cluster]
node_id = 1

4
release/next Normal file
View File

@ -0,0 +1,4 @@
* Added bootstrapping admin password and API key
* Added abra.sh functions for creating clients, groups and roles with the Rauthy API
* Documentation and example for Nextcloud integration