diff --git a/arkindex/ponos/api.py b/arkindex/ponos/api.py
index 8fa0542c1ce052e1952782c0af45b14972d873cc..4cdee3e3bebb4b3d76386a47f57150f6ad63ce35 100644
--- a/arkindex/ponos/api.py
+++ b/arkindex/ponos/api.py
@@ -26,11 +26,11 @@ from arkindex.ponos.models import Agent, Artifact, Farm, Secret, State, Task
 from arkindex.ponos.permissions import (
     IsAgent,
     IsAgentOrArtifactGuest,
-    IsAgentOrTaskAdmin,
     IsAgentOrTaskGuest,
     IsAssignedAgentOrReadOnly,
     IsAssignedAgentOrTaskOrReadOnly,
     IsTask,
+    IsTaskAdmin,
 )
 from arkindex.ponos.renderers import PublicKeyPEMRenderer
 from arkindex.ponos.serializers import (
@@ -364,7 +364,9 @@ class TaskArtifactDownload(APIView):
 )
 class TaskCreate(CreateAPIView):
     """
-    Create a task with a parent
+    Create a task that depends on an existing task.
+
+    Requires authentication as a Ponos task. Tasks can only be created on the process of the authenticated task.
     """
 
     authentication_classes = (TaskAuthentication, )
@@ -375,18 +377,28 @@ class TaskCreate(CreateAPIView):
 @extend_schema(tags=["ponos"])
 @extend_schema_view(
     put=extend_schema(
-        description="Update a task, allowing humans to change the task's state"
+        description=dedent("""
+        Update a task.
+
+        Requires **admin** access on the task's process, or to be the creator of the process.
+        Cannot be used with Ponos agent or task authentication.
+        """),
     ),
     patch=extend_schema(
-        description="Partially update a task, allowing humans to change the task's state"
+        description=dedent("""
+        Partially update a task.
+
+        Requires **admin** access on the task's process, or to be the creator of the process.
+        Cannot be used with Ponos agent or task authentication.
+        """),
     ),
 )
 class TaskUpdate(UpdateAPIView):
-    """
-    Admins and task creators can use this endpoint to update a task
-    """
-    permission_classes = (IsAgentOrTaskAdmin, )
-    queryset = Task.objects.all()
+    # Only allow regular users, not Ponos agents or tasks
+    authentication_classes = (TokenAuthentication, SessionAuthentication)
+    # Only allow regular users that have admin access to the task's process
+    permission_classes = (IsTaskAdmin, )
+    queryset = Task.objects.select_related('process__corpus', 'process__revision__repo')
     serializer_class = TaskTinySerializer
 
 
diff --git a/arkindex/ponos/permissions.py b/arkindex/ponos/permissions.py
index f8624fd93a5a3260d4058384730392ccb379b353..c27e8d3b5e6e4a04e4b87149c5d279c38846fda8 100644
--- a/arkindex/ponos/permissions.py
+++ b/arkindex/ponos/permissions.py
@@ -1,8 +1,8 @@
 from rest_framework.permissions import SAFE_METHODS
 
 from arkindex.ponos.models import Task
-from arkindex.project.mixins import CorpusACLMixin, ProcessACLMixin
-from arkindex.project.permissions import IsAuthenticated
+from arkindex.project.mixins import ProcessACLMixin
+from arkindex.project.permissions import IsAuthenticated, IsVerified
 from arkindex.users.models import Role
 
 
@@ -79,26 +79,17 @@ class IsAssignedAgentOrTaskOrReadOnly(IsAgentOrTask):
         )
 
 
-class IsAgentOrTaskAdmin(CorpusACLMixin, IsAuthenticated):
+class IsTaskAdmin(ProcessACLMixin, IsVerified):
     """
-    Permission to access a task with high privilege
-
-    Allowed for admins, agents, creators of the task's process,
-    and users with an admin right on the process' corpus.
+    Allow instance admins and users with a verified email and admin access to the task's process.
     """
 
     def has_object_permission(self, request, view, task):
         # Add request to attributes for the ACL mixin to work with self.user
         self.request = request
