update
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# Authelia configuration for the BookStack MCP server.
|
||||
#
|
||||
# Merge these blocks into your existing configuration.yml. Tested against
|
||||
# Authelia 4.39+, where ID Token claims changed (see the claims_policies note).
|
||||
#
|
||||
# Generate the client id and secret first:
|
||||
#
|
||||
# docker run --rm authelia/authelia:latest \
|
||||
# authelia crypto rand --length 72 --charset rfc3986
|
||||
#
|
||||
# docker run --rm authelia/authelia:latest \
|
||||
# authelia crypto hash generate pbkdf2 --variant sha512 \
|
||||
# --random --random.length 72 --random.charset rfc3986
|
||||
#
|
||||
# The second command prints BOTH a "Random Password" and a "Digest".
|
||||
# Digest -> client_secret below
|
||||
# Random Password -> OIDC_CLIENT_SECRET in /etc/bookstack-mcp/env
|
||||
# Do not put the digest in the MCP server's env file.
|
||||
|
||||
identity_providers:
|
||||
oidc:
|
||||
# -----------------------------------------------------------------------
|
||||
# 1. Claims policy
|
||||
#
|
||||
# Authelia 4.39 stopped putting non-standard claims in the ID Token by
|
||||
# default. The MCP server reads identity from the ID Token (because
|
||||
# Authelia issues opaque access tokens), so groups and preferred_username
|
||||
# have to be put back explicitly. Without this, MCP_REQUIRED_GROUPS will
|
||||
# never match and every tool call gets denied.
|
||||
# -----------------------------------------------------------------------
|
||||
claims_policies:
|
||||
mcp_claims:
|
||||
id_token:
|
||||
- 'groups'
|
||||
- 'preferred_username'
|
||||
- 'email'
|
||||
- 'email_verified'
|
||||
- 'name'
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# 2. Lifespan
|
||||
#
|
||||
# Authelia's default refresh_token lifespan is 90m. A claude.ai connector
|
||||
# that sits idle longer than that has to be re-authorised by hand, which
|
||||
# gets old fast. A long refresh token with a short access token is the
|
||||
# right shape for a background integration.
|
||||
# -----------------------------------------------------------------------
|
||||
lifespans:
|
||||
custom:
|
||||
mcp:
|
||||
access_token: '1h'
|
||||
id_token: '1h'
|
||||
refresh_token: '30d'
|
||||
|
||||
clients:
|
||||
- client_id: 'REPLACE_WITH_GENERATED_ID'
|
||||
client_name: 'BookStack MCP'
|
||||
client_secret: 'REPLACE_WITH_PBKDF2_DIGEST'
|
||||
public: false
|
||||
|
||||
# one_factor if you don't want to be prompted for 2FA on reconnect.
|
||||
authorization_policy: 'two_factor'
|
||||
|
||||
claims_policy: 'mcp_claims'
|
||||
lifespan: 'mcp'
|
||||
|
||||
# Claude re-authorises on token refresh. Explicit consent every time is
|
||||
# painful; pre-configured remembers the grant for the given duration.
|
||||
consent_mode: 'pre-configured'
|
||||
pre_configured_consent_duration: '1 month'
|
||||
|
||||
require_pkce: true
|
||||
pkce_challenge_method: 'S256'
|
||||
|
||||
redirect_uris:
|
||||
- 'https://mcp.example.com/auth/callback'
|
||||
|
||||
# offline_access is what gets you a refresh token. Without it the
|
||||
# connector dies when the access token expires.
|
||||
scopes:
|
||||
- 'openid'
|
||||
- 'profile'
|
||||
- 'email'
|
||||
- 'groups'
|
||||
- 'offline_access'
|
||||
|
||||
grant_types:
|
||||
- 'authorization_code'
|
||||
- 'refresh_token'
|
||||
|
||||
response_types:
|
||||
- 'code'
|
||||
|
||||
# Authelia's default and what the spec requires. Leave it alone unless
|
||||
# you see 'invalid_client' at the token endpoint, in which case try
|
||||
# 'client_secret_post'.
|
||||
token_endpoint_auth_method: 'client_secret_basic'
|
||||
|
||||
# Both are already the default; stated explicitly because they matter.
|
||||
# 'none' means access tokens stay opaque rather than being JWTs, which
|
||||
# is exactly why the MCP server needs OIDC_VERIFY_ID_TOKEN=true.
|
||||
access_token_signed_response_alg: 'none'
|
||||
userinfo_signed_response_alg: 'none'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Access control bypass <-- the one everybody misses
|
||||
#
|
||||
# If mcp.example.com is behind Authelia's forward-auth on your reverse proxy,
|
||||
# Claude will be served an HTML login page instead of the MCP endpoint and the
|
||||
# connector will fail with a parse error or a redirect loop. Claude cannot do
|
||||
# cookie-based forward auth; the MCP server runs its own OAuth flow against
|
||||
# Authelia instead. So the host must bypass forward-auth entirely.
|
||||
#
|
||||
# This is not a security hole: the MCP server refuses every unauthenticated
|
||||
# request itself, and Authelia is still the thing deciding who gets a token.
|
||||
# ---------------------------------------------------------------------------
|
||||
access_control:
|
||||
rules:
|
||||
- domain: 'mcp.example.com'
|
||||
policy: 'bypass'
|
||||
@@ -0,0 +1,43 @@
|
||||
[Unit]
|
||||
Description=BookStack MCP Server
|
||||
Documentation=https://github.com/your/bookstack-mcp
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
User=bookstack-mcp
|
||||
Group=bookstack-mcp
|
||||
|
||||
EnvironmentFile=/etc/bookstack-mcp/env
|
||||
WorkingDirectory=/opt/bookstack-mcp
|
||||
ExecStart=/opt/bookstack-mcp/venv/bin/python -m bookstack_mcp
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
TimeoutStopSec=20s
|
||||
|
||||
# --- Hardening ---
|
||||
# These all work in an unprivileged Proxmox/LXD container. If the service fails
|
||||
# with a 226/NAMESPACE or 227/SECCOMP status on an older kernel, comment out the
|
||||
# block below and it will start fine -- it only reduces defence in depth.
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RemoveIPC=yes
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
CapabilityBoundingSet=
|
||||
AmbientCapabilities=
|
||||
|
||||
# Journald captures stdout/stderr: journalctl -u bookstack-mcp -f
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=bookstack-mcp
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Executable
+150
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Install the BookStack MCP server into a Debian/Ubuntu LXC container.
|
||||
# Run as root, from inside the container, in the directory containing pyproject.toml:
|
||||
#
|
||||
# bash deploy/lxc/install.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
APP_DIR=/opt/bookstack-mcp
|
||||
CONF_DIR=/etc/bookstack-mcp
|
||||
SVC_USER=bookstack-mcp
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
|
||||
die() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# Find the project root. Normally this script lives at deploy/lxc/install.sh so
|
||||
# the root is two levels up, but people often copy the tree around flat, so
|
||||
# check the obvious candidates rather than assuming.
|
||||
find_src() {
|
||||
local candidate
|
||||
for candidate in "$SCRIPT_DIR/../.." "$SCRIPT_DIR/.." "$SCRIPT_DIR" "$PWD"; do
|
||||
if [[ -f "$candidate/pyproject.toml" && -d "$candidate/bookstack_mcp" ]]; then
|
||||
(cd "$candidate" && pwd)
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! SRC_DIR="$(find_src)"; then
|
||||
die "Can't find the project source (needs pyproject.toml and a bookstack_mcp/ directory).
|
||||
Looked in: $SCRIPT_DIR/../.. , $SCRIPT_DIR/.. , $SCRIPT_DIR , $PWD
|
||||
|
||||
Get the whole project into the container, then run the script from its root:
|
||||
git clone <your-repo> /opt/src/bookstack-mcp
|
||||
cd /opt/src/bookstack-mcp
|
||||
bash deploy/lxc/install.sh"
|
||||
fi
|
||||
|
||||
[[ $EUID -eq 0 ]] || die "Run this as root (or with sudo)."
|
||||
|
||||
# The script copies SRC_DIR into APP_DIR, so they must not be the same place.
|
||||
if [[ "$SRC_DIR" == "$APP_DIR" ]]; then
|
||||
die "The source is $APP_DIR, which is also the install target.
|
||||
Keep the source somewhere else and let the installer copy it in:
|
||||
mkdir -p /opt/src && mv $APP_DIR /opt/src/bookstack-mcp
|
||||
cd /opt/src/bookstack-mcp && bash deploy/lxc/install.sh"
|
||||
fi
|
||||
|
||||
log "Installing from $SRC_DIR"
|
||||
|
||||
# --- Python version check -------------------------------------------------
|
||||
# Needs 3.11+. Debian 12/13 and Ubuntu 24.04 are fine; Debian 11 and
|
||||
# Ubuntu 22.04 ship something older and need a newer python installed first.
|
||||
log "Checking Python version"
|
||||
if ! command -v python3 >/dev/null; then
|
||||
die "python3 not installed. Try: apt install -y python3 python3-venv"
|
||||
fi
|
||||
PY_OK=$(python3 -c 'import sys; print(1 if sys.version_info >= (3,11) else 0)')
|
||||
if [[ "$PY_OK" != "1" ]]; then
|
||||
die "Python $(python3 -V | cut -d' ' -f2) found, but 3.11+ is required.
|
||||
Debian 12+/Ubuntu 24.04+ work out of the box. On older releases install a
|
||||
newer python3 first (e.g. via deadsnakes) and re-run."
|
||||
fi
|
||||
|
||||
# --- Dependencies ---------------------------------------------------------
|
||||
log "Installing system packages"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
# A single broken third-party repo shouldn't abort the whole install, so the
|
||||
# update is advisory; only the install itself is allowed to fail the script.
|
||||
apt-get update -qq || log "apt update reported errors, continuing"
|
||||
apt-get install -y -qq python3-venv python3-pip ca-certificates >/dev/null
|
||||
|
||||
# --- Service user ---------------------------------------------------------
|
||||
if ! id -u "$SVC_USER" >/dev/null 2>&1; then
|
||||
log "Creating service user $SVC_USER"
|
||||
useradd --system --home-dir "$APP_DIR" --shell /usr/sbin/nologin "$SVC_USER"
|
||||
fi
|
||||
|
||||
# --- Application ----------------------------------------------------------
|
||||
log "Installing application to $APP_DIR"
|
||||
mkdir -p "$APP_DIR"
|
||||
rm -rf "$APP_DIR/bookstack_mcp"
|
||||
cp -r "$SRC_DIR/bookstack_mcp" "$APP_DIR/"
|
||||
cp "$SRC_DIR/pyproject.toml" "$APP_DIR/"
|
||||
|
||||
if [[ ! -d "$APP_DIR/venv" ]]; then
|
||||
log "Creating virtualenv"
|
||||
python3 -m venv "$APP_DIR/venv"
|
||||
fi
|
||||
|
||||
log "Installing Python dependencies (this takes a minute)"
|
||||
"$APP_DIR/venv/bin/pip" install --quiet --upgrade pip
|
||||
"$APP_DIR/venv/bin/pip" install --quiet "$APP_DIR"
|
||||
|
||||
chown -R "$SVC_USER:$SVC_USER" "$APP_DIR"
|
||||
|
||||
# --- Configuration --------------------------------------------------------
|
||||
mkdir -p "$CONF_DIR"
|
||||
if [[ ! -f "$CONF_DIR/env" ]]; then
|
||||
log "Creating $CONF_DIR/env from the template"
|
||||
# systemd EnvironmentFile does not understand quotes or inline comments the
|
||||
# way a shell does, so strip comments and blank lines out of the example.
|
||||
grep -vE '^\s*(#|$)' "$SRC_DIR/.env.example" > "$CONF_DIR/env"
|
||||
NEW_CONFIG=1
|
||||
else
|
||||
log "Keeping existing $CONF_DIR/env"
|
||||
NEW_CONFIG=0
|
||||
fi
|
||||
chown root:"$SVC_USER" "$CONF_DIR/env"
|
||||
chmod 640 "$CONF_DIR/env"
|
||||
|
||||
# --- systemd --------------------------------------------------------------
|
||||
log "Installing systemd unit"
|
||||
UNIT_SRC=""
|
||||
for candidate in "$SRC_DIR/deploy/lxc/bookstack-mcp.service" "$SCRIPT_DIR/bookstack-mcp.service"; do
|
||||
[[ -f "$candidate" ]] && { UNIT_SRC="$candidate"; break; }
|
||||
done
|
||||
[[ -n "$UNIT_SRC" ]] || die "Can't find bookstack-mcp.service"
|
||||
install -m 644 "$UNIT_SRC" /etc/systemd/system/bookstack-mcp.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable bookstack-mcp >/dev/null
|
||||
|
||||
echo
|
||||
log "Install complete."
|
||||
echo
|
||||
if [[ "$NEW_CONFIG" == "1" ]]; then
|
||||
cat <<EOF
|
||||
Next steps:
|
||||
|
||||
1. Edit the config and fill in your BookStack details:
|
||||
nano $CONF_DIR/env
|
||||
|
||||
At minimum: BOOKSTACK_URL, BOOKSTACK_TOKEN_ID, BOOKSTACK_TOKEN_SECRET.
|
||||
Generate a Claude Code token with: openssl rand -hex 32
|
||||
|
||||
2. Start it:
|
||||
systemctl start bookstack-mcp
|
||||
systemctl status bookstack-mcp
|
||||
journalctl -u bookstack-mcp -f
|
||||
|
||||
3. Verify locally (401 with a WWW-Authenticate header is the correct answer
|
||||
when auth is enabled):
|
||||
curl -i http://127.0.0.1:8080/mcp
|
||||
EOF
|
||||
else
|
||||
echo " systemctl restart bookstack-mcp"
|
||||
fi
|
||||
Reference in New Issue
Block a user