A learner finishes a SCORM module and the player shows it as completed, yet Moodle leaves the activity (and therefore the course) incomplete, blocking certificates and compliance reporting. The cause is almost always a mismatch between the runtime status the SCORM package reports and the status the activity’s completion condition requires, compounded by the fact that course completion is recalculated on a schedule, not instantly. This guide explains the three layers of “complete” in Moodle, how to see what your package actually reported, and how to fix the mismatch. Paths and SQL are for Moodle 4.5.

Prefer to watch? Here is a short walkthrough of the completion mismatch and how to fix it:

The three layers of “complete”

The word “complete” means three different things at three different layers, and a learner is only finished when all three agree:

  1. The SCORM runtime status – what the package itself reports back to Moodle (for example cmi.core.lesson_status = completed).
  2. Activity completion – Moodle’s own decision about whether the SCORM activity is complete, based on the conditions you configured on that activity.
  3. Course completion – whether the course is complete, based on the activities and criteria you nominated, recalculated by a scheduled task.

A breakdown at any layer stops the chain. The most common failure is between layers 1 and 2.

Layer 1: what the SCORM package reports

SCORM 1.2 and AICC report a single field, cmi.core.lesson_status, which can be passed, completed, failed, incomplete, browsed, or not attempted.

SCORM 2004 splits this into two separate fields: cmi.completion_status (completed / incomplete) and cmi.success_status (passed / failed). This split matters: in Moodle, for a SCORM 2004 package the only status treated as complete is cmi.completion_status = completed. A package that reports success_status = passed but leaves completion_status at incomplete will never satisfy a completion-status condition.

The quickest way to see exactly what your package reported is in the interface: open the SCORM activity, go to Reports, click the learner’s attempt, and open Track details. The reported status is shown there. If you would rather query it directly, the tracking tables in Moodle 4.5 are:

SELECT a.userid, e.element, v.value
FROM mdl_scorm_scoes_value v
JOIN mdl_scorm_attempt a ON a.id = v.attemptid
JOIN mdl_scorm_element e ON e.id = v.elementid
WHERE a.scormid = ?
  AND e.element IN ('cmi.core.lesson_status', 'cmi.completion_status', 'cmi.success_status')
ORDER BY a.userid;

(These tables were restructured in Moodle 4.x; on 4.0 and earlier the equivalent data lived in a single mdl_scorm_scoes_track table.)

Layer 2: the SCORM activity’s completion conditions

A SCORM activity offers three completion conditions in the Activity completion section of its settings:

  • Require status – a set of checkboxes (Passed, Completed, Failed, Incomplete, Browsed, Not attempted). The activity is marked complete when the reported status matches at least one of the ticked statuses.
  • Require minimum score – the activity is complete only when the SCORM score reaches the value you set.
  • All SCOs must return completion status – every SCO in a multi-SCO package must report a status, not just the launch SCO.

This is where the mismatch lives. If your package reports completed but Require status has only Passed ticked, the condition is never met and the activity never completes, even though the player clearly showed “completed”. The reverse is equally common: a package reports passed, but only Completed is ticked.

You can confirm the resulting activity-completion state per learner:

SELECT cmc.userid, cmc.completionstate
FROM mdl_course_modules_completion cmc
JOIN mdl_course_modules cm ON cm.id = cmc.coursemoduleid
WHERE cm.instance = ?  -- the SCORM instance id
  AND cm.module = (SELECT id FROM mdl_modules WHERE name = 'scorm');

completionstate is 0 (incomplete), 1 (complete), 2 (complete with pass), or 3 (complete with fail).

Layer 3: course completion and the recalculation lag

Course completion is configured under Course > More > Course completion. To make the SCORM activity count, add it under Condition: Activity completion and check whether the overall condition requires all selected activities or any of them.

Two things commonly trip people here:

  • The SCORM activity is complete, but it was never added as a course-completion criterion, so the course can never complete on the back of it.
  • The activity is complete, but the course still shows incomplete for a while. Course completion is not recalculated the instant an activity completes; it is processed by the scheduled task \core\task\completion_regular_task. Until the next run (or the learner reloads the course), the course status lags behind the activity status.

You can see which activities the course depends on:

SELECT course, criteriatype, moduleinstance
FROM mdl_course_completion_criteria
WHERE course = ?;

A criteriatype of 4 is an activity-completion criterion; moduleinstance is the course-module id of the activity.

Fixing it

  1. Match the required status to what the package reports. Open the SCORM activity, go to Activity completion, and under Require status tick the status your package actually sends (confirmed in Track details or the query above). If you are unsure what it sends, tick both Passed and Completed; the learner only needs to satisfy one of them.
  2. For SCORM 2004 packages, ensure the package sets cmi.completion_status = completed. If it only sets success_status, the completion-status condition will not be met; either fix the package or base completion on the minimum score instead.
  3. Check the “All SCOs” condition. If the package has multiple SCOs (for example a menu plus content SCOs) and one never reports a status, untick All SCOs must return completion status or fix the package so every SCO reports one.
  4. Add the activity to course completion and confirm the any/all aggregation matches your intent.
  5. Allow for the recalculation lag, or trigger it. After fixing the conditions, course completion updates on the next cron run. To verify immediately, run cron (php admin/cli/cron.php) or have the learner reload the course.

Re-checking existing learners

Changing a completion condition does not always retro-apply cleanly to learners who already attempted the activity. After adjusting the condition, use Reports > Activity completion and Course completion to confirm the affected learners now show complete, and run cron so course completion catches up. For a learner whose attempt predates the fix, a fresh review of completion (or, in stubborn cases, re-marking the attempt) may be needed.

Related

If your problem is a numeric pass/fail rather than a status mismatch (a learner sees a passing score on screen but Moodle records a fail), that is a different failure mode caused by grade rounding. See our guide on SCORM and quiz grade rounding for that case.

Solin specializes in Moodle and Totara completion, SCORM, and certification workflows. Contact us if a completion problem is blocking your reporting.

Someone has told you to make your Moodle “GDPR compliant”, and most of what you find online is legal theory. This guide skips that and gives the concrete list of things you, as the administrator, actually set up and operate inside Moodle. There are five, and Moodle has a dedicated tool for each. Everything below lives under Site administration > Users > Privacy and policies, powered by two core tools, tool_dataprivacy and tool_policy. Paths and behavior are for Moodle 4.5. None of this is legal advice; it is the operational checklist that sits underneath whatever your DPO decides.

Prefer to watch? Here is a short walkthrough of the Moodle GDPR compliance setup:

