mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 04:06:42 +00:00
fix(web-app-moodle): set the cURL egress policy where the security helper reads it
Moodle's curl_security_helper refuses any URL resolving to a blocked host, and Moodle 5.3 ships 192.168.0.0/16 in its default list. container_extra_hosts._sso_pins pins auth.<onion> to the swarm manager's 192.168.244.10, so the helper blocked Moodle's own OIDC token exchange. curl::request() then returns the blocked-URL string as the body, utils::process_json_response() sees non-JSON and throws erroroidccall - the 'Error in OpenID Connect' notification the specs saw. The role already knew this and rewrote the list with admin/cli/cfg.php --set. That writes one database row from one container at deploy time, while the helper reads $CFG on every request across three replicas and the cron container, and the observed behaviour matched the default list rather than the role's. config.php is where get_config() looks first, ahead of the database and the MUC cache. Removing the old task is mandatory, not tidy: a value set in config.php lands in $CFG->config_php_settings and cfg.php then exits 4 with 'unable to change', which would abort the deploy. The request never left the container. openresty's log for the whole job carries no Moodle PHP user agent at all while the callback POST is present and the token POST is not. Negative control in the same stack: stalwart_webmail, PHP on glibc with the identical pin and resolv.conf, completed the very same token POST with 200 - so the pin resolves from PHP, the address is routable and the destination answers. Refuted with evidence: the dropped Secure cookie is not the mechanism, since the trace shows MoodleSession set without Secure and sent back on the callback, and the plugin tolerates the dropped auth_oidc_csrf by design; wwwroot matches; Keycloak authenticated and posted the code back. Residual worth acting on if this does not hold: host_is_blocked() also returns true when gethostbynamel() yields nothing, and the live curlsecurityblockedhosts row was unreadable because the rescue bundle carries no database. The Roundcube control makes that gate unlikely, so this attributes the block to the listed IP - but if the next onion run still shows erroroidccall, the resolution gate is the live one and the pin is what to chase. Values are byte-identical to what the removed task wrote, so clearnet behaviour is unchanged; the one new hard-fail is a stale .env/env without the two keys, which the role re-renders on every deploy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f2804e6662
commit
1590ec2c6f
5 files changed
+16
-20
No files matched your search
@@ -82,7 +82,9 @@ Remove the MCP client roles, or pin `mcp.enabled: false` for this role. The web-
|
||||
|
||||
Moodle ships a cURL blocklist that rejects every request to `10.0.0.0/8`, `172.16.0.0/12` and `192.168.0.0/16`, and only permits ports 80 and 443. Both settings gate `\core\files\curl_security_helper`, which `auth_oidc` consults for the Keycloak token exchange and `\core\http_client` consults for the AI provider request. The gateway answers on `192.168.80.0/24`, and on an onion node `container_extra_hosts` pins the Keycloak authority to the Docker bridge gateway, which `NETWORK_DOCKER_ADDRESS_POOLS` draws from `10.208.0.0/12`. With the upstream defaults the OIDC login obtains no token and the AI provider cannot reach the gateway.
|
||||
|
||||
`tasks/01_manager_ops.yml` therefore rewrites `curlsecurityblockedhosts` to `MOODLE_EGRESS_BLOCKED_HOSTS` and extends `curlsecurityallowedport` with the port of `MOODLE_LITELLM_CHAT_ENDPOINT` when the `litellm` service is enabled. Loopback, `0.0.0.0` and the cloud metadata address stay blocked; the RFC1918 ranges are opened. The Docker address pool and the bridge gateway that serves the pinned Keycloak authority both live inside them.
|
||||
`files/build/config.php` therefore assigns `$CFG->curlsecurityblockedhosts` from `MOODLE_EGRESS_BLOCKED_HOSTS` and `$CFG->curlsecurityallowedport` from `MOODLE_EGRESS_ALLOWED_PORTS`, which carries the port of `MOODLE_LITELLM_CHAT_ENDPOINT` when the `litellm` service is enabled. Both arrive as comma-separated `.env/env` values and are expanded to the newline-separated form `\core\files\curl_security_helper` expects. Loopback, `0.0.0.0` and the cloud metadata address stay blocked; the RFC1918 ranges are opened. The Docker address pool and the bridge gateway that serves the pinned Keycloak authority both live inside them.
|
||||
|
||||
The helper reads both values straight off `$CFG` on every request, so they belong in `config.php` rather than in the admin database: a deploy-time `admin/cli/cfg.php --set` writes a single row from one container, whereas each swarm replica and the cron container answer their own requests from this file. A setting carried in `config.php` also makes `admin/cli/cfg.php --set` exit 4, which is why no task writes these two names.
|
||||
|
||||
This is an accepted trade-off, not an oversight. Moodle offers no allowlist, so a single internal host cannot be permitted without unblocking the range it sits in. The cost is that any account holding a URL-fetching capability, such as `block/rss_client:myaddinstance` or a `repository/url` instance, can make the server issue requests to sibling containers. Grant those capabilities only to roles you trust with that reach, or keep the upstream blocklist and accept that SSO login and the AI surface do not work.
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ function moodle_env_bool(string $name): bool {
|
||||
return filter_var(moodle_env($name), FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
function moodle_env_lines(string $name): string {
|
||||
return implode(PHP_EOL, preg_split('/[\s,]+/', moodle_env($name), -1, PREG_SPLIT_NO_EMPTY));
|
||||
}
|
||||
|
||||
unset($CFG);
|
||||
global $CFG;
|
||||
$CFG = new stdClass();
|
||||
@@ -45,6 +49,9 @@ $CFG->routerconfigured = true;
|
||||
$CFG->reverseproxy = moodle_env_bool('MOODLE_REVERSEPROXY');
|
||||
$CFG->sslproxy = moodle_env_bool('MOODLE_SSLPROXY');
|
||||
|
||||
$CFG->curlsecurityblockedhosts = moodle_env_lines('MOODLE_EGRESS_BLOCKED_HOSTS');
|
||||
$CFG->curlsecurityallowedport = moodle_env_lines('MOODLE_EGRESS_ALLOWED_PORTS');
|
||||
|
||||
$_moodle_internal_host = moodle_env('MOODLE_INTERNAL_HOST');
|
||||
if (explode(':', $_SERVER['HTTP_HOST'] ?? '', 2)[0] === $_moodle_internal_host) {
|
||||
$_SERVER['HTTP_HOST'] = parse_url($CFG->wwwroot, PHP_URL_HOST);
|
||||
|
||||
@@ -74,17 +74,6 @@
|
||||
changed_when: moodle_admin_password.rc == 0
|
||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
- name: "🌐 Open Moodle's cURL egress policy for the deployment's own networks"
|
||||
ansible.builtin.shell: >
|
||||
container exec --user {{ MOODLE_RUNTIME_USER }} {{ MOODLE_CONTAINER_ADDRESS }}
|
||||
php {{ [MOODLE_CLI_DIR, 'cfg.php'] | path_join }}
|
||||
--name={{ item.name | quote }}
|
||||
--set={{ item.value | quote }}
|
||||
loop: "{{ MOODLE_EGRESS_SETTINGS }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
changed_when: false
|
||||
|
||||
- name: "📂 Include ownership settings tasks for moodle"
|
||||
include_tasks: 03_ownership.yml
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ MOODLE_DB_PREFIX=mdl_
|
||||
MOODLE_REVERSEPROXY=false
|
||||
MOODLE_SSLPROXY={{ lookup('tls', application_id, 'enabled') | string | lower }}
|
||||
|
||||
MOODLE_EGRESS_BLOCKED_HOSTS={{ MOODLE_EGRESS_BLOCKED_HOSTS | join(',') }}
|
||||
MOODLE_EGRESS_ALLOWED_PORTS={{ MOODLE_EGRESS_ALLOWED_PORTS }}
|
||||
|
||||
MOODLE_DEBUG={{ MODE_DEBUG | bool }}
|
||||
|
||||
MOODLE_SITE_NAME={{ MOODLE_SITE_TITLE | dotenv_quote }}
|
||||
|
||||
@@ -80,16 +80,11 @@ MOODLE_EGRESS_BLOCKED_HOSTS:
|
||||
- "localhost"
|
||||
- "169.254.169.254"
|
||||
- "0000::1"
|
||||
MOODLE_EGRESS_ALLOWED_PORTS:
|
||||
MOODLE_EGRESS_WEB_PORTS:
|
||||
- "80"
|
||||
- "443"
|
||||
MOODLE_EGRESS_NEWLINE: "\n"
|
||||
MOODLE_EGRESS_SETTINGS:
|
||||
- name: curlsecurityblockedhosts
|
||||
value: "{{ MOODLE_EGRESS_BLOCKED_HOSTS | join(MOODLE_EGRESS_NEWLINE) }}"
|
||||
- name: curlsecurityallowedport
|
||||
value: "{{ (MOODLE_EGRESS_ALLOWED_PORTS + ([MOODLE_LITELLM_CHAT_ENDPOINT | urlsplit('port') | string] if MOODLE_LITELLM_ENABLED | bool else [])) | join(MOODLE_EGRESS_NEWLINE)
|
||||
}}"
|
||||
MOODLE_EGRESS_ALLOWED_PORTS: "{{ (MOODLE_EGRESS_WEB_PORTS + ([MOODLE_LITELLM_CHAT_ENDPOINT | urlsplit('port') | string] if MOODLE_LITELLM_ENABLED | bool else []))
|
||||
| join(',') }}"
|
||||
|
||||
MOODLE_OBJECTFS_ENABLED: "{{ lookup('config', application_id, 'services.seaweedfs.enabled') }}"
|
||||
MOODLE_OBJECTFS_PLUGIN_COMMIT: "{{ lookup('config', application_id, 'addons.tool_objectfs.version') }}"
|
||||
|
||||
Reference in new issue
Block a user