diff --git a/Makefile b/Makefile
index eb773082..9a40c688 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: commit-acceptance pylint mypy black reformat test authorino poetry poetry-no-dev mgc container-image polish-junit reportportal authorino-standalone limitador kuadrant kuadrant-only disruptive
+.PHONY: commit-acceptance pylint mypy black reformat test authorino poetry poetry-no-dev mgc container-image polish-junit reportportal authorino-standalone limitador kuadrant kuadrant-only disruptive kuadrantctl
 
 TB ?= short
 LOGLEVEL ?= INFO
@@ -45,7 +45,7 @@ test pytest tests: kuadrant
 
 authorino: ## Run only authorino related tests
 authorino: poetry-no-dev
-	$(PYTEST) -n4 -m 'authorino' --dist loadfile --enforce $(flags) testsuite
+	$(PYTEST) -n4 -m 'authorino' --dist loadfile --enforce $(flags) testsuite/tests/kuadrant
 
 authorino-standalone: ## Run only test capable of running with standalone Authorino
 authorino-standalone: poetry-no-dev
@@ -53,15 +53,15 @@ authorino-standalone: poetry-no-dev
 
 limitador: ## Run only Limitador related tests
 limitador: poetry-no-dev
-	$(PYTEST) -n4 -m 'limitador' --dist loadfile --enforce $(flags) testsuite
+	$(PYTEST) -n4 -m 'limitador' --dist loadfile --enforce $(flags) testsuite/tests/kuadrant
 
 kuadrant: ## Run all tests available on Kuadrant
 kuadrant: poetry-no-dev
-	$(PYTEST) -n4 -m 'not standalone_only and not disruptive' --dist loadfile --enforce $(flags) testsuite
+	$(PYTEST) -n4 -m 'not standalone_only and not disruptive' --dist loadfile --enforce $(flags) testsuite/tests/kuadrant
 
 kuadrant-only: ## Run Kuadrant-only tests
 kuadrant-only: poetry-no-dev
-	$(PYTEST) -n4 -m 'kuadrant_only and not standalone_only and not disruptive' --dist loadfile --enforce $(flags) testsuite
+	$(PYTEST) -n4 -m 'kuadrant_only and not standalone_only and not disruptive' --dist loadfile --enforce $(flags) testsuite/tests/kuadrant
 
 dnstls: ## Run DNS and TLS tests
 dnstls: poetry-no-dev
@@ -71,6 +71,10 @@ disruptive: ## Run disruptive tests
 disruptive: poetry-no-dev
 	$(PYTEST) -m 'disruptive' $(flags) testsuite
 
+kuadrantctl: ## Run Kuadrantctl tests
+kuadrantctl: poetry-no-dev
+	$(PYTEST) -n4 --dist loadfile --enforce $(flags) testsuite/tests/kuadrantctl
+
 poetry.lock: pyproject.toml
 	poetry lock
 
diff --git a/config/settings.local.yaml.tpl b/config/settings.local.yaml.tpl
index 413e16c2..81f89c85 100644
--- a/config/settings.local.yaml.tpl
+++ b/config/settings.local.yaml.tpl
@@ -5,6 +5,7 @@
 #    api_url: "https://api.openshift.com"      # Optional: OpenShift API URL, if None it will OpenShift that you are logged in
 #    token: "KUADRANT_RULEZ"                   # Optional: OpenShift Token, if None it will OpenShift that you are logged in
 #    kubeconfig_path: "~/.kube/config"         # Optional: Kubeconfig to use, if None the default one is used
+#  kuadrantctl: kuadrantctl
 #  tools:
 #    project: "tools"                          # Optional: OpenShift project, where external tools are located
 #  keycloak:
diff --git a/config/settings.yaml b/config/settings.yaml
index c6426e31..fb1da0e1 100644
--- a/config/settings.yaml
+++ b/config/settings.yaml
@@ -1,6 +1,7 @@
 default:
   dynaconf_merge: true
   cluster: {}
+  kuadrantctl: "kuadrantctl"
   tools:
     project: "tools"
   cfssl: "cfssl"