1. Decide who is responsible: a Privacy Officer role

First, decide who handles data requests, because GDPR gives you a fixed deadline to respond and someone has to own that clock. Create a Privacy Officer role (base it on the Manager archetype) and grant it the data privacy capabilities your process needs, in particular tool/dataprivacy:managedatarequests and tool/dataprivacy:managedataregistry, plus the download and make-requests-for-children capabilities if you use them. Then assign it to a real person at site level. The step that is easy to miss: tick that role under Data privacy settings > Privacy officer so the holders are actually emailed when a request arrives, and turn on Contact the privacy officer. That second toggle does more than add a contact link: it is what makes the Export all of my personal data and Delete my account options appear on each user’s profile, under Privacy and policies. Leave it off (which is the default) and your users have no way to file their own request; only an admin or privacy officer can raise one on their behalf from the Data requests page. Sensible default: enable it, and name one privacy officer plus one backup so requests do not stall when someone is on leave. Skip all of this and a site admin can still process requests (Moodle does not lock you out), but with no named owner they are more likely to be missed or handled late, which is itself a compliance risk.

2. Declare why and how long: the data registry

The Data registry (/admin/tool/dataprivacy/dataregistry.php) is where you record, in Moodle’s own terms, what personal data you hold, why you are allowed to hold it, and how long you keep it. This is the documentation auditors and data protection officers ask for, and it also drives Moodle’s retention handling. It is built from two pieces: categories, which label a type of data, and purposes, which state the reason you process it and its retention period. You set a default category and purpose for the whole site and can override them lower down, per course category, course, activity, or block.

The real decisions live in the purpose. When you create one, you fill in:

  • Lawful basis (GDPR Art. 6): why you are legally allowed to process the data. For workplace training this is usually Legal obligation (you must be able to evidence mandatory training) or Contract (training forms part of employment). A public body might use Public task; genuinely optional data, such as a marketing preference, uses Consent.
  • Retention period: how long you keep the data. When it expires, Moodle flags the data and lists it for deletion for an administrator to confirm, so nothing is destroyed silently. For a user, the period is counted from the last time they accessed the site.
  • Protected: tick this for data you are legally required to keep, so a right-to-be-forgotten request cannot remove it before the retention period ends.
  • Sensitive data reasons (GDPR Art. 9): only needed if you hold special-category data such as health information.

If you are not sure where to start, a sensible default for a typical workplace-training site is one category, Learner records, and one site-wide purpose, Training records, with lawful basis Legal obligation and a retention period that matches how long you must be able to prove compliance (for example, three years since the learner’s last access). Mark certification or mandatory-training evidence as Protected so it survives an erasure request until that period is up. Then make that category and purpose the site-wide default: on the Data registry page the tree’s top node, Site, is selected for you, so choose your category and purpose there and click Save changes, making sure both selectors show an actual value and not Not set before you save. Do this on the Site node itself, not via the separate Set defaults button: that button only covers course categories, courses, activities, and blocks, so it never sets the site-level default. This matters before you handle any requests. Until a site default purpose is set, the Data requests page shows a red “A site purpose and category have not been defined” warning, and an erasure request would remove all of a user’s data instead of expiring only what is past its retention period. Retention itself is enforced on the separate Data deletion page (/admin/tool/dataprivacy/datadeletion.php), which lists contexts that are past their retention period and waits for your confirmation before the “Delete expired contexts” scheduled task removes the data.

3. Operate the data request queue (the part GDPR requires)

This is the heart of it. Under Data requests (/admin/tool/dataprivacy/datarequests.php) you will find the queue. A user clicks “Data request” in their profile to either export their personal data or have it deleted, and it lands here for your Privacy Officer to action.

  • Export request: approve it, and Moodle assembles a downloadable archive of the personal data it can reach through its privacy system. How complete that archive is depends on plugin privacy support, which is point 4. This is your right-of-access answer.
  • Erasure (delete) request: approve it, and Moodle runs its deletion workflow for that user’s personal data, within the limits of core and plugin privacy support. The right to erasure is not absolute, so your DPO still decides whether a given request should be granted. This is your right-to-be-forgotten answer.

Both are built in; your job is to make sure someone owns the queue and works it within the legal time limit, which under GDPR is one month. Sensible defaults: leave automatic approval off, at least for deletions, so a human reviews each erasure before any data is destroyed, and set a Data request expiry so completed export downloads do not stay available indefinitely.

4. The trap almost everyone misses: the plugin privacy registry

The Plugin privacy registry (/admin/tool/dataprivacy/pluginregistry.php) lists every plugin on your site and whether it properly implements Moodle’s Privacy API. Here is the catch: if a non-compliant plugin stores personal data, that data is not included when you export someone’s data, and it is not removed when you process an erasure request. Personal data can therefore survive quietly inside that plugin, and you would never know it from the request itself. There is nothing to fill in here; it is a review, and one you should repeat after every plugin install or upgrade. Sensible default: treat “does not implement the Privacy API” as a blocker for any plugin that stores personal data, and prefer an alternative that does, because a non-compliant plugin quietly undermines both of the rights in point 3.

5. Publish policies and record consent

Finally, publish your policies and capture consent. One setting makes or breaks this: under Policy settings, set the Site policy handler to Policies (tool_policy). Until you do, the tool stores nothing and no consent is enforced, which is the usual reason “I created a policy but nobody is asked to accept it.” Then, under Policies (/admin/tool/policy/managedocs.php), create each document with a name, a type (Site policy, Privacy policy, Third parties, or Other), a short summary, the full text, and an audience (usually All users). Set the status to Active and users must consent at their next login, with each acceptance recorded against a policy version and timestamp as your evidence. Sensible default: publish at least a Privacy policy and a Site policy aimed at All users; later, when you edit one, mark trivial wording fixes as a Minor change so you do not force everyone to re-consent unnecessarily. One caution: Moodle’s recorded policy acceptance is not the same thing as GDPR consent as a lawful basis. Plenty of processing relies on contract, legal obligation, or legitimate interests instead, so your DPO still decides the lawful basis for each purpose (point 2).

Recap

An owner, retention defined in the data registry, the request queue, the plugin privacy check, and policies with tracked consent. Keep these five operational and you have covered the main Moodle-side admin workflows that support GDPR compliance. The lawful basis for each purpose, your retention policy, processor agreements, external systems, backups, and the final decision on each request remain your DPO’s responsibility.

What this does not cover

