diff --git a/docs/authentication.rst b/docs/authentication.rst
index d7ae95a..e17889d 100644
--- a/docs/authentication.rst
+++ b/docs/authentication.rst
@@ -14,6 +14,12 @@ authentication methods and piggybacks the existing Kubernetes clients.
 Custom authentication
 =====================
 
+In most setups, the normal authentication from one of the API client libraries
+is enough --- it works out of the box if those clients are installed
+(see :ref:`auth-piggybacking` below). Custom authentication is only needed
+if the normal authentication methods do not work for some reason, such as if
+you have a specific and unusual cluster setup (e.g. your own auth tokens).
+
 To implement a custom authentication method, one or a few login-handlers
 can be added. The login handlers should either return nothing (``None``)
 or an instance of `kopf.ConnectionInfo`::
diff --git a/kopf/_cogs/clients/auth.py b/kopf/_cogs/clients/auth.py
index 632984d..4b4af34 100644
--- a/kopf/_cogs/clients/auth.py
+++ b/kopf/_cogs/clients/auth.py
@@ -44,7 +44,12 @@ def reauthenticated_request(fn: _F) -> _F:
                 return await fn(*args, **kwargs, context=context)
             except errors.APIUnauthorizedError as e:
                 await vault.invalidate(key, exc=e)
-        raise credentials.LoginError("Ran out of connection credentials.")
+
+        # Normally, either `vault.extended()` or `vault.invalidate()` raise the login errors.
+        # The for-cycle can only end if the yielded credentials are not invalidated before trying
+        # the next ones -- but this case exits by `return` or by other (non-401) errors.
+        raise RuntimeError("Reached an impossible state: the end of the authentication cycle.")
+
     return cast(_F, wrapper)
 
 
@@ -76,7 +81,12 @@ def reauthenticated_stream(fn: _F) -> _F:
                 return
             except errors.APIUnauthorizedError as e:
                 await vault.invalidate(key, exc=e)
-        raise credentials.LoginError("Ran out of connection credentials.")
+
+        # Normally, either `vault.extended()` or `vault.invalidate()` raise the login errors.
+        # The for-cycle can only end if the yielded credentials are not invalidated before trying
+        # the next ones -- but this case exits by `return` or by other (non-401) errors.
+        raise RuntimeError("Reached an impossible state: the end of the authentication cycle.")
+
     return cast(_F, wrapper)
 
 