diff --git a/testsuite/gateway/gateway_api/route.py b/testsuite/gateway/gateway_api/route.py
index 902e5d9c..714458b0 100644
--- a/testsuite/gateway/gateway_api/route.py
+++ b/testsuite/gateway/gateway_api/route.py
@@ -114,3 +114,15 @@ def add_backend(self, backend: "Backend", prefix="/"):
     @modify
     def remove_all_backend(self):
         self.model.spec.rules.clear()
+
+    def wait_for_ready(self):
+        """Waits until HTTPRoute is reconcilled by GatewayProvider"""
+
+        def _ready(obj):
+            for condition_set in obj.model.status.parents:
+                if condition_set.controllerName == "istio.io/gateway-controller":
+                    return (all(x.status == "True" for x in condition_set.conditions),)
+            return False
+
+        success = self.wait_until(_ready, timelimit=10)
+        assert success, f"{self.kind()} did got get ready in time"
diff --git a/testsuite/kuadrantctl.py b/testsuite/kuadrantctl.py
new file mode 100644
index 00000000..f8e8995b
--- /dev/null
+++ b/testsuite/kuadrantctl.py
@@ -0,0 +1,52 @@
+# pylint: disable=line-too-long
+"""
+Help as of 0.2.3
+Kuadrant configuration command line utility
+
+Usage:
+  kuadrantctl [command]
+
+Available Commands:
+  completion  Generate the autocompletion script for the specified shell
+  generate    Commands related to kubernetes object generation
+    gatewayapi  Generate Gataway API resources
+        httproute   Generate Gateway API HTTPRoute from OpenAPI 3.0.X
+    kuadrant    Generate Kuadrant resources
+        authpolicy      Generate Kuadrant AuthPolicy from OpenAPI 3.0.X
+        ratelimitpolicy Generate Kuadrant Rate Limit Policy from OpenAPI 3.0.X
+
+
+  help        Help about any command
+  version     Print the version number of kuadrantctl
+
+Flags:
+  -h, --help                   help for httproute
+      --oas string             Path to OpenAPI spec file (in JSON or YAML format), URL, or '-' to read from standard input (required)
+  -o, --output-format string   Output format: 'yaml' or 'json'. (default "yaml")
+
+Global Flags:
+  -v, --verbose   verbose output
+
+
+Use "kuadrantctl [command] --help" for more information about a command.
+
+"""
+
+import subprocess
+
+
+class KuadrantCTL:
+    """Wrapper on top of kuadrantctl binary"""
+
+    def __init__(self, binary) -> None:
+        super().__init__()
+        self.binary = binary
+
+    def run(self, *args, **kwargs):
+        """Passes arguments to Subprocess.run, see that for more details"""
+        args = (self.binary, *args)
+        kwargs.setdefault("capture_output", True)
+        kwargs.setdefault("check", True)
+        kwargs.setdefault("text", True)
+        # We do supply value for check :)
+        return subprocess.run(args, **kwargs)  # pylint: disable= subprocess-run-check
diff --git a/testsuite/oas.py b/testsuite/oas.py
new file mode 100644
index 00000000..306dcce3
--- /dev/null
+++ b/testsuite/oas.py
@@ -0,0 +1,49 @@
+"""OAS processing"""
+
+import contextlib
+import json
+import tempfile
+from collections import UserDict
+
+import yaml
+
+from testsuite.backend import Backend
+from testsuite.gateway import Referencable, Hostname
+
+
+@contextlib.contextmanager
+def as_tmp_file(text):
+    """Saves text in a temporary file and returns absolute path"""
+    with tempfile.NamedTemporaryFile("w") as file:
+        file.write(text)
+        file.flush()
+        yield file.name
+
+
+class OASWrapper(UserDict):
+    """Wrapper for OpenAPISpecification"""
+
+    def as_json(self):
+        """Returns OAS as JSON"""
+        return json.dumps(self.data)
+
+    def as_yaml(self):
+        """Returns OAS as YAML"""
+        return yaml.dump(self.data)
+
+    def add_backend_to_paths(self, backend: Backend):
+        """Adds backend to all paths, should be only used in tests that do not test this section"""
+        for path in self["paths"].values():
+            path["x-kuadrant"] = {
+                "backendRefs": [backend.reference],
+            }
+
+    def add_top_level_route(self, parent: Referencable, hostname: Hostname, name: str):
+        """Adds top-level x-kuadrant definition for Route, should be only used in tests that do not test this section"""
+        self["x-kuadrant"] = {
+            "route": {
+                "name": name,
+                "hostnames": [hostname.hostname],
+                "parentRefs": [parent.reference],
+            }
+        }
diff --git a/testsuite/openshift/client.py b/testsuite/openshift/client.py
index 92d61707..bf51adc7 100644
--- a/testsuite/openshift/client.py
+++ b/testsuite/openshift/client.py
@@ -119,3 +119,11 @@ def project_exists(self):
             return True
         except oc.OpenShiftPythonException:
             return False