This checklist covers Moodle’s built-in operational tools only. It does not replace a GDPR review of your lawful bases, your privacy notice, processor agreements, your hosting setup and backups, or any external plugins, LTI tools, analytics, video platforms, or custom integrations that handle personal data outside these screens. Setting up these five areas is the Moodle-side groundwork; it is not, on its own, a statement that your site is GDPR compliant.

Solin helps organizations in regulated sectors operate Moodle and Totara in a GDPR-aligned way, including privacy API gaps in third-party plugins. Contact us for a review.

An auditor asks you to prove that everyone completed their mandatory training. Not “we delivered it”, but proof, per person, that stands up when they pull a sample at random. The good news is that Moodle already holds everything you need; the skill is knowing which report to pull, and in what form an auditor will actually accept it. This guide covers the four evidence sources and what makes each one defensible. Paths and behavior are for Moodle 4.5.

Prefer to watch? Here is a short walkthrough of pulling audit-ready completion evidence:

What “audit-ready” means

Before pulling anything, it helps to know the bar. Evidence that auditors accept is:

  • Dated – undated records are the single most common thing auditors reject.
  • Granular – down to the individual person and what they did.
  • Exportable – they will want a copy, not a screen.
  • Inclusive – it covers everyone in scope, including contractors and temporary staff, not just permanent employees.

Each source below contributes to that picture.

1. The course completion report (your primary evidence)

Start with Reports > Course completion. For every learner it shows whether they met the requirements and, crucially, when. Use the Download in spreadsheet format option at the foot of the report to export who completed what and on which date. That export is your primary evidence pack. (If completion tracking was never switched on for the course, this report is empty; set that up first, because retro-dating completions is not something an auditor will accept.)

2. Activity logs (corroboration for a sample)

Auditors sample, so be ready to back any single record up. The Logs report (Reports > Logs) gives dated, per-user activity: when each person accessed the material and when they submitted. Filter it to the course and to the specific people in the auditor’s sample, and you have corroboration for the completion record straight from the system’s own audit trail.

One caveat worth stating up front: Moodle’s standard logs are subject to a retention period set under Site administration > Plugins > Logging > Standard log, the “Keep logs for” setting. If your audit window is longer than that retention, either extend the retention before the period elapses or export logs periodically. Do not assume two-year-old logs are still there.

3. Grade export (proof of the assessment result)

If the training carries an assessment, export the grades too: Grades > Export > Excel spreadsheet. Now your evidence pack also shows the score each person achieved and that they passed, dated alongside the completion record. This matters where “attended” is not enough and the standard requires a demonstrated pass mark.

4. Verifiable certificates (the strongest single artifact)

The most defensible single artifact is a certificate the auditor can verify independently. Moodle core does not ship a certificate activity, but the widely used Custom certificate plugin (mod_customcert) does, and it issues certificates with a verification code. The certificate records the learner’s name, the course, and the date; the auditor goes to the certificate verification page, types in the code, and Moodle itself confirms the certificate is genuine and who it belongs to. That is about as defensible as evidence gets, because it does not rely on your word.

If you do not have a certificate plugin installed, the completion report plus logs and grades is still a complete evidence pack; the certificate is the icing, not the cake.

Putting the pack together

For a typical audit response: export the course completion report as your headline evidence, keep the grade export alongside it if there is an assessment, and be ready to produce filtered logs and a verified certificate for any name the auditor samples. Make sure the scope of the export includes contractors and temporary accounts. Dated, granular, exportable, inclusive: Moodle holds all of it, you just need to know where to look.

Solin helps regulated organizations build audit-ready evidence workflows in Moodle and Totara, including ISO 27001 and sector compliance. Contact us to talk it through.

Every plugin you add to Moodle is code written by someone outside the Moodle project, and someone has to keep updating it for each new Moodle version. When they stop, that plugin quietly becomes your problem: it can break on your next upgrade, and worse, it stops receiving security fixes. Here is how to check your whole site in about two minutes, and how to judge whether a plugin is genuinely abandoned or just stable. Paths and behavior are for Moodle 4.5.

Step 1: find your third-party plugins

Go to Site administration > Plugins > Plugins overview (/admin/plugins.php). This lists every plugin installed, grouped by type. The ones that are not part of the standard Moodle distribution are your third-party additions (the overview groups these as Additional plugins); those are the ones whose maintenance is not the Moodle project’s responsibility.

Step 2: read the update signals

If a plugin is flagged under Available updates, that is actually good news: it means the plugin is still being looked after, so you simply update it. Use the Check for available updates button to refresh that list. The ones to pay attention to are the additional plugins with no update available and a version that has not moved in a long time. The version string and release date shown here are your first clue, but they are not the whole story; for that, go to the source.

Step 3: check the plugin in the Moodle directory

The authoritative “is it abandoned” signal is the plugin’s page in the Moodle plugins directory at moodle.org/plugins. Open it and look at three things:

  1. The date of the latest release. Years since the last release is a warning sign.
  2. The supported Moodle versions. Does it even list your version? A plugin that does not claim support for your Moodle is running on borrowed time.
  3. How many sites still use it. Usage trailing off, combined with the above, points to abandonment.

A plugin whose last release was years ago, that does not support your Moodle version, with usage falling away, is effectively abandoned, even if it still happens to run today.

Step 4: decide what to do

Once you have found an abandoned plugin you have three choices:

  • Update it if a newer version exists (the easy case).
  • Replace it with a maintained alternative that covers the same need.
  • Remove it entirely if you no longer rely on it.

The one thing you should not do is leave abandoned, unpatched code running on a site that holds real people’s data. Before removing or replacing a plugin that stores data, check what happens to that data (and whether it implements the Privacy API) so you do not strand personal information or break existing courses. Do this check before every major Moodle upgrade, not just when something breaks; an unsupported plugin is the most common reason an upgrade stalls.

Solin manages Moodle and Totara plugin estates, upgrades, and third-party code audits. Contact us if you would like a plugin health check.

You opened Site administration > Reports > Security overview, and one row is sitting on a yellow Warning: “XSS trusted users”, with a number next to it. It reads like an alarm. It is not. Nobody has been hacked, and nothing is broken.

That row is a trust roster. It tells you how many people on your site hold a permission powerful enough that, in the wrong hands, it could be misused. Your job is not to make the number zero. Your job is to look at the list and confirm that everyone on it is someone you actually trust. This guide walks through exactly how to read that warning and act on it, step by step, using only the Moodle admin screens. No database queries, no command line. Paths and behavior are for Moodle 4.5.

