April 3, 2026 · 4 min
handle_absent_entries: remove Almost Deleted Everything
Patrick McClory
The thing that makes declarative automation powerful is exactly the thing that makes it dangerous. I wrote a user management task with handle_absent_entries: remove, defined a partial list, and RouterOS refused to execute because it would have deleted the last user with full access permissions. The safety net caught it. The lesson is about knowing where aggressive automation ends and self-inflicted disaster begins.
handle_absent_entries: remove Almost Deleted Everything
The thing that makes declarative automation powerful is exactly the thing that makes it dangerous.
I was building out a user management task for the router, a MikroTik R630 running RouterOS 7.22, managed via Ansible using the community.routeros.api_modify module. The task was simple: define the service users the platform needs, converge the router to match that definition. Declarative. Clean. Correct.
The parameter that does the work is handle_absent_entries. Set it to remove and the module deletes anything on the device that isn’t in your data list. This is the feature that makes declarative automation actually declarative. It doesn’t just add what you defined, it removes what you didn’t.
I defined the service users I cared about. I set handle_absent_entries: remove. I ran the playbook.
RouterOS refused. The error: the user is last one with full access permissions.
admin and pmdev were already gone. Only apiuser survived because RouterOS has a hard protection against deleting the last user with full access. The router protected itself from me.
How I Got Here
The context matters. I’ve been running an aggressive automation stance on this platform, everything through Ansible, no manual configuration that isn’t immediately reinforced by code, push hard and fix fast. This is the right approach for building a production-grade reference architecture. It’s also the approach that occasionally finds the edges.
The user management task was new. I wrote it to manage service accounts: the mktxp monitoring user, the API user, the SSH operator user. I defined those in routing_service_users in host_vars and wrote the task to converge the router’s user list to match.
What I didn’t think through clearly: handle_absent_entries: remove on the user path means “delete every user not in this list.” My list had three entries. The router had five. The module did exactly what I told it to do.
Having the safety net emboldened me to push further than I’d normally push without thinking through the blast radius of each parameter. That’s not a failure of the stance. It’s what safety nets are for. And it’s a lesson about where more care is required regardless of how good your recovery path is. Declarative removal semantics on security-critical resources is one of those places.
The Safety Net
I had IPMI access and a clean recovery path. The recovery was simple. Names are easier than IP addresses under pressure. Worth keeping your out-of-band endpoints in DNS.
The recovery was clean. API access was still working on the new port. I recreated pmdev via the API module, imported the SSH key via the playbook, confirmed access, moved on.
The Lesson About Declarative Removal
Declarative automation has two modes: additive and authoritative.
Additive means: add what’s defined, leave everything else alone. This is safe everywhere. It’s also incomplete. Over time, drift accumulates as things get added manually or by other processes and never get cleaned up.
Authoritative means: the definition is the truth, the device must match it exactly, remove anything that isn’t in the definition. This is what makes declarative automation actually powerful. It’s also what makes it dangerous when the definition is incomplete.
The question to ask before setting handle_absent_entries: remove on any path is: what happens if my data list is missing something that should be there?
For firewall rules, NAT rules, static routes: missing entries get removed and you have a network problem. Recoverable, potentially annoying, caught quickly.
For users: missing entries get removed and you may lose access to the device entirely. In the worst case, you lose access to everything including the out-of-band management you’d use to recover.
The fix is simple: handle_absent_entries: ignore for the user path. Additive only. Service users get added and updated, existing users never get removed by automation. If a user needs to be removed, that’s a deliberate manual action, not a side effect of an incomplete data list.
- name: Configure service users
community.routeros.api_modify:
path: user
handle_absent_entries: ignore # additive only, never remove users via automation
handle_entries_content: remove_as_much_as_possible
data: "{{ routing_service_users | default([]) }}"
This pattern applies everywhere declarative automation touches security-critical resources: users, credentials, firewall rules that allow management access, SSH keys. The authoritative pattern is powerful and appropriate for most things. It requires explicit thought for anything that could lock you out.
What the Aggressive Stance Actually Means
I’m not walking back the aggressive automation approach. Pushing hard is how you find the edges, and finding the edges is how you learn where the boundaries actually are rather than where you assumed they were. The boundary here is: authoritative declarative removal on user accounts requires a complete and verified list, not an incremental one.
I found that boundary by hitting it. RouterOS caught it before actual damage was done. I fixed the task, recreated the users, confirmed access, and committed the corrected version with a comment explaining exactly why ignore is the right setting for this path.
The platform is more correct now than it was before I made the mistake. That’s the outcome the aggressive stance is supposed to produce.
The beach is nice. The router is healthy. The IPMI DNS is set up. Onward.