Why are you here? Because someone asked "can we just open the network for five minutes while we debug this incident" and you realized the honest answer was "yes, and then I will forget to close it." Out of the jokes, this is the automation we built to fix that, and the security review that politely told us it wasn't finished.
We run a self-hosted NetBird mesh (separate post, if you haven't read it) as our zero-trust network layer. Access policies are deliberately tight — most peers only see what they need. Which is correct, right up until 2am when someone debugging a robot fleet incident needs temporary access to something they normally can't reach, and the choices are "wait for someone to manually edit a policy" or "grant broad access and hope someone remembers to revoke it."
Neither of those is a real answer. So we automated it.
The Problem
Incident response needs temporary, scoped, auditable network access. What we had was manual policy edits, which fails on all three:
- Temporary — nothing expires anything automatically. Access granted during an incident stays granted until someone remembers to undo it.
- Scoped — "give them access to debug this" tends to become "give them access to everything nearby" because scoping precisely takes longer than the incident allows for.
- Auditable — a policy edit in the NetBird dashboard doesn't say why. Six months later nobody remembers which incident justified which rule.
We already track incidents in ClickUp. The access grant should be a side effect of the ticket, not a separate manual step someone has to remember.
The Build
The shape of it: ClickUp webhook → small Flask service → NetBird REST API.
- A ticket moves into an "active incident" status in ClickUp.
- ClickUp fires a webhook to the Flask app.
- The Flask app validates the payload, maps the ticket to a pre-defined access exception (which peers, which group), and calls the NetBird REST API to apply it.
- When the ticket closes, the reverse webhook fires and the Flask app calls NetBird again to remove the exception.
@app.route("/webhook/clickup", methods=["POST"])
def handle_ticket_event():
payload = request.get_json()
if not verify_signature(request):
abort(403)
ticket = payload["task_id"]
status = payload["history_items"][0]["after"]["status"]
if status == "active-incident":
grant_exception(ticket)
elif status == "closed":
revoke_exception(ticket)
return "", 200
Straightforward. Deployed it, tested it against a couple of real incidents, it worked, everyone was happy for a while.
The Security Review (This Is The Part That Matters More Than The Build)
Ran an internal review before treating this as production-grade, and it found three real problems — none of them "the code is broken," all of them "the code does exactly what it was told to do, and what it was told to do isn't safe enough."
Problem 1: the approval gate isn't actually enforceable
The design assumed a ticket only reaches "active-incident" status after someone reviews it. In practice, status is just a field. Anyone with edit access to the ticket can set it, which means anyone with ClickUp access can trigger a network access grant. The "approval" was a convention, not a control.
Problem 2: no TTL on granted access
The revoke path depends entirely on the ticket being closed. If a ticket gets abandoned, forgotten, or just left open because the incident fizzled out without a clean resolution, the access exception stays live indefinitely. There's no automatic expiry independent of ClickUp's state — which is exactly the manual-revoke problem we built this to solve, just moved one layer up.
Problem 3: the NetBird PAT is over-scoped
The personal access token the Flask app uses to call the NetBird API has more permission than "create and remove this specific kind of exception" requires. Fine as long as the Flask app itself is never compromised. Not fine as a design, because "as long as X is never compromised" is not a security boundary, it's a hope.
Where This Leaves Us
Not rolled back — it's genuinely better than manual edits for the common case — but not trusted as-is for anything sensitive either:
| Issue | Status |
|---|---|
| Approval gate bypassable via ticket edit | Open — needs a real approval step, not a status field |
| No TTL / expiry | Open — needs an independent timeout regardless of ticket state |
| Over-scoped PAT | Open — needs a token scoped to exactly the exception-management calls |
What I Would Tell Past Me
- "The webhook works" and "the automation is safe" are two different milestones. Ship the first, don't confuse it for the second.
- Any control gated on a field a normal user can edit is not a control. If approval matters, it needs its own step that isn't also the trigger.
- Anything that grants access needs an expiry that doesn't depend on a downstream system behaving correctly. ClickUp closing the ticket is a nice-to-have, not a safety net.
- Scope tokens to the automation's actual job, always, even for internal tooling nobody expects to be attacked. Especially for internal tooling nobody expects to be attacked.
v2 is scoping the PAT down and adding a hard TTL on every grant, independent of ticket status. The approval-gate problem is the hard one and doesn't have a clean answer yet.