Prefer to watch? Here is a short walkthrough of reading and acting on the dangerous capabilities warning:

What the warning actually means

Every permission in Moodle (every capability) can carry one or more risk flags. They are advisory labels, not faults. There are six:

  • XSS – the user can submit content that Moodle does not clean, such as HTML with active scripting or unchecked files. This is the one the “XSS trusted users” check counts.
  • Configuration – the user can change site-wide settings.
  • Personal – the user can reach other people’s private data.
  • Spam – the user can put content in front of others, or message them.
  • Data loss – the user can destroy large amounts of data that is hard to recover.
  • Manage trust – the user can manage other users’ trust settings.

A capability flagged XSS is not a vulnerability. It is simply powerful: anyone who can author rich content (paste HTML, embed media, restore a course, manage roles) can in principle insert a script. That is normal and necessary. The point of the flag is so you can reason about who holds that power.

The most useful fact to keep in mind is how Moodle assigns these risks by default:

  • Guest holds capabilities with no risk at all.
  • Student adds Spam.
  • Teacher adds Personal and XSS.
  • Manager and Administrator effectively hold everything.

So your administrators, managers, and teachers are supposed to be on the XSS list. A non-empty list is the correct, healthy state. The warning is really one question: is everyone here someone I trust to post rich content?

Step 1: Read the list

Go to Site administration > Reports > Security overview and click XSS trusted users (or More info on that row).

Moodle now shows you every user who holds an XSS-flagged capability, anywhere on the site, each name linked to their profile. The page states the task plainly: verify the list and make sure you trust these people completely. Read the names. This is the audit.

Step 2: Decide who belongs

For each person, ask one question: should this account be able to author rich content, manage roles, or restore courses?

  • Expected, leave them alone: administrators, managers, teachers, course creators, and any content-author role you deliberately created.
  • Investigate: an account that should never author content (a plain learner, a generic “info” or service account), an unfamiliar name, or anything that looks automated or left over.

There is also a list-length signal. If the roster is dramatically longer than the number of staff who should have content power, a risky capability has almost certainly leaked into a role that is handed to many people at once, or a staff role has been assigned far too broadly. Hold that thought for Step 5.

Step 3: Find out why a user is on the list

This is the step most write-ups skip, because Moodle has no single screen that says “this user is risky because of capability X in role Y.” You assemble it from two screens, and it takes about thirty seconds per person.

From the list in Step 1, click the user’s name to open their profile, then go to Preferences > This user’s role assignments (under the Roles heading).

This page shows every role the user holds and the exact context for each one: system, a category, or a single course. It is where the cause usually jumps out, for example a support-style or helpdesk-style role assigned at system level, a Teacher or Manager role assigned site-wide instead of inside one course, or a custom role that grants far more than its name suggests.

A teacher assigned inside one course is exactly where they should be. A “support” role sitting at the top, system level, is the kind of thing this audit exists to catch.

Step 4: Confirm which capability is responsible

Open the role you just identified: Site administration > Users > Permissions > Define roles, then open that role.

On the capability table, the Risks column on the right shows a small icon against every capability that carries a risk. Hover an icon to see its label; the one marked XSS risk is your confirmation that this is what put the user on the list. Reading down that column tells you, at a glance, how much power the role really grants, which is often far more than its name implies.

If you would rather work the other way around, Site administration > Reports > Capability overview lets you pick a specific capability and see which roles grant it across the whole site. That is the fastest way to answer “which roles hand out this exact permission?”

Step 5: Fix it the right way

You have three clean options. Choose by what is actually wrong, and follow the principle of least privilege: give each person the least power that lets them do their job.

  1. The person should not have that power. Remove them from the role, or narrow the assignment to the correct context. You can do this from the user’s role-assignments page or under the role’s Assign roles tab.
  2. The role should not grant that capability. Edit the role (Define roles > [role] > Edit) and set the offending capability to Not set or Prevent, then save. This affects everyone who holds that role, so make sure none of them need it.
  3. The capability is on a role given to everyone. This is the most common cause of a bloated list, and the most urgent. Check the Authenticated user, Guest, and Front page roles first: a single risky capability there is handed to your entire user base at once. These roles should never carry a dangerous capability.

One thing not to do: do not strip XSS capabilities from your standard Teacher role just to clear the warning. Teachers are on the list by design, and removing those capabilities breaks legitimate content authoring. If a particular teacher should be more limited, narrow where they are assigned rather than rewriting the shipped role. Editing default roles globally also wipes any course-level overrides your teachers rely on, so prefer a context-specific override or a purpose-built custom role.

Step 6: Verify, then re-check

Confirm the change did what you intended before you move on. In the relevant context (a course’s Participants > Permissions > Check permissions, or the system-level Check permissions screen), look up the user and confirm they can, or can no longer, do the thing you changed. This shows you the computed result rather than your assumption of it.

Then re-run Reports > Security overview. The XSS trusted users count should now contain only people you trust. Again: the target is not zero. The target is no surprises.

A note on “trusted content”

If you want to understand why XSS is a risk category at all, it comes down to how Moodle cleans HTML. By default Moodle strips active content from what users submit. There is an optional setting, Enable trusted content (under Site administration > Security), that, together with the moodle/site:trustcontent capability, lets named users save HTML that bypasses that cleaning.

It is off by default, and that is the right default for almost everyone. Leave it off unless you have a concrete reason to turn it on, and if you do, grant moodle/site:trustcontent to the smallest, most trusted group possible, because it widens exactly the list you just audited. For extra hardening in the other direction, the $CFG->forceclean setting forces content cleaning everywhere.

The same habit covers the whole report

The XSS trusted users check is one of several access checks in the Security overview, alongside Administrators, Guest role, Default user role, Front page role, and Backup data access risk. They all answer the same kind of question: are these settings, and these people, what I expect? Treat the security report as a periodic review rather than a one-time cleanup, and run through it after any change to roles or permissions.

Quick checklist

  1. Reports > Security overview > XSS trusted users: read the list.
  2. Tick off the people who should be there (admins, managers, teachers, content authors).
  3. For anyone who looks wrong: Profile > Preferences > This user’s role assignments to find the role and context.
  4. Define roles > [role]: read the Risks column to confirm the capability, or use the Capability overview report.
  5. Fix by least privilege: unassign the person, narrow the context, or prevent the capability. Check blanket roles (Authenticated user, Guest, Front page) first.
  6. Check permissions to verify, then re-run the Security overview. Aim for “only trusted people”, not zero.