-
-        return (
-            require_agent_or_admin(request, view)
-            or (
-                task.process is not None
-                and task.process.corpus_id is not None
-                and self.has_admin_access(task.process.corpus)
-            )
-        )
+        level = self.process_access_level(task.process)
+        # process_access_level can return None if there is no access at all
+        return level and level >= Role.Admin.value
 
 
 class IsAgentOrTaskGuest(ProcessACLMixin, IsAuthenticated):
diff --git a/arkindex/ponos/tests/test_api.py b/arkindex/ponos/tests/test_api.py
index 31501484a2e335bcf0b297004cbbe5c0fbd0a0ac..0e4717cc0386b6ca65712cfe7e2c734ff7ba5bf2 100644
--- a/arkindex/ponos/tests/test_api.py
+++ b/arkindex/ponos/tests/test_api.py
@@ -733,6 +733,85 @@ class TestAPI(FixtureAPITestCase):
         # Both parents are completed, the child task is now pending
         self.assertEqual(self.task3.state, State.Pending)
 
+    def test_update_task_requires_login(self):
+        with self.assertNumQueries(0):
+            resp = self.client.put(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
+            )
+            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
+
+    def test_update_task_requires_verified(self):
+        self.user.verified_email = False
+        self.user.save()
+        self.client.force_login(self.user)
+
+        with self.assertNumQueries(2):
+            resp = self.client.put(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
+            )
+            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
+
+    def test_update_task_forbids_agent(self):
+        with self.assertNumQueries(0):
+            resp = self.client.put(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
+                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
+            )
+            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
+
+    def test_update_task_forbids_task(self):
+        with self.assertNumQueries(0):
+            resp = self.client.put(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
+                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
+            )
+            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
+
+    def test_update_task_requires_process_admin_corpus(self):
+        self.process.creator = self.superuser
+        self.process.save()
+        self.corpus.public = False
+        self.corpus.save()
+        self.client.force_login(self.user)
+
+        for role in [None, Role.Guest, Role.Contributor]:
+            with self.subTest(role=role):
+                self.corpus.memberships.filter(user=self.user).delete()
+                if role:
+                    self.corpus.memberships.create(user=self.user, level=role.value)
+
+                with self.assertNumQueries(5):
+                    resp = self.client.put(
+                        reverse("api:task-update", args=[self.task1.id]),
+                        data={"state": State.Stopping.value},
+                    )
+                    self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
+
+    def test_update_task_requires_process_admin_repo(self):
+        self.process.mode = ProcessMode.Repository
+        self.process.corpus = None
+        self.process.revision = self.rev
+        self.process.creator = self.superuser
+        self.process.save()
+        self.client.force_login(self.user)
+
+        for role in [None, Role.Guest, Role.Contributor]:
+            with self.subTest(role=role):
+                self.rev.repo.memberships.filter(user=self.user).delete()
+                if role:
+                    self.rev.repo.memberships.create(user=self.user, level=role.value)
+
+                with self.assertNumQueries(5):
+                    resp = self.client.put(
+                        reverse("api:task-update", args=[self.task1.id]),
+                        data={"state": State.Stopping.value},
+                    )
+                    self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
+
     def test_update_running_task_state_stopping(self):
         self.task1.state = State.Running
         self.task1.save()