diff --git a/kopf/_cogs/structs/credentials.py b/kopf/_cogs/structs/credentials.py
index e9bca12..3d9cae6 100644
--- a/kopf/_cogs/structs/credentials.py
+++ b/kopf/_cogs/structs/credentials.py
@@ -205,7 +205,9 @@ class Vault(AsyncIterable[Tuple[VaultKey, ConnectionInfo]]):
             it can lead to improper items returned.
         """
         if not self._current:
-            raise LoginError("No valid credentials are available.")
+            raise LoginError("Ran out of valid credentials. Consider installing "
+                             "an API client library or adding a login handler. See more: "
+                             "https://kopf.readthedocs.io/en/stable/authentication/")
         prioritised: Dict[int, List[Tuple[VaultKey, VaultItem]]]
         prioritised = collections.defaultdict(list)
         for key, item in self._current.items():
@@ -252,11 +254,14 @@ class Vault(AsyncIterable[Tuple[VaultKey, ConnectionInfo]]):
 
         # If the re-auth has failed, re-raise the original exception in the current stack.
         # If the original exception is unknown, raise normally on the next iteration's yield.
+        # The error here is optional -- for better stack traces of the original exception `exc`.
         # Keep in mind, this routine is called in parallel from many tasks for the same keys.
         async with self._lock:
             if not self._current:
                 if exc is not None:
-                    raise exc
+                    raise LoginError("Ran out of valid credentials. Consider installing "
+                                     "an API client library or adding a login handler. See more: "
+                                     "https://kopf.readthedocs.io/en/stable/authentication/") from exc
 
     async def populate(
             self,
diff --git a/kopf/_core/intents/piggybacking.py b/kopf/_core/intents/piggybacking.py
index e05714d..df57ba1 100644
--- a/kopf/_core/intents/piggybacking.py
+++ b/kopf/_core/intents/piggybacking.py
@@ -46,6 +46,7 @@ def login_via_client(
         **kwargs: Any,
 ) -> Optional[credentials.ConnectionInfo]:
 
+    # Keep imports in the function, as module imports are mocked in some tests.
     try:
         import kubernetes.config
     except ImportError:
@@ -59,7 +60,8 @@ def login_via_client(
             kubernetes.config.load_kube_config()  # developer's config files
             logger.debug("Client is configured via kubeconfig file.")
         except kubernetes.config.ConfigException as e2:
-            raise credentials.LoginError(f"Cannot authenticate client neither in-cluster, nor via kubeconfig.")
+            raise credentials.LoginError("Cannot authenticate the client library "
+                                         "neither in-cluster, nor via kubeconfig.")
 
     # We do not even try to understand how it works and why. Just load it, and extract the results.
     # For kubernetes client >= 12.0.0 use the new 'get_default_copy' method
@@ -100,6 +102,7 @@ def login_via_pykube(
         **kwargs: Any,
 ) -> Optional[credentials.ConnectionInfo]:
 
+    # Keep imports in the function, as module imports are mocked in some tests.
     try:
         import pykube
     except ImportError:
@@ -115,8 +118,8 @@ def login_via_pykube(
             config = pykube.KubeConfig.from_file()
             logger.debug("Pykube is configured via kubeconfig file.")
         except (pykube.PyKubeError, FileNotFoundError):
-            raise credentials.LoginError(f"Cannot authenticate pykube "
-                                         f"neither in-cluster, nor via kubeconfig.")
+            raise credentials.LoginError("Cannot authenticate pykube "
+                                         "neither in-cluster, nor via kubeconfig.")
 
     # We don't know how this token will be retrieved, we just get it afterwards.
     provider_token = None

diff --git a/tests/authentication/test_vault.py b/tests/authentication/test_vault.py
index 50cc939..d39ef7c 100644
--- a/tests/authentication/test_vault.py
+++ b/tests/authentication/test_vault.py
@@ -55,7 +55,8 @@ async def test_invalidation_reraises_if_nothing_is_left_with_exception(mocker):
     with pytest.raises(Exception) as e:
         await vault.invalidate(key1, exc=exc)
 
-    assert e.value is exc
+    assert isinstance(e.value, LoginError)
+    assert e.value.__cause__ is exc
     assert vault._ready.wait_for.await_args_list == [((True,),)]
 
 
diff --git a/tests/k8s/test_creating.py b/tests/k8s/test_creating.py
index 7d45fd5..c2958dc 100644
--- a/tests/k8s/test_creating.py
+++ b/tests/k8s/test_creating.py
@@ -40,7 +40,8 @@ async def test_full_body_with_identifiers(
     assert data == {'x': 'y', 'metadata': {'name': 'name1', 'namespace': namespace}}
 
 
-@pytest.mark.parametrize('status', [400, 401, 403, 404, 409, 500, 666])
+# Note: 401 is wrapped into a LoginError and is tested elsewhere.
+@pytest.mark.parametrize('status', [400, 403, 404, 409, 500, 666])
 async def test_raises_api_errors(
         resp_mocker, aresponses, hostname, status, resource, namespace,
         cluster_resource, namespaced_resource):
diff --git a/tests/k8s/test_errors.py b/tests/k8s/test_errors.py
index 54465f3..411d4a0 100644
--- a/tests/k8s/test_errors.py
+++ b/tests/k8s/test_errors.py
@@ -3,7 +3,7 @@ import pytest
 
 from kopf._cogs.clients.auth import APIContext, reauthenticated_request
 from kopf._cogs.clients.errors import APIConflictError, APIError, APIForbiddenError, \
-                                      APINotFoundError, APIUnauthorizedError, check_response
+                                      APINotFoundError, check_response
 
 
 @reauthenticated_request
@@ -47,9 +47,9 @@ async def test_no_error_on_success(
     await get_it(f"http://{hostname}/")
 
 
+# Note: 401 is wrapped into a LoginError and is tested elsewhere.
 @pytest.mark.parametrize('status, exctype', [
     (400, APIError),
-    (401, APIUnauthorizedError),
     (403, APIForbiddenError),
     (404, APINotFoundError),
     (409, APIConflictError),
diff --git a/tests/k8s/test_list_objs.py b/tests/k8s/test_list_objs.py
index 57822a6..cfa9e84 100644
--- a/tests/k8s/test_list_objs.py
+++ b/tests/k8s/test_list_objs.py
@@ -3,6 +3,7 @@ import pytest
 
 from kopf._cogs.clients.errors import APIError
 from kopf._cogs.clients.fetching import list_objs_rv
+from kopf._cogs.structs.credentials import LoginError
 
 
 async def test_listing_works(
@@ -23,8 +24,9 @@ async def test_listing_works(
     assert list_mock.call_count == 1
 
 
-@pytest.mark.parametrize('status', [400, 401, 403, 500, 666])
-async def test_raises_api_error(
+# Note: 401 is wrapped into a LoginError and is tested elsewhere.
+@pytest.mark.parametrize('status', [400, 403, 500, 666])
+async def test_raises_direct_api_errors(
         resp_mocker, aresponses, hostname, status, resource, namespace,
         cluster_resource, namespaced_resource):
 
diff --git a/tests/k8s/test_patching.py b/tests/k8s/test_patching.py
index 0f758da..0702df9 100644
--- a/tests/k8s/test_patching.py
+++ b/tests/k8s/test_patching.py
@@ -177,7 +177,8 @@ async def test_ignores_absent_objects(
     assert result is None
 
 
-@pytest.mark.parametrize('status', [400, 401, 403, 500, 666])
+# Note: 401 is wrapped into a LoginError and is tested elsewhere.
+@pytest.mark.parametrize('status', [400, 403, 500, 666])
 async def test_raises_api_errors(
         resp_mocker, aresponses, hostname, status, resource, namespace,
         cluster_resource, namespaced_resource):

Authentication during startup
## Problem
I'm trying to create/update CRDs using the `@kopf.on.startup()` event. However, it seems to me that the authentication even has not yet fired, which means that there is no valid connection to the Kubernetes api.

I can get around this by running `config.load_kube_config()` and/or `config.load_incluster_config()` but it seems a bit dirty (this is what the kopf authentication stuff is mode for, after all).


## Proposal
Add a new event, `@kopf.on.post_auth_startup()` that executes after the authentication stuff is initialized.

## Checklist

- [x] Many users can benefit from this feature, it is not a one-time case
- [x] The proposal is related to the K8s operator framework, not to the K8s client libraries

tests/authentication/test_vault.py::test_invalidation_reraises_if_nothing_is_left_with_exception tests/k8s/test_creating.py::test_raises_api_errors[namespaced-409] tests/k8s/test_errors.py::test_exception_with_payload[cluster] tests/k8s/test_errors.py::test_error_with_payload[namespaced-409-APIConflictError] tests/k8s/test_errors.py::test_error_with_nonjson_payload[namespaced-666] tests/k8s/test_list_objs.py::test_listing_works[cluster] tests/k8s/test_patching.py::test_without_subresources[cluster] tests/k8s/test_patching.py::test_status_as_body_field_with_combined_payload[cluster] tests/k8s/test_patching.py::test_raises_api_errors[cluster-666]
tests/authentication/test_vault.py::test_evals_as_false_when_empty tests/authentication/test_vault.py::test_evals_as_true_when_filled tests/authentication/test_vault.py::test_yielding_after_creation tests/authentication/test_vault.py::test_yielding_after_population tests/authentication/test_vault.py::test_invalidation_continues_if_nothing_is_left_without_exception tests/authentication/test_vault.py::test_invalidation_continues_if_something_is_left tests/authentication/test_vault.py::test_yielding_after_invalidation tests/authentication/test_vault.py::test_duplicates_are_remembered tests/authentication/test_vault.py::test_caches_from_factory tests/authentication/test_vault.py::test_caches_with_same_purpose tests/authentication/test_vault.py::test_caches_with_different_purposes tests/k8s/test_creating.py::test_simple_body_with_arguments[namespaced] tests/k8s/test_creating.py::test_simple_body_with_arguments[cluster] tests/k8s/test_creating.py::test_full_body_with_identifiers[namespaced] tests/k8s/test_creating.py::test_full_body_with_identifiers[cluster] tests/k8s/test_creating.py::test_raises_api_errors[namespaced-400] tests/k8s/test_creating.py::test_raises_api_errors[namespaced-403] tests/k8s/test_creating.py::test_raises_api_errors[namespaced-404] tests/k8s/test_creating.py::test_raises_api_errors[namespaced-500] tests/k8s/test_creating.py::test_raises_api_errors[cluster-400] tests/k8s/test_creating.py::test_raises_api_errors[cluster-403] tests/k8s/test_creating.py::test_raises_api_errors[cluster-404] tests/k8s/test_creating.py::test_raises_api_errors[cluster-409] tests/k8s/test_creating.py::test_raises_api_errors[cluster-500] tests/k8s/test_creating.py::test_raises_api_errors[cluster-666] tests/k8s/test_errors.py::test_aiohttp_is_not_leaked_outside[namespaced] tests/k8s/test_errors.py::test_aiohttp_is_not_leaked_outside[cluster] tests/k8s/test_errors.py::test_exception_without_payload[namespaced] tests/k8s/test_errors.py::test_exception_without_payload[cluster] tests/k8s/test_errors.py::test_exception_with_payload[namespaced] tests/k8s/test_errors.py::test_no_error_on_success[namespaced-200] tests/k8s/test_errors.py::test_no_error_on_success[namespaced-202] tests/k8s/test_errors.py::test_no_error_on_success[namespaced-304] tests/k8s/test_errors.py::test_no_error_on_success[cluster-200] tests/k8s/test_errors.py::test_no_error_on_success[cluster-202] tests/k8s/test_errors.py::test_no_error_on_success[cluster-300] tests/k8s/test_errors.py::test_no_error_on_success[cluster-304] tests/k8s/test_errors.py::test_error_with_payload[namespaced-400-APIError] tests/k8s/test_errors.py::test_error_with_payload[namespaced-403-APIForbiddenError] tests/k8s/test_errors.py::test_error_with_payload[namespaced-404-APINotFoundError] tests/k8s/test_errors.py::test_error_with_payload[namespaced-500-APIError] tests/k8s/test_errors.py::test_error_with_payload[cluster-400-APIError] tests/k8s/test_errors.py::test_error_with_payload[cluster-403-APIForbiddenError] tests/k8s/test_errors.py::test_error_with_payload[cluster-404-APINotFoundError] tests/k8s/test_errors.py::test_error_with_payload[cluster-409-APIConflictError] tests/k8s/test_errors.py::test_error_with_payload[cluster-500-APIError] tests/k8s/test_errors.py::test_error_with_payload[cluster-666-APIError] tests/k8s/test_errors.py::test_error_with_nonjson_payload[namespaced-400] tests/k8s/test_errors.py::test_error_with_nonjson_payload[namespaced-500] tests/k8s/test_errors.py::test_error_with_nonjson_payload[cluster-400] tests/k8s/test_errors.py::test_error_with_nonjson_payload[cluster-666] tests/k8s/test_errors.py::test_error_with_parseable_nonk8s_payload[namespaced-400] tests/k8s/test_errors.py::test_error_with_parseable_nonk8s_payload[namespaced-500] tests/k8s/test_errors.py::test_error_with_parseable_nonk8s_payload[namespaced-666] tests/k8s/test_errors.py::test_error_with_parseable_nonk8s_payload[cluster-400] tests/k8s/test_errors.py::test_error_with_parseable_nonk8s_payload[cluster-500] tests/k8s/test_errors.py::test_error_with_parseable_nonk8s_payload[cluster-666] tests/k8s/test_list_objs.py::test_listing_works[namespaced] tests/k8s/test_list_objs.py::test_raises_direct_api_errors[namespaced-400] tests/k8s/test_list_objs.py::test_raises_direct_api_errors[namespaced-500] tests/k8s/test_list_objs.py::test_raises_direct_api_errors[namespaced-666] tests/k8s/test_list_objs.py::test_raises_direct_api_errors[cluster-400] tests/k8s/test_list_objs.py::test_raises_direct_api_errors[cluster-403] tests/k8s/test_list_objs.py::test_raises_direct_api_errors[cluster-500] tests/k8s/test_list_objs.py::test_raises_direct_api_errors[cluster-666] tests/k8s/test_patching.py::test_without_subresources[namespaced] tests/k8s/test_patching.py::test_status_as_subresource_with_combined_payload[namespaced] tests/k8s/test_patching.py::test_status_as_subresource_with_object_fields_only[namespaced] tests/k8s/test_patching.py::test_status_as_subresource_with_object_fields_only[cluster] tests/k8s/test_patching.py::test_status_as_subresource_with_status_fields_only[namespaced] tests/k8s/test_patching.py::test_status_as_subresource_with_status_fields_only[cluster] tests/k8s/test_patching.py::test_status_as_body_field_with_combined_payload[namespaced] tests/k8s/test_patching.py::test_ignores_absent_objects[namespaced-404] tests/k8s/test_patching.py::test_raises_api_errors[namespaced-403] tests/k8s/test_patching.py::test_raises_api_errors[namespaced-500] tests/k8s/test_patching.py::test_raises_api_errors[namespaced-666] tests/k8s/test_patching.py::test_raises_api_errors[cluster-400] tests/k8s/test_patching.py::test_raises_api_errors[cluster-403] tests/k8s/test_patching.py::test_raises_api_errors[cluster-500]