Solin specializes in Moodle role and permission security. Contact us for a roles and capabilities audit.

The hard question in mandatory training is not “did we run it”, it is “who has not done it yet”. Too often the honest answer lives in a spreadsheet that somebody updates by hand and that is out of date the moment it is saved. Moodle can answer that question on demand if you wire up four things: a group that defines who is in scope, automatic enrollment for that group, a clear definition of what “complete” means, and the report that reads it back. This guide walks through all four on Moodle 4.5. The approach is the same for compliance, health and safety, and onboarding training.

Prefer to watch? Here is a short walkthrough of tracking mandatory training completion:

Step 1: define who is in scope with a cohort

Start with the people who have to do the training. In Moodle this is a cohort: a named group of users such as All Staff, New Starters, or a particular department or site. Create it under Site administration > Users > Cohorts, then use the Assign action (the people icon) to add members. You can also bulk-load membership by uploading users with a cohort1 column, or via Upload cohorts for the groups themselves.

The point of building the group once, in one place, is that it becomes your single source of truth for who is in scope. Every course that uses it inherits the same membership, and you maintain that membership in exactly one location.

Step 2: connect the cohort to the course with Cohort sync

Now enroll that group into the mandatory course automatically. In the course, open Participants > Enrolment methods, add Cohort sync, point it at your cohort, and assign the Student role. The enrol_cohort plugin is part of Moodle core, so nothing needs installing.

Two things make this better than manual enrollment:

  • Everyone in the cohort is enrolled at once, with no chance of missing someone.
  • When someone is added to the cohort later, a new hire for example, they are enrolled in the mandatory training automatically, without anyone touching the course.

By default, removing a user from the cohort unenrolls them from the course (their grades are hidden, not deleted). You can change this to “Suspend” in the cohort sync method’s settings if you would rather keep the records visible while revoking access.

Step 3: tell Moodle what “complete” actually means

This is the step people skip, and without it “completion” is just a guess. First, make sure Enable completion tracking is set to Yes in the course settings. Then open Course > More > Course completion and define the condition. For most mandatory training the right condition is Activity completion of the required activity (the policy page, the SCORM module, the quiz, and so on), with the aggregation set to require all selected activities.

For this to work, the activity itself must also have completion configured, under its Activity completion section, so that Moodle knows when that activity is done (viewed, submitted, passed, and so on). With both in place, completion becomes a real, tracked status you can report on and stand behind, rather than a manual tick.

Step 4: read the Course completion report

This is the payoff. Open Reports > Course completion (/report/completion/index.php?course=ID). You get a grid of every person in scope and exactly where they stand: who has met the requirements and who has not. The incomplete rows are your follow-up list, accurate and live rather than a stale spreadsheet.

  • Export it straight to a spreadsheet (CSV or Excel) for your records, using the download options at the bottom of the report.
  • Break it down by group with the group selector, so each manager sees only their own team. Set up Moodle groups in the course if you want this per-team view.

A note on recurring (annual) training

One honest limitation: standard Moodle course completion does not reset itself on a schedule. If your compliance training has to be repeated every year, a one-off completion will stay “complete” indefinitely. For genuinely recurring requirements you have a few options: reset completion for the cohort at the start of each cycle, run a fresh course per cycle, or move to Totara, whose certification feature is built around recurring recertification windows. Choose the approach before you launch, because retrofitting it across historical records is more work.

Recap

Define the group, sync it into the course, define what “done” means, and read the report. That is mandatory training tracking that maintains itself. Whatever platform you are on, insist on these four things; in Moodle they are all built in.

Solin specializes in Moodle and Totara compliance tracking, completion, and reporting. Contact us if you would like help setting this up.

A Moodle site runs out of disk and the culprit is moodledata: the trash directory, automated backup files, or the recycle bin has grown unchecked. It is tempting to blame “trashdir not emptying,” but on a current Moodle the real causes are usually elsewhere. This guide explains what actually consumes the space and the settings that control each one.

What trashdir actually is

moodledata/trashdir is the trash for Moodle’s file storage pool (filedir), not for backups. When a stored file is dereferenced (no activity points at its content any more), Moodle moves the underlying content file into trashdir. A scheduled task, \core\task\file_trash_cleanup_task, empties it. In Moodle 4.5 that task runs every six hours.

So if trashdir is large, the question is whether that cleanup task is running. Check it under Site administration > Server > Tasks > Scheduled tasks (look for the file trash cleanup task) and review Task logs for failures. If cron itself is not running, this task is not running either, and trashdir grows. Confirm cron is alive:

sudo -u www-data php admin/cli/cron.php

A correctly functioning site empties trashdir on schedule. If yours is not, the problem is cron or that specific task, not a missing setting.

What automated backups do (and the safeguard you may not know about)

A common assumption is that a misconfigured or unreachable backup destination causes files to pile up in moodledata until the disk fills. On Moodle 4.5 the opposite is true: there is an explicit safeguard against exactly that. When the automated backup task cannot use the configured external destination (it is missing, not a directory, or not writable), Moodle logs an error, skips the copy, and deletes the backup file rather than leaving it behind. The code comment states the intent directly: it is there to prevent moodledata from filling up when the destination is misconfigured.

That means a broken destination gives you failed backups and error logs, not a slow disk-fill. If moodledata is genuinely filling from backups, look at the settings that govern retention instead.

The settings that actually control backup disk use

Under Site administration > Courses > Backups > Automated backup setup:

  • Automated backup storage (backup_auto_storage) decides where automated backups are kept: in course backup areas (inside moodledata), in a specified external directory, or both. If this is set to keep them inside moodledata, that is where your space is going.
  • Maximum number of backups kept (backup_auto_max_kept) caps how many backups are retained per course. If this is high (or effectively unlimited) and you back up frequently, old backups accumulate. Lowering it lets Moodle prune the surplus.
  • Delete backups older than (backup_auto_delete_days) prunes by age. Combined with max-kept, these two settings are the real levers for backup disk consumption.

Set these to match your actual retention policy. Most sites do not need to keep every automated backup forever inside moodledata.

The recycle bin: the other quiet consumer

Moodle’s recycle bin (the tool_recyclebin admin tool) retains deleted courses and deleted activities so they can be restored. Those retained items live in moodledata and can be substantial, a deleted course holds its entire backup. Two scheduled tasks (cleanup_course_bin and cleanup_category_bin) purge expired items, governed by the expiry settings:

  • tool_recyclebin / coursebinexpiry and categorybinexpiry set how long deleted items are kept before automatic purging.