@@ -780,7 +859,7 @@ class TestAPI(FixtureAPITestCase):
         self.task1.save()
         self.client.force_login(self.superuser)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(5):
             resp = self.client.put(
                 reverse("api:task-update", args=[self.task1.id]),
                 data={"state": State.Pending.value},
@@ -920,6 +999,85 @@ class TestAPI(FixtureAPITestCase):
         # Both parents are completed, the child task is now pending
         self.assertEqual(self.task3.state, State.Pending)
 
+    def test_partial_update_task_requires_login(self):
+        with self.assertNumQueries(0):
+            resp = self.client.patch(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
+            )
+            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
+
+    def test_partial_update_task_requires_verified(self):
+        self.user.verified_email = False
+        self.user.save()
+        self.client.force_login(self.user)
+
+        with self.assertNumQueries(2):
+            resp = self.client.patch(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
+            )
+            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
+
+    def test_partial_update_task_forbids_agent(self):
+        with self.assertNumQueries(0):
+            resp = self.client.patch(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
+                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
+            )
+            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
+
+    def test_partial_update_task_forbids_task(self):
+        with self.assertNumQueries(0):
+            resp = self.client.patch(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
+                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
+            )
+            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
+
+    def test_partial_update_task_requires_process_admin_corpus(self):
+        self.process.creator = self.superuser
+        self.process.save()
+        self.corpus.public = False
+        self.corpus.save()
+        self.client.force_login(self.user)
+
+        for role in [None, Role.Guest, Role.Contributor]:
+            with self.subTest(role=role):
+                self.corpus.memberships.filter(user=self.user).delete()
+                if role:
+                    self.corpus.memberships.create(user=self.user, level=role.value)
+
+                with self.assertNumQueries(5):
+                    resp = self.client.patch(
+                        reverse("api:task-update", args=[self.task1.id]),
+                        data={"state": State.Stopping.value},
+                    )
+                    self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
+
+    def test_partial_update_task_requires_process_admin_repo(self):
+        self.process.mode = ProcessMode.Repository
+        self.process.corpus = None
+        self.process.revision = self.rev
+        self.process.creator = self.superuser
+        self.process.save()
+        self.client.force_login(self.user)
+
+        for role in [None, Role.Guest, Role.Contributor]:
+            with self.subTest(role=role):
+                self.rev.repo.memberships.filter(user=self.user).delete()
+                if role:
+                    self.rev.repo.memberships.create(user=self.user, level=role.value)
+
+                with self.assertNumQueries(5):
+                    resp = self.client.patch(
+                        reverse("api:task-update", args=[self.task1.id]),
+                        data={"state": State.Stopping.value},
+                    )
+                    self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
+
     def test_partial_update_running_task_state_stopping(self):
         self.task1.state = State.Running
         self.task1.save()
@@ -967,7 +1125,7 @@ class TestAPI(FixtureAPITestCase):
         self.task1.save()
         self.client.force_login(self.superuser)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(5):
             resp = self.client.patch(
                 reverse("api:task-update", args=[self.task1.id]),
                 data={"state": State.Pending.value},
diff --git a/arkindex/project/tests/test_ponos_view.py b/arkindex/project/tests/test_ponos_view.py
deleted file mode 100644
index 3091f790bf60c799ceab7f7448ea66ae0125b5f5..0000000000000000000000000000000000000000
--- a/arkindex/project/tests/test_ponos_view.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from django.test import override_settings
-from django.urls import reverse
-from rest_framework import status
-
-from arkindex.documents.models import Corpus
-from arkindex.ponos.models import Artifact
-from arkindex.process.models import ProcessMode
-from arkindex.project.tests import FixtureAPITestCase
-from arkindex.users.models import Role, User
-
-
-@override_settings(PONOS_PRIVATE_KEY='staging')
-class TestPonosView(FixtureAPITestCase):
-
-    @classmethod
-    def setUpTestData(cls):
-        super().setUpTestData()
-        cls.creator = User.objects.create(email="creator@user.me")
-        cls.artifact = Artifact.objects.get(path='/path/to/docker_build')
-        cls.task = cls.artifact.task
-
-        # Assign a corpus to the task's process so we can test the process corpus permissions
-        cls.process_corpus = Corpus.objects.create(name='Another public corpus', public=True)
-        cls.corpus_admin = User.objects.create(email='corpusadmin@test.me')
-        cls.corpus_admin.rights.create(content_object=cls.process_corpus, level=Role.Admin.value)
-
-        process = cls.task.process
-        process.mode = ProcessMode.Files
-        process.corpus = cls.process_corpus
-        process.save()
-
-    def test_update_task(self):
-        """
-        Only users with an admin privilege have the ability to update a process task
-        """
-        test_cases = (
-            (None, status.HTTP_403_FORBIDDEN, 0),
-            (self.creator, status.HTTP_403_FORBIDDEN, 8),
-            (self.user, status.HTTP_403_FORBIDDEN, 8),
-            (self.superuser, status.HTTP_200_OK, 10),
-            (self.corpus_admin, status.HTTP_200_OK, 14),
-        )
-        for user, status_code, requests_count in test_cases:
-            with self.subTest(user=user):
-                if user:
-                    self.client.force_login(user)
-                with self.assertNumQueries(requests_count):
-                    response = self.client.patch(
-                        reverse('api:task-update', kwargs={'pk': str(self.task.id)}),
-                        json={'state': 'stopping'}
-                    )
-                    self.assertEqual(response.status_code, status_code)