+
+    def apply_from_string(self, string, cls, cmd_args=None):
+        """Applies new object from the string to the server and returns it wrapped in the class"""
+        with self.context:
+            selector = oc.apply(string, cmd_args=cmd_args)
+            obj = selector.object(cls=cls)
+            obj.context = self.context
+        return obj
diff --git a/testsuite/resources/oas/__init__.py b/testsuite/resources/oas/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/testsuite/resources/oas/base_httpbin.yaml b/testsuite/resources/oas/base_httpbin.yaml
new file mode 100644
index 00000000..4ef74b6a
--- /dev/null
+++ b/testsuite/resources/oas/base_httpbin.yaml
@@ -0,0 +1,35 @@
+---
+openapi: 3.1.0
+info:
+  title: Httpbin
+  version: 0.0.51
+paths:
+  "/get":
+    get:
+      operationId: get_get
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                type: string
+  "/anything":
+    get:
+      operationId: get_anything
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                type: string
+    put:
+      operationId: put_anything
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                type: string
\ No newline at end of file
diff --git a/testsuite/tests/kuadrantctl/__init__.py b/testsuite/tests/kuadrantctl/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/testsuite/tests/kuadrantctl/cli/__init__.py b/testsuite/tests/kuadrantctl/cli/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/testsuite/tests/kuadrantctl/cli/test_basic_commands.py b/testsuite/tests/kuadrantctl/cli/test_basic_commands.py
new file mode 100644
index 00000000..e2134e43
--- /dev/null
+++ b/testsuite/tests/kuadrantctl/cli/test_basic_commands.py
@@ -0,0 +1,12 @@
+"""Tests basic commands"""
+
+import pytest
+
+
+# https://github.com/Kuadrant/kuadrantctl/issues/90
+@pytest.mark.parametrize("command", ["help", "version"])
+def test_commands(kuadrantctl, command):
+    """Test that basic commands exists and returns anything"""
+    result = kuadrantctl.run(command)
+    assert not result.stderr, f"Command '{command}' returned an error: {result.stderr}"
+    assert result.stdout, f"Command '{command}' returned empty output"
diff --git a/testsuite/tests/kuadrantctl/cli/test_simple_auth.py b/testsuite/tests/kuadrantctl/cli/test_simple_auth.py
new file mode 100644
index 00000000..6e04aa60
--- /dev/null
+++ b/testsuite/tests/kuadrantctl/cli/test_simple_auth.py
@@ -0,0 +1,66 @@
+"""Tests that you can generate simple AuthPolicy, focused on the cmdline options more than on extension functionality"""
+
+import pytest
+
+from testsuite.httpx.auth import HttpxOidcClientAuth
+from testsuite.oas import as_tmp_file
+from testsuite.policy.authorization.auth_policy import AuthPolicy
+
+
+@pytest.fixture(scope="module")
+def auth(keycloak):
+    """Returns authentication object for HTTPX"""
+    return HttpxOidcClientAuth(keycloak.get_token, "authorization")
+
+
+@pytest.fixture(scope="module")
+def oas(oas, keycloak, blame, gateway, hostname, backend):
+    """Add OIDC configuration"""
+    oas.add_top_level_route(gateway, hostname, blame("route"))
+
+    oas["components"] = {
+        "securitySchemes": {
+            "oidc": {
+                "type": "openIdConnect",
+                "openIdConnectUrl": keycloak.well_known["issuer"],
+                # https://github.com/Kuadrant/kuadrantctl/issues/94
+                # "openIdConnectUrl": keycloak.well_known["issuer"] + "/.well-known/openid-configuration",
+            }
+        }
+    }
+    anything = oas["paths"]["/anything"]
+    anything["x-kuadrant"] = {
+        "backendRefs": [backend.reference],
+    }
+    anything["get"]["security"] = [{"oidc": []}]
+    return oas
+
+
+@pytest.mark.parametrize("encoder", [pytest.param("as_json", id="JSON"), pytest.param("as_yaml", id="YAML")])
+@pytest.mark.parametrize("stdin", [pytest.param(True, id="STDIN"), pytest.param(False, id="File")])
+def test_generate_authpolicy(request, kuadrantctl, oas, encoder, openshift, client, stdin, auth):
+    """Generates Policy from OAS and tests that it works as expected"""
+    encoded = getattr(oas, encoder)()
+
+    if stdin:
+        result = kuadrantctl.run("generate", "kuadrant", "authpolicy", "--oas", "-", input=encoded)
+    else:
+        with as_tmp_file(encoded) as file_name:
+            result = kuadrantctl.run("generate", "kuadrant", "authpolicy", "--oas", file_name)
+
+    policy = openshift.apply_from_string(result.stdout, AuthPolicy)
+    request.addfinalizer(policy.delete)
+
+    policy.wait_for_ready()
+
+    response = client.get("/anything")
+    assert response.status_code == 401
+
+    response = client.get("/anything", auth=auth)
+    assert response.status_code == 200
+
+    response = client.get("/anything", headers={"Authorization": "Bearer xyz"})
+    assert response.status_code == 401
+
+    response = client.put("/anything")
+    assert response.status_code == 200
diff --git a/testsuite/tests/kuadrantctl/cli/test_simple_limit.py b/testsuite/tests/kuadrantctl/cli/test_simple_limit.py
new file mode 100644
index 00000000..68f17741
--- /dev/null
+++ b/testsuite/tests/kuadrantctl/cli/test_simple_limit.py
@@ -0,0 +1,45 @@
+"""
+Tests that you can generate simple RateLimitPolicy, focused on the cmdline options more than on extension
+functionality
+"""
+
+import pytest
+
+from testsuite.oas import as_tmp_file
+from testsuite.policy.rate_limit_policy import Limit, RateLimitPolicy
+from testsuite.utils import asdict
+
+
+@pytest.fixture(scope="module")
+def oas(oas, blame, gateway, hostname, backend):
+    """Add X-Kuadrant specific fields"""
+    oas.add_top_level_route(gateway, hostname, blame("route"))
+    oas.add_backend_to_paths(backend)
+
+    oas["paths"]["/anything"]["get"]["x-kuadrant"] = {"rate_limit": {"rates": [asdict(Limit(3, 20))]}}
+    return oas
+
+
+@pytest.mark.parametrize("encoder", [pytest.param("as_json", id="JSON"), pytest.param("as_yaml", id="YAML")])
+@pytest.mark.parametrize("stdin", [pytest.param(True, id="STDIN"), pytest.param(False, id="File")])
+def test_generate_limit(request, kuadrantctl, oas, encoder, openshift, client, stdin):
+    """Tests that RateLimitPolicy can be generated and that it is enforced as expected"""
+    encoded = getattr(oas, encoder)()
+
+    if stdin:
+        result = kuadrantctl.run("generate", "kuadrant", "ratelimitpolicy", "--oas", "-", input=encoded)
+    else:
+        with as_tmp_file(encoded) as file_name:
+            result = kuadrantctl.run("generate", "kuadrant", "ratelimitpolicy", "--oas", file_name)
+
+    policy = openshift.apply_from_string(result.stdout, RateLimitPolicy)
+    request.addfinalizer(policy.delete)
+    policy.wait_for_ready()
+
+    responses = client.get_many("/anything", 3)
+    responses.assert_all(status_code=200)
+    assert client.get("/anything").status_code == 429
+
+    # Check that it did not affect other endpoints
+    responses = client.get_many("/get", 5)
+    responses.assert_all(status_code=200)
diff --git a/testsuite/tests/kuadrantctl/cli/test_simple_route.py b/testsuite/tests/kuadrantctl/cli/test_simple_route.py
new file mode 100644
index 00000000..ee4eff6b
--- /dev/null
+++ b/testsuite/tests/kuadrantctl/cli/test_simple_route.py
@@ -0,0 +1,66 @@
+"""Tests that you can generate simple HTTPRoute, focused on the cmdline options more than on extension functionality"""
+
+import pytest
+
+from testsuite.gateway.gateway_api.route import HTTPRoute
+from testsuite.oas import as_tmp_file
+
+
+@pytest.fixture(scope="module")
+def route():
+    """Make sure Route is not created automatically"""
+    return None
+
+
+@pytest.fixture(scope="module")
+def oas(oas, blame, gateway, hostname, backend):
+    """Add Route and Backend specifications to OAS"""
+    oas["x-kuadrant"] = {
+        "route": {
+            "name": blame("route"),
+            "hostnames": [hostname.hostname],
+            "parentRefs": [gateway.reference],
+        }
+    }
+    oas.add_backend_to_paths(backend)
+    return oas
+
+
+@pytest.mark.parametrize("encoder", [pytest.param("as_json", id="JSON"), pytest.param("as_yaml", id="YAML")])
+@pytest.mark.parametrize("stdin", [pytest.param(True, id="STDIN"), pytest.param(False, id="File")])
+def test_generate_route(request, kuadrantctl, oas, encoder, openshift, client, stdin):
+    """Tests that Route can be generated and that is works as expected"""
+    encoded = getattr(oas, encoder)()
+
+    if stdin:
+        result = kuadrantctl.run("generate", "gatewayapi", "httproute", "--oas", "-", input=encoded)
+    else:
+        with as_tmp_file(encoded) as file_name:
+            result = kuadrantctl.run("generate", "gatewayapi", "httproute", "--oas", file_name)
+
+    # https://github.com/Kuadrant/kuadrantctl/issues/91
+    route = openshift.apply_from_string(result.stdout, HTTPRoute, cmd_args="--validate=false")
+    # route = openshift.apply_from_string(result.stdout, HTTPRoute)
+    request.addfinalizer(route.delete)
+    route.wait_for_ready()
+
+    response = client.get("/get")
+    assert response.status_code == 200
+
+    response = client.get("/anything")
+    assert response.status_code == 200
+
+    response = client.put("/anything")
+    assert response.status_code == 200
+
+    # Incorrect methods
+    response = client.post("/anything")
+    assert response.status_code == 404
+
+    # Incorrect path
+    response = client.get("/anything/test")
+    assert response.status_code == 404
+
+    # Incorrect endpoint
+    response = client.post("/post")
+    assert response.status_code == 404
diff --git a/testsuite/tests/kuadrantctl/conftest.py b/testsuite/tests/kuadrantctl/conftest.py
new file mode 100644
index 00000000..9b63789d
--- /dev/null
+++ b/testsuite/tests/kuadrantctl/conftest.py
@@ -0,0 +1,48 @@
+"""Conftest for kuadrantctl tests"""
+
+import shutil
+from importlib import resources
+
+import pytest
+import yaml
+
+from testsuite.gateway.gateway_api.route import HTTPRoute
+from testsuite.kuadrantctl import KuadrantCTL
+from testsuite.oas import OASWrapper
+
+
+@pytest.fixture(scope="session")
+def kuadrantctl(testconfig, skip_or_fail):
+    """Return Kuadrantctl wrapper"""
+    binary_path = testconfig["kuadrantctl"]
+    if not shutil.which(binary_path):
+        skip_or_fail("Kuadrantctl binary not found")
+    return KuadrantCTL(binary_path)
+
+
+@pytest.fixture(scope="module")
+def oas():
+    """
+    OpenAPISpecification definition
+    """
+    return OASWrapper(
+        yaml.safe_load(resources.files("testsuite.resources.oas").joinpath("base_httpbin.yaml").read_text())
+    )
+
+
+@pytest.fixture(scope="function")
+def route(request, kuadrantctl, oas, openshift):
+    """Generates Route from OAS"""
+    result = kuadrantctl.run("generate", "gatewayapi", "httproute", "--oas", "-", input=oas.as_yaml(), check=False)
+    assert result.returncode == 0, f"Unable to create Route from OAS: {result.stderr}"
+    route = openshift.apply_from_string(result.stdout, HTTPRoute, cmd_args="--validate=false")
+    request.addfinalizer(route.delete)
+    return route
+
+
+@pytest.fixture(scope="function")
+def client(hostname, route):  # pylint: disable=unused-argument
+    """Returns httpx client to be used for requests, it also commits AuthConfig"""
+    client = hostname.client()
+    yield client
+    client.close()