If these expiry periods are long (or set to never expire) and people delete and re-create courses regularly, the recycle bin can quietly become one of the largest consumers of moodledata. Check its retention settings under Site administration > Plugins > Admin tools > Recycle bin.

Diagnosing where the space has actually gone

Before changing settings, measure. From the moodledata directory:

du -sh trashdir filedir backup
du -sh * | sort -rh | head -20

This tells you whether the space is in trashdir (cleanup task issue), filedir (real content, or dereferenced content awaiting trash), backup areas (retention settings), or elsewhere. Treat the largest directory first rather than assuming it is trashdir.

Clearing trashdir manually, safely

If trashdir has grown and you need space back immediately while you fix the underlying cron/task issue, it can be emptied by hand, because by definition it only holds dereferenced content-pool files:

find moodledata/trashdir -type f -delete

This is what the cleanup task does anyway. Using find rather than rm -rf moodledata/trashdir/* avoids argument-list limits on directories with very many files, and leaves the directory itself in place. It does not touch live files, backups, or the recycle bin. Still, take the usual care: confirm the path, and make sure you are operating on trashdir and not filedir.

The short version

trashdir is emptied by a scheduled task every six hours; if it is full, fix cron, not a setting. A broken backup destination does not fill moodledata on 4.5 (there is a safeguard). The real backup disk levers are the storage location, max-kept, and delete-after-days settings. And do not overlook the recycle bin, which retains deleted courses in moodledata until its expiry settings purge them. Measure with du before acting.

Uploading a large zip file to Moodle succeeds, but extracting it (for example in Private files, or any file manager that offers “Unzip”) fails with a generic error. The reason is that Moodle keeps the original archive in the file area while unpacking, so extraction needs room for the zip plus its uncompressed contents at once, and that combined size is checked against the file area’s size limit, not against the per-file upload limit that let the zip in.

Two different limits, and why that matters

There are two distinct kinds of limit, and the unzip failure is about the second one:

  1. The per-file upload limit caps the size of a single uploaded file. It is the minimum of PHP's upload_max_filesize and post_max_size, the site limit ($CFG->maxbytes), and the course/activity limit. This governs getting the zip in, and it is usually not the problem here.
  2. The file-area size limit (areamaxbytes) caps the total size of everything in a given file area at once. For Private files this comes from $CFG->userquota. This is the limit the unzip operation actually checks.

The trap is that these are different numbers. A zip can be small enough to upload, then fail to extract because the zip plus its contents exceed the area limit. Note that a plain File resource does not impose an area limit at all, so the failure typically shows up in Private files or other quota-bound areas rather than when adding a File resource to a course.

Why the math catches you out

Consider a 600 MB zip file containing a large SCORM package or video collection. The actual uncompressed size is also approximately 600 MB. During extraction:

  • The zip file remains on disk: 600 MB
  • The extracted files are written alongside it: 600 MB
  • Peak combined usage: ~1.2 GB

If the area limit is 1 GB, extraction fails. The upload succeeded because 600 MB is under 1 GB. The extraction fails because the zip plus its extracted contents, ~1.2 GB in the area at once, is not.

The error message Moodle shows, something like “Cannot unzip file”, does not explain this. It looks identical to a corrupt zip or a permissions error.

Finding which limit is triggering

If the failure is in Private files (the most common case), the area limit is the user quota. Check it at:

Site administration > Security > Site security settings > User quota

The user quota ($CFG->userquota) is the total a user may hold in their private files area. The zip plus its extracted contents must fit under it simultaneously, which is the ~2x requirement. Users with the moodle/user:ignoreuserquota capability are exempt, which is why an administrator may not reproduce a learner's failure.

Confirm the zip did clear the separate per-file upload limit (it almost always did, since it uploaded). The effective upload limit is the minimum of the PHP, site, and course values; cross-reference PHP with:

php -r "echo ini_get('upload_max_filesize'), ' / ', ini_get('post_max_size'), PHP_EOL;"

The fix

Raise the area limit so the zip and its contents fit at once. For a Private files failure, that means temporarily increasing the user quota to at least twice the zip size:

Site administration > Security > Site security settings > User quota

Extract the zip, then return the quota to its normal value. Because the user quota is site-wide, do not leave it inflated permanently. An alternative that avoids changing the quota at all is to grant the affected user the moodle/user:ignoreuserquota capability for the extraction, then remove it.

Avoiding the problem

Do not try to “extract on the server” by unzipping into moodledata directly. Moodle stores file content by SHA1 content hash with matching rows in the mdl_files table, so loose files dropped into the filesystem are not recognised, and there is no CLI tool to register them after the fact. Work through Moodle's own file handling instead.

The cleaner long-term fix is to avoid pushing very large archives through a quota-bound area at all. For SCORM packages specifically, many authoring tools can produce smaller packages by splitting large assets (video) out of the SCORM zip and referencing them as external resources, which keeps both the upload and the extracted footprint well under the limits.

Moodle already contains useful data about student engagement: course access, activity completion, grades, and discussion activity. The difficulty is that this data is often scattered across reports, or surfaced through analytics tools that teachers may not fully trust because the reasoning behind a warning is not always obvious.

To solve this, we built Solin Early Warning. It is a Moodle block (a small panel that appears on the side of a course page) that pulls relevant signals into a ranked list directly inside the course. Instead of pushing opaque alerts or automated emails, it uses a multi-signal architecture to tell you exactly why a student was flagged, right where you need to see it.

Solin Early Warning block shown in the sidebar of a Moodle course page, listing flagged students
The Solin Early Warning block sits in the course sidebar, listing flagged students right next to the course content.

This guide explains how the heuristics work, the research behind them, and how you can tune the settings for your own courses.

Prefer to watch? Here is a short walkthrough of the block in action:

Before you start: Completion tracking

Before the block can use all of its signals, your course needs activity completion tracking enabled. Two signals depend on this: assessment miss and stalled completion.

For the assessment-miss signal to work, the relevant activities also need an “Expect completed on” date configured. If completion tracking is not enabled in your course, the block will still run using inactivity, grade trend, and optional forum silence, but it cannot tell whether students are missing expected activity completions.

The research: Architecture over magic numbers

The most authoritative public guidance on early warning systems (such as the 2018 NCES Forum Guide) explicitly states that universal “default” thresholds do not exist. What counts as at-risk in a short compliance module is very different from a 14-week university semester.

For this reason, Solin Early Warning provides a research-informed architecture with institution-tunable thresholds. The combination of signals is backed by empirical literature, but the exact numbers are conservative starting points that you are expected to tune.

To make this distinction visible, every default in the next section is labeled with its evidence type:

  • Strong empirical: the signal or threshold is directly supported by published studies.
  • Institutional convention: the most common starting value across documented practitioner sources, but not directly empirically validated.
  • Conservative starting point: a reasonable default that you should expect to tune for your context.
  • Contested: the empirical literature disagrees about whether the signal predicts what we think it predicts.

How the 5 signals work

The block evaluates students against five independent signals. If a student triggers any of these signals, they appear in the block.

The Solin Early Warning flagged student list with one student expanded to show why they were flagged
Each flagged student can be expanded to show exactly which signals were triggered, instead of an opaque score.

1. Inactivity (Default: 7 days)

  • How it works: Flags a student if they have not accessed the course in the last 7 days.
  • The research: The signal itself is well supported (course access is a basic engagement indicator across the literature). The 7-day threshold is institutional convention: a 7 to 15-day window is the most common starting value in higher-education practitioner sources.
  • Configuration advice: For short, fast-paced courses, tighten this to 3 or 5 days. For long-cadence or self-paced courses, widen it to 14 days or more.

2. Assessment miss (Default: 14-day window)

  • How it works: Flags a student who has not completed an activity whose expected completion date fell within the last 14 days. This applies to assignments, quizzes, SCORM packages, lessons, H5P activities, graded forums, and any other Moodle activity that uses completion tracking.
  • The research: Strong empirical support for the signal itself. Open University (OU Analyse) research shows that students who miss the first Tutor Marked Assignment have a very high probability of course failure; LAK 2025 confirms missed-deadline behavior as a strong predictor across more than 50,000 assignments. The 14-day window is a conservative starting point and is not itself empirically optimized.
  • Caveat: The signal uses the activity-level expected completion date. It does not yet account for per-student extensions (such as quiz access overrides or assignment user-flag extensions). A student given an extra week on Quiz 3 will still be flagged as “not completed” while their override is active.

3. Negative grade trend (Default: Enabled)

  • How it works: Flags a student whose course total grade has trended downward for two consecutive weeks. This signal only becomes available after the block has collected enough weekly grade snapshots.
  • The research: The broader literature supports academic performance and negative momentum as useful risk indicators, but the empirical evidence does not validate any specific delta or interval. We deliberately do not require a percentage drop. The “two consecutive weeks” rule is a conservative starting point.

4. Stalled completion vs peers (Default: Bottom quartile in 14 days)

  • How it works: Flags a student who is in the bottom 25% of the class for completing activities over the last two weeks.
  • The research: The Purdue Course Signals project made peer-relative activity a central part of its early-warning model, and the JISC case study identified it as a key differentiator from absolute-threshold systems. The idea is simple: a student’s activity level is easier to interpret when compared to the actual pace of the class. The specific “bottom quartile in 14 days” rule is a conservative starting point.

5. Forum/discussion silence (Default: Disabled / Opt-in)

  • How it works: Flags a student with zero forum posts in the last 14 days, provided the rest of the class is actively posting.
  • The research: Contested. Some studies find forum participation significantly predictive; Rogers et al. (2025) find a weak negative correlation with academic performance. Vendor systems like Brightspace treat it as a core indicator, but that assumes forums are structurally central to the course pedagogy.
  • Configuration advice: Because of the contested evidence, this signal ships disabled. A site administrator can enable it under Site Administration → Plugins → Blocks → Solin Early Warning. Once enabled site-wide, individual teachers can override it per course (force on, force off, or inherit the site default) via the block’s gear icon. Only enable it if discussion forums are central to your course pedagogy.

How configuration works

Solin Early Warning has three layers of configuration, in order of reach:

  1. Site-level defaults. Set by a site administrator under Site Administration → Plugins → Blocks → Solin Early Warning. These are the institution-wide defaults every block instance starts from.
  2. Per-block-instance overrides. A teacher with the right capability can override site defaults for a specific course by clicking the gear icon on the block. The configuration form is explicit about what is overridden and what is inherited (it shows “Inheriting site default: 7 days” rather than just “7 days” so it is obvious when an institution-wide change will affect this course).
  3. Inline sensitivity preset. The “Show: More / Default / Fewer” dropdown on the block header. This is the fastest way to recalibrate without leaving the course. It writes to the per-block-instance configuration, so a teacher’s preset persists across visits.

Most teachers will only ever use layer 3. Most administrators will only ever set layer 1.

Context matters: Percentiles, small classes, and calibration

Raw data is useless without context. The block includes specific features to make sure the flags make sense in the real world.

Peer-percentile rank and small classes

Next to every flag, the block shows the student’s percentile rank compared to their peers. If a student has not logged in for 9 days, the block will also tell you if that puts them in the bottom 10% of the class.

Expanded Solin Early Warning list showing every flagged student with reasons and peer percentile rank
Expanded view: every flagged student with their per-signal reasons and peer percentile rank.

However, peer-relative signals need a meaningful peer group. In very small courses (fewer than 10 active enrollments), the block will automatically disable peer-relative signals because a single student can distort the comparison. In classes between 10 and 19 students, the block will show a caveat advising you to interpret the peer comparison with care.

The 4-week calibration window

Empirical studies repeatedly show that the first 3 to 5 weeks of a course are the highest-signal window for predicting dropouts.

  • Weeks 1 and 2: The block only flags students who have not accessed the course at all yet. Other signals are still gathering data.
  • Weeks 3 and 4: All enabled signals run, but flagged students are marked with a “Tentative” badge. This allows teachers to see the heuristics calibrating during the period when early intervention matters most.
  • Week 5 onward: The tentative badge is removed and the block runs with full confidence.

If you install the block on a course with no enrolled students, the block will say “Heuristics will activate when students enroll”. This is expected behavior, not a bug.

Holidays and term breaks

Time-based signals would otherwise produce a flood of false positives during institutional breaks: every student looks “inactive” during winter break, and any activity scheduled across the break window appears “missed”. The block handles this in three ways:

  • Site-level breaks calendar. A site administrator can declare institution-wide break ranges (Christmas, spring break, summer holidays) under the block’s site settings. Time inside those ranges is discounted in time-based calculations, so a holiday does not make students look inactive simply because the course was paused. Activities whose expected completion date falls inside a break are excluded from the assessment-miss signal.
  • Per-course break ranges. A teacher can declare ad-hoc break ranges for a specific course via the block’s gear icon. These add to the site-level list. Use this for course-specific pauses that do not apply institution-wide.
  • Pause for one week. A “Pause for one week” link in the block header is available to teachers and adds a one-week break to the current course. A “Resume now” link appears in the active-break banner if you need to end the pause early.

When the current render time falls inside a configured break, the block shows a banner explaining that the list reflects pre-break activity. When past breaks are dampening the current numbers, the block shows a small note explaining how many days of break time were excluded. The flagged list is never hidden during a break — it stays visible so a teacher preparing for resumption can see what is queued up.

Day-to-day use for teachers

The block is designed to be scannable and actionable within seconds.

  • Reading severity: A student who triggers exactly one signal gets a Yellow “Watch” label. A student triggering two or more signals gets a Red “At risk” label. The list automatically sorts the most severe cases to the top.
  • Adjusting sensitivity: Teachers will not use a tool that floods them with noise. In the block header, there is a “Show: More / Default / Fewer” dropdown. This allows you to instantly recalibrate the block for your course without opening the settings form. “More” adjusts the thresholds in the direction that shows more students. “Fewer” only shows the clearest cases.
SettingMore (shows more students)DefaultFewer (shows fewer students)
Inactivity5 days7 days14 days
Assessment-miss lookback21 days14 days10 days
Forum-silence lookback10 days14 days21 days
Grade trendunchangedunchangedunchanged
Stalled completionunchangedunchangedunchanged
  • The release valve (Dismiss for one week): If a student is flagged but you know the situation is already explained (for example: illness, a planned absence, or a temporary extension), you can click “Dismiss for one week”. The student is hidden from the list for 7 days. After that, they will reappear only if they still trigger one or more signals.

What about Moodle’s built-in analytics?

Moodle includes a Learning Analytics tool with a “students at risk” model. It is a different design choice: it uses a machine-learning backend (the PHP backend mlbackend_php ships bundled and is the default, with an optional Python backend mlbackend_python for larger sites) to predict dropout probability, and surfaces insights through Moodle’s messaging system, including email. It can suit institutions that are prepared to maintain the required analytics setup and that prefer push notifications.

Solin Early Warning takes a different approach: in-course visibility, transparent heuristics rather than ML, no email blasts, and an explicit per-signal explanation for every flag. Both can run on the same site. They answer different questions.

What this block does not do

Solin Early Warning does not predict dropout probability, and it does not replace teacher judgment. It does not use demographic profiling, student-background data, or a machine-learning model. It only uses observable Moodle course data and shows the reason for each flag.

A flag means: this student is worth checking. It does not mean: this student will definitely drop out.

Research behind this guide

The signal design and initial configuration defaults of the Solin Early Warning block are informed by the following sources:

Next steps

Solin Early Warning is designed to make risk signals visible inside Moodle. For institutions that want a broader view across courses, Solin can also help review engagement patterns and tune the thresholds to the shape of your courses.

You can install the plugin from the Moodle plugin directory, download it from GitHub, or learn more at solin.co/early-warning.

Certbot stores the webroot path it used during initial certificate issuance. If you later move Moodle's document root, for example, when separating the codebase from the data directory, Let's Encrypt HTTP challenge requests hit a 404 and auto-renewal fails silently until the certificate expires.

How Certbot’s webroot validation works

The HTTP-01 challenge works by placing a temporary token file at:

/.well-known/acme-challenge/<token>

Let’s Encrypt then fetches that file over HTTP to prove you control the domain. Certbot writes the token to a directory on disk and the web server serves it. The directory it writes to is recorded when the certificate is first issued and stored in the renewal configuration file.

If the web server’s document root has changed since then, the file is written to the old path, the web server cannot find it, and Let’s Encrypt gets a 404. The renewal fails.

Diagnosing the problem

Run a dry-run renewal to see the error without modifying anything:

certbot renew --dry-run

A failing renewal will show output like:

Attempting to renew cert (yourdomain.com) via certbot...
Challenge failed for domain yourdomain.com
http-01 challenge for yourdomain.com
Cleaning up challenges
Failed to renew certificate yourdomain.com with error:
Some challenges have failed.

Check the current webroot path Certbot has on record:

cat /etc/letsencrypt/renewal/yourdomain.com.conf

The webroot is recorded in two places in this file: a webroot_path line under [renewalparams], and a domain-to-path entry under the [[webroot_map]] subsection (note the double brackets). Both point at the old document root:

[renewalparams]
authenticator = webroot
webroot_path = /home/oldsitepath/public_html,
...
[[webroot_map]]
yourdomain.com = /home/oldsitepath/public_html

Compare this against your Apache or Nginx virtual host configuration to find the current document root. If they differ, that is the problem.

Fixing it

Edit the renewal config file directly:

vim /etc/letsencrypt/renewal/yourdomain.com.conf

Update both the webroot_path line under [renewalparams] and the entry under [[webroot_map]] to the current document root. Editing only one of them is the usual reason the fix appears not to take:

[renewalparams]
authenticator = webroot
webroot_path = /home/newsitepath/public_html,
...
[[webroot_map]]
yourdomain.com = /home/newsitepath/public_html

(certbot 2.3 and later also offer certbot reconfigure as a supported way to change renewal parameters without hand-editing the file.)

Save, then test:

certbot renew --dry-run

If the dry-run succeeds, the next scheduled renewal will work correctly.

Alternative: re-run certificate issuance

If you prefer not to edit the config file manually, you can re-run Certbot’s webroot mode, pointing it at the new path. This updates the stored configuration as a side effect:

certbot certonly --webroot 
  -w /home/newsitepath/public_html 
  -d yourdomain.com 
  --force-renewal

Use --force-renewal only in this recovery scenario, it counts against Let’s Encrypt’s rate limits.

Checking that the challenge path is web-accessible

Before the dry-run, verify the web server can actually serve from /.well-known/acme-challenge/. On Apache, the default WordPress or Moodle .htaccess sometimes redirects all requests to index.php, which blocks the challenge path. Check for a rule like this in your .htaccess:

RewriteRule ^ index.php [L]

If present, add an exception before it:

RewriteRule ^.well-known - [L]

On Nginx, verify there is no try_files or return directive that catches all requests before the challenge path can be served.

Automating renewal checks

Certbot installs a systemd timer or cron job for auto-renewal, but failures are only logged, no alert is sent by default. Add a simple check to your monitoring:

certbot certificates 2>/dev/null | grep -E "Domains:|Expiry Date:|VALID"

Or use ssl-cert-check to get an alert before the certificate reaches a critical expiry window.