From f0e6e1fe573d44069a9861cb3f79b452c33dfa96 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Wed, 6 Nov 2024 22:23:13 +0000 Subject: [PATCH] Deployed dd31f6f to main with MkDocs 1.6.1 and mike 2.1.3 --- main/404.html | 2 +- .../social/reference/apis/policy.v1alpha1.png | Bin 0 -> 41598 bytes main/assets/images/social/reference/index.png | Bin 0 -> 44408 bytes .../images/social/reference/json-schemas.png | Bin 0 -> 42087 bytes main/community/contribute/index.html | 2 +- main/community/index.html | 2 +- .../making-a-pull-request/index.html | 2 +- main/community/reporting-a-bug/index.html | 2 +- .../reporting-a-docs-issue/index.html | 2 +- main/community/requesting-a-change/index.html | 2 +- main/index.html | 4 +-- main/intro/index.html | 2 +- main/jp/functions/index.html | 2 +- main/jp/index.html | 2 +- main/performance/index.html | 4 +-- main/policies/asserts/index.html | 2 +- main/policies/authz-policy/index.html | 2 +- main/policies/policies/index.html | 2 +- main/quick-start/index.html | 2 +- .../reference/apis/policy.v1alpha1/index.html | 1 + main/reference/index.html | 1 + main/reference/json-schemas/index.html | 26 ++++++++++++++++++ main/search/search_index.json | 2 +- main/sitemap.xml | 12 ++++++++ main/sitemap.xml.gz | Bin 366 -> 398 bytes main/tutorials/istio/index.html | 2 +- main/tutorials/mtls-istio/index.html | 4 +-- main/tutorials/standalone-envoy/index.html | 2 +- 28 files changed, 63 insertions(+), 23 deletions(-) create mode 100644 main/assets/images/social/reference/apis/policy.v1alpha1.png create mode 100644 main/assets/images/social/reference/index.png create mode 100644 main/assets/images/social/reference/json-schemas.png create mode 100644 main/reference/apis/policy.v1alpha1/index.html create mode 100644 main/reference/index.html create mode 100644 main/reference/json-schemas/index.html diff --git a/main/404.html b/main/404.html index c214428..2aad45e 100644 --- a/main/404.html +++ b/main/404.html @@ -1 +1 @@ - Kyverno Envoy Plugin

404 - Not found

\ No newline at end of file + Kyverno Envoy Plugin

404 - Not found

\ No newline at end of file diff --git a/main/assets/images/social/reference/apis/policy.v1alpha1.png b/main/assets/images/social/reference/apis/policy.v1alpha1.png new file mode 100644 index 0000000000000000000000000000000000000000..856bbc57b5b8ac14afed9ec4dabaa299afd5ba06 GIT binary patch literal 41598 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

R#pG#eaWcIH}R&CvjX{Hs5(IzU+f{4Siw$l5? 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 Yzxgz8-nsyswJpE?;^R?$>ipGz1HUg6T>t<8 literal 0 HcmV?d00001 diff --git a/main/assets/images/social/reference/index.png b/main/assets/images/social/reference/index.png new file mode 100644 index 0000000000000000000000000000000000000000..5f64231a0bfe937b744e452dc354a7187430ee4a GIT binary patch literal 44408 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[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 E7vc8y%m4rY literal 0 HcmV?d00001 diff --git a/main/community/contribute/index.html b/main/community/contribute/index.html index 4cb8407..ece10fc 100644 --- a/main/community/contribute/index.html +++ b/main/community/contribute/index.html @@ -1 +1 @@ - Contributing - Kyverno Envoy Plugin

Contributing

Kyverno Envoy Plugin, developed by Kyverno, is an advanced end-to-end testing tool for Kubernetes. Our community plays a crucial role in shaping the project by reporting bugs, suggesting features, and improving documentation.

We aim to make our issue tracker, discussion board, and documentation well-structured and easy to navigate. By following our guidelines, you can help us address your requests efficiently.

How you can contribute

We appreciate your efforts in reporting bugs, requesting features, and engaging in discussions. Here's how you can contribute:

Creating an issue

  •   Something is not working?


    Report a bug by creating an issue with a reproduction


    Report a bug

  •   Missing information in our docs?


    Report missing information or potential inconsistencies in our documentation


    Report a docs issue

  •   Want to submit an idea?


    Propose a change, feature request, or suggest an improvement


    Request a change

  •   Have a question or need help?


    Ask a question on our discussion board and get in touch with our community


    Ask a question

Contributing

  •   Want to create a pull request?


    Learn how to create a comprehensive and useful pull request (PR)


    Create a pull request

Checklist

Before interacting within the project, please consider the following questions to ensure you're using the correct issue template and providing all necessary information.

Issues, discussions, and comments are forever

Please note that everything you write is permanent and will remain for everyone to read – forever. Therefore, please always be nice and constructive, follow our contribution guidelines, and comply with our Code of Conduct.

Before creating an issue

  • Are you using the appropriate issue template, or is there another issue template that better fits the context of your request?
  • Have you checked if a similar bug report or change request has already been created, or have you stumbled upon something that might be related?
  • Did you fill out every field as requested and provide all additional information needed to comprehend your request?

Before asking a question

  • Is the topic a question for our discussion board, or is it a bug report or change request that should be raised on our issue tracker?
  • Is there an open discussion on the topic of your request? If the answer is yes, does your question match the direction of the discussion, or should you open a new discussion?
  • Did you provide our community with all the necessary information to understand your question and help you quickly, or can you make it easier to help you?

Before commenting

  • Is your comment relevant to the topic of the current page, post, issue, or discussion, or is it better to create a new issue or discussion?
  • Does your comment add value to the conversation? Is it constructive and respectful to our community and maintainers? Could you just use a reaction instead?

Rights and responsibilities

As maintainers, we are entrusted with the responsibility to moderate communication within our community, including the authority to close, remove, reject, or edit issues, discussions, comments, commits, and to block users who do not align with our contribution guidelines and our Code of Conduct. This role requires us to be actively involved in maintaining the integrity and positive atmosphere of our community. Upholding these standards decisively ensures a respectful and inclusive environment for all members.

Code of Conduct

Our Code of Conduct outlines the expectation for all community members to treat one another with respect, employing inclusive and welcoming language. Our commitment is to foster a positive and supportive environment, free of inappropriate, offensive, or harmful behavior.

We take any violations seriously and will take appropriate action in response to uphold these values.1

Incomplete issues and duplicates

We have invested significant time and effort in the setup of our contribution process, ensuring that we assess the essential requirements for reviewing and responding to issues effectively. Each field in our issue templates is thoughtfully designed to help us fully understand your concerns and the nature of your matter. We encourage all members to utilize the search function before submitting new issues or starting discussions to help avoid duplicates. Your cooperation is crucial in keeping our community's discussions constructive and organized.

  • Mandatory completion of issue templates: We need all of the information required in our issue templates because it ensures that every user and maintainer, regardless of their experience, can understand the content and severity of your bug report or change request.

  • Closing incomplete issues: We reserve the right to close issues lacking essential information, such as but not limited to [minimal reproductions] or those not adhering to the quality standards and requirements specified in our issue templates. Such issues can be reopened once the missing information has been provided.

  • Handling duplicates: To maintain organized and efficient communication within our issue tracker and discussion board, we reserve the right to close any duplicated issues or lock duplicated discussions. Opening multiple channels to ask the same question or report the same issue across different forums hinders our ability to manage and address community concerns effectively. This approach is vital for efficient time management, as duplicated questions can consume the time of multiple team members simultaneously. Ensuring that each issue or discussion is unique and progresses with new information helps us to maintain focus and support our community.

    We further reserve the right to immediately close discussions or issues that are reopened without providing new information or simply because users have not yet received a response to their issue/question, as the issue is marked as incomplete.

  • Limitations of automated tools: While we believe in the value and efficiency that automated tools bring to identifying potential issues (such as those identified by Lighthouse, Accessibility tools, and others), simply submitting an issue generated by these tools does not constitute a complete bug report. These tools sometimes produce verbose outputs and may include false positives, which necessitate a critical evaluation. You are of course welcome to attach generated reports to your issue. However, this does not substitute the requirement for a minimal reproduction or a thorough discussion of the findings. We reserve the right to mark these issues as incomplete and close them. This practice ensures that we are addressing genuine concerns with precision and clarity, rather than navigating through extensive automated outputs.


  1. Warning and blocking policy: Given the increasing popularity of our project and our commitment to a healthy community, we've defined clear guidelines on how we proceed with violations:

    1.1. First warning: Users displaying repeated inappropriate, offensive, or harmful behavior will receive a first warning. This warning serves as a formal notice that their behavior is not in alignment with our community standards and Code of Conduct. The first warning is permanent.

    1.2. Second warning and opportunity for resolution: If the behavior persists, a second warning will be issued. Upon receiving the second warning, the user will be given a 5-day period for reflection, during which they are encouraged to publicly explain or apologize for their actions. This period is designed to offer an opportunity for openly clearing out any misunderstanding.

    1.3. Blocking: Should there be no response or improvement in behavior following the second warning, we reserve the right to block the user from the community and repository. Blocking is considered a last resort, used only when absolutely necessary to protect the community's integrity and positive atmosphere.

    Blocking has been an exceptionally rare necessity in our overwhelmingly positive community, highlighting our preference for constructive dialogue and mutual respect. It aims to protect our community members and team. 

\ No newline at end of file + Contributing - Kyverno Envoy Plugin

Contributing

Kyverno Envoy Plugin, developed by Kyverno, is an advanced end-to-end testing tool for Kubernetes. Our community plays a crucial role in shaping the project by reporting bugs, suggesting features, and improving documentation.

We aim to make our issue tracker, discussion board, and documentation well-structured and easy to navigate. By following our guidelines, you can help us address your requests efficiently.

How you can contribute

We appreciate your efforts in reporting bugs, requesting features, and engaging in discussions. Here's how you can contribute:

Creating an issue

  •   Something is not working?


    Report a bug by creating an issue with a reproduction


    Report a bug

  •   Missing information in our docs?


    Report missing information or potential inconsistencies in our documentation


    Report a docs issue

  •   Want to submit an idea?


    Propose a change, feature request, or suggest an improvement


    Request a change

  •   Have a question or need help?


    Ask a question on our discussion board and get in touch with our community


    Ask a question

Contributing

  •   Want to create a pull request?


    Learn how to create a comprehensive and useful pull request (PR)


    Create a pull request

Checklist

Before interacting within the project, please consider the following questions to ensure you're using the correct issue template and providing all necessary information.

Issues, discussions, and comments are forever

Please note that everything you write is permanent and will remain for everyone to read – forever. Therefore, please always be nice and constructive, follow our contribution guidelines, and comply with our Code of Conduct.

Before creating an issue

  • Are you using the appropriate issue template, or is there another issue template that better fits the context of your request?
  • Have you checked if a similar bug report or change request has already been created, or have you stumbled upon something that might be related?
  • Did you fill out every field as requested and provide all additional information needed to comprehend your request?

Before asking a question

  • Is the topic a question for our discussion board, or is it a bug report or change request that should be raised on our issue tracker?
  • Is there an open discussion on the topic of your request? If the answer is yes, does your question match the direction of the discussion, or should you open a new discussion?
  • Did you provide our community with all the necessary information to understand your question and help you quickly, or can you make it easier to help you?

Before commenting

  • Is your comment relevant to the topic of the current page, post, issue, or discussion, or is it better to create a new issue or discussion?
  • Does your comment add value to the conversation? Is it constructive and respectful to our community and maintainers? Could you just use a reaction instead?

Rights and responsibilities

As maintainers, we are entrusted with the responsibility to moderate communication within our community, including the authority to close, remove, reject, or edit issues, discussions, comments, commits, and to block users who do not align with our contribution guidelines and our Code of Conduct. This role requires us to be actively involved in maintaining the integrity and positive atmosphere of our community. Upholding these standards decisively ensures a respectful and inclusive environment for all members.

Code of Conduct

Our Code of Conduct outlines the expectation for all community members to treat one another with respect, employing inclusive and welcoming language. Our commitment is to foster a positive and supportive environment, free of inappropriate, offensive, or harmful behavior.

We take any violations seriously and will take appropriate action in response to uphold these values.1

Incomplete issues and duplicates

We have invested significant time and effort in the setup of our contribution process, ensuring that we assess the essential requirements for reviewing and responding to issues effectively. Each field in our issue templates is thoughtfully designed to help us fully understand your concerns and the nature of your matter. We encourage all members to utilize the search function before submitting new issues or starting discussions to help avoid duplicates. Your cooperation is crucial in keeping our community's discussions constructive and organized.

  • Mandatory completion of issue templates: We need all of the information required in our issue templates because it ensures that every user and maintainer, regardless of their experience, can understand the content and severity of your bug report or change request.

  • Closing incomplete issues: We reserve the right to close issues lacking essential information, such as but not limited to [minimal reproductions] or those not adhering to the quality standards and requirements specified in our issue templates. Such issues can be reopened once the missing information has been provided.

  • Handling duplicates: To maintain organized and efficient communication within our issue tracker and discussion board, we reserve the right to close any duplicated issues or lock duplicated discussions. Opening multiple channels to ask the same question or report the same issue across different forums hinders our ability to manage and address community concerns effectively. This approach is vital for efficient time management, as duplicated questions can consume the time of multiple team members simultaneously. Ensuring that each issue or discussion is unique and progresses with new information helps us to maintain focus and support our community.

    We further reserve the right to immediately close discussions or issues that are reopened without providing new information or simply because users have not yet received a response to their issue/question, as the issue is marked as incomplete.

  • Limitations of automated tools: While we believe in the value and efficiency that automated tools bring to identifying potential issues (such as those identified by Lighthouse, Accessibility tools, and others), simply submitting an issue generated by these tools does not constitute a complete bug report. These tools sometimes produce verbose outputs and may include false positives, which necessitate a critical evaluation. You are of course welcome to attach generated reports to your issue. However, this does not substitute the requirement for a minimal reproduction or a thorough discussion of the findings. We reserve the right to mark these issues as incomplete and close them. This practice ensures that we are addressing genuine concerns with precision and clarity, rather than navigating through extensive automated outputs.


  1. Warning and blocking policy: Given the increasing popularity of our project and our commitment to a healthy community, we've defined clear guidelines on how we proceed with violations:

    1.1. First warning: Users displaying repeated inappropriate, offensive, or harmful behavior will receive a first warning. This warning serves as a formal notice that their behavior is not in alignment with our community standards and Code of Conduct. The first warning is permanent.

    1.2. Second warning and opportunity for resolution: If the behavior persists, a second warning will be issued. Upon receiving the second warning, the user will be given a 5-day period for reflection, during which they are encouraged to publicly explain or apologize for their actions. This period is designed to offer an opportunity for openly clearing out any misunderstanding.

    1.3. Blocking: Should there be no response or improvement in behavior following the second warning, we reserve the right to block the user from the community and repository. Blocking is considered a last resort, used only when absolutely necessary to protect the community's integrity and positive atmosphere.

    Blocking has been an exceptionally rare necessity in our overwhelmingly positive community, highlighting our preference for constructive dialogue and mutual respect. It aims to protect our community members and team. 

\ No newline at end of file diff --git a/main/community/index.html b/main/community/index.html index b337d32..6bcf1ba 100644 --- a/main/community/index.html +++ b/main/community/index.html @@ -1 +1 @@ - Community - Kyverno Envoy Plugin

Community

The Kyverno Envoy Plugin has a growing community and we would definitely love to see you join and contribute.

Everyone is welcome to make suggestions, report bugs, open feature requests, contribute code or docs, participate in discussions, write blogs or anything that can benefit the project.


The Kyverno Envoy Plugin is built and maintained under the Kyverno umbrella but decisions are

Community driven

Everyone's voice matters


Slack channel

Join our slack channel #kyverno to meet with users, contributors and maintainers.

RoadMap

For detailed information on our planned features and upcoming updates, please view our Roadmap.

Contributing

Please read the contributing guide for details around:

  1. Code of Conduct
  2. Code Culture
  3. Details on how to contribute

Adopters

If you are using the Kyverno Envoy Plugin and want to share it publicly we always appreciate a bit of support. Pull requests to the ADOPTERS LIST will put a smile on our faces 😄

\ No newline at end of file + Community - Kyverno Envoy Plugin

Community

The Kyverno Envoy Plugin has a growing community and we would definitely love to see you join and contribute.

Everyone is welcome to make suggestions, report bugs, open feature requests, contribute code or docs, participate in discussions, write blogs or anything that can benefit the project.


The Kyverno Envoy Plugin is built and maintained under the Kyverno umbrella but decisions are

Community driven

Everyone's voice matters


Slack channel

Join our slack channel #kyverno to meet with users, contributors and maintainers.

RoadMap

For detailed information on our planned features and upcoming updates, please view our Roadmap.

Contributing

Please read the contributing guide for details around:

  1. Code of Conduct
  2. Code Culture
  3. Details on how to contribute

Adopters

If you are using the Kyverno Envoy Plugin and want to share it publicly we always appreciate a bit of support. Pull requests to the ADOPTERS LIST will put a smile on our faces 😄

\ No newline at end of file diff --git a/main/community/making-a-pull-request/index.html b/main/community/making-a-pull-request/index.html index 981ebba..2a8588e 100644 --- a/main/community/making-a-pull-request/index.html +++ b/main/community/making-a-pull-request/index.html @@ -1,4 +1,4 @@ - Making a pull request - Kyverno Envoy Plugin

Pull Requests

You can contribute by making a pull request that will be reviewed by maintainers and integrated into the main repository when the changes made are approved. You can contribute bug fixes, documentation changes, or new functionalities.

Considering a pull request

Before deciding to spend effort on making changes and creating a pull request, please discuss what you intend to do. If you are responding to what you think might be a bug, please issue a bug report first. If you intend to work on documentation, create a documentation issue. If you want to work on a new feature, please create a change request.

Keep in mind the guidance given and let people advise you. It might be that there are easier solutions to the problem you perceive and want to address. It might be that what you want to achieve can already be done by configuration or [customization].

Learning about pull requests

Pull requests are a concept layered on top of Git by services that provide Git hosting. Before you consider making a pull request, you should familiarize yourself with the documentation on GitHub, the service we are using. The following articles are of particular importance:

  1. Forking a repository
  2. Creating a pull request from a fork
  3. Creating a pull request

Note that they provide tailored documentation for different operating systems and different ways of interacting with GitHub. We do our best in the documentation here to describe the process as it applies but cannot cover all possible combinations of tools and ways of doing things. It is also important that you understand the concept of a pull-request in general before continuing.

Pull request process

In the following, we describe the general process for making pull requests. The aim here is to provide the 30k ft overview before describing details later on.

Preparing changes and draft PR

The diagram below describes what typically happens to repositories in the process or preparing a pull request. We will be discussing the review-revise process below. It is important that you understand the overall process first before you worry about specific commands. This is why we cover this first before providing instructions below.

sequenceDiagram
+ Making a pull request - Kyverno Envoy Plugin      

Pull Requests

You can contribute by making a pull request that will be reviewed by maintainers and integrated into the main repository when the changes made are approved. You can contribute bug fixes, documentation changes, or new functionalities.

Considering a pull request

Before deciding to spend effort on making changes and creating a pull request, please discuss what you intend to do. If you are responding to what you think might be a bug, please issue a bug report first. If you intend to work on documentation, create a documentation issue. If you want to work on a new feature, please create a change request.

Keep in mind the guidance given and let people advise you. It might be that there are easier solutions to the problem you perceive and want to address. It might be that what you want to achieve can already be done by configuration or [customization].

Learning about pull requests

Pull requests are a concept layered on top of Git by services that provide Git hosting. Before you consider making a pull request, you should familiarize yourself with the documentation on GitHub, the service we are using. The following articles are of particular importance:

  1. Forking a repository
  2. Creating a pull request from a fork
  3. Creating a pull request

Note that they provide tailored documentation for different operating systems and different ways of interacting with GitHub. We do our best in the documentation here to describe the process as it applies but cannot cover all possible combinations of tools and ways of doing things. It is also important that you understand the concept of a pull-request in general before continuing.

Pull request process

In the following, we describe the general process for making pull requests. The aim here is to provide the 30k ft overview before describing details later on.

Preparing changes and draft PR

The diagram below describes what typically happens to repositories in the process or preparing a pull request. We will be discussing the review-revise process below. It is important that you understand the overall process first before you worry about specific commands. This is why we cover this first before providing instructions below.

sequenceDiagram
   autonumber
 
   participant upstream
diff --git a/main/community/reporting-a-bug/index.html b/main/community/reporting-a-bug/index.html
index 6784e51..3d8761b 100644
--- a/main/community/reporting-a-bug/index.html
+++ b/main/community/reporting-a-bug/index.html
@@ -1 +1 @@
- Reporting a bug - Kyverno Envoy Plugin      

Bug Reports

If you think you have discovered a bug, you can help us by submitting an issue in our public issue tracker, following this guide.

Before Creating an Issue

With numerous users, issues are created regularly. The maintainers of this project strive to address bugs promptly. By following this guide, you will know exactly what information we need to help you quickly.

Please do the following before creating an issue:

Upgrade to Latest Version

Chances are that the bug you discovered was already fixed in a subsequent version. Before reporting an issue, ensure that you're running the latest version.

Bug fixes are not backported

Please understand that only bugs that occur in the latest version will be addressed. Also, to reduce duplicate efforts, fixes cannot always be backported to earlier versions.

Remove Customizations

If you're using customizations like additional configurations, remove them before reporting a bug. We can't offer official support for bugs that might hide in your overrides, so make sure to omit custom settings from your configuration files.

Don't be shy to ask on our discussion board for help if you run into problems.

Search for Solutions

At this stage, we know that the problem persists in the latest version and is not caused by any of your customizations. However, the problem might result from a small typo or a syntactical error in a configuration file.

Before creating a bug report, save time for us and yourself by doing some research:

  1. Search our documentation for relevant sections related to your problem. Ensure everything is configured correctly.
  2. [Search our issue tracker] as another user might have already reported the same problem.
  3. [Search our discussion board] to see if other users are facing similar issues and find possible solutions.

Keep track of all search terms and relevant links; you'll need them in the bug report.


If you still haven't found a solution to your problem, create an issue. It's now likely that you've encountered something new. Read the following section to learn how to create a complete and helpful bug report.

Issue Template

We have created a new issue template to make the bug reporting process as simple as possible and more efficient for our community and us. It consists of the following parts:

Title

A good title is short and descriptive. It should be a one-sentence executive summary of the issue, so the impact and severity of the bug can be inferred from the title.

Example
Clear apply command fails with specific CRD
Wordy The apply command fails when used with a certain Custom Resource Definition
Unclear Command does not work
Useless Help

Context optional

Before describing the bug, you can provide additional context to help us understand what you were trying to achieve. Explain the circumstances under which the bug happens, and what you think might be relevant. Don't describe the bug here.

Bug Description

Provide a clear, focused, specific, and concise summary of the bug you encountered. Explain why you think this is a bug that should be reported, and not to one of its dependencies. Follow these principles:

  • Explain the what, not the how – don't explain how to reproduce the bug here, we're getting there. Focus on articulating the problem and its impact.
  • Keep it short and concise – if the bug can be precisely explained in one or two sentences, perfect. Don't inflate it.
  • One bug at a time – if you encounter several unrelated bugs, create separate issues for them.

Share links to relevant sections of our documentation and any related issues or discussions. This helps us improve our documentation and understand the problem better.

Reproduction

A minimal reproduction is essential for a well-written bug report, as it allows us to recreate the conditions necessary to inspect the bug. Follow the guide to create a reproduction:

[ Create reproduction][Create reproduction]{ .md-button .md-button--primary }

After creating the reproduction, you should have a .zip file, ideally not larger than 1 MB. Drag and drop the .zip file into the issue field, which will automatically upload it to GitHub.

Don't share links to repositories

While linking to a repository is a common practice, we currently don't support this. The reproduction, created using the built-in info plugin, contains all necessary environment information.

Steps to Reproduce

List specific steps to follow when running your reproduction to observe the bug. Keep the steps concise and ensure nothing is left out. Use simple language and focus on continuity.

Browser optional

If the bug only occurs in specific browsers, let us know which ones are affected. This field is optional, as it is only relevant for bugs that do not involve a crash when previewing or building your site.

Incognito Mode

Verify that the bug is not caused by a browser extension by switching to incognito mode. If the bug disappears, it is likely caused by an extension.

Checklist

Before submitting, ensure you have:

  • Followed this guide thoroughly
  • Provided all necessary information
  • Created a minimal reproduction

Thanks for following the guide and creating a high-quality bug report. We will take it from here.

\ No newline at end of file + Reporting a bug - Kyverno Envoy Plugin

Bug Reports

If you think you have discovered a bug, you can help us by submitting an issue in our public issue tracker, following this guide.

Before Creating an Issue

With numerous users, issues are created regularly. The maintainers of this project strive to address bugs promptly. By following this guide, you will know exactly what information we need to help you quickly.

Please do the following before creating an issue:

Upgrade to Latest Version

Chances are that the bug you discovered was already fixed in a subsequent version. Before reporting an issue, ensure that you're running the latest version.

Bug fixes are not backported

Please understand that only bugs that occur in the latest version will be addressed. Also, to reduce duplicate efforts, fixes cannot always be backported to earlier versions.

Remove Customizations

If you're using customizations like additional configurations, remove them before reporting a bug. We can't offer official support for bugs that might hide in your overrides, so make sure to omit custom settings from your configuration files.

Don't be shy to ask on our discussion board for help if you run into problems.

Search for Solutions

At this stage, we know that the problem persists in the latest version and is not caused by any of your customizations. However, the problem might result from a small typo or a syntactical error in a configuration file.

Before creating a bug report, save time for us and yourself by doing some research:

  1. Search our documentation for relevant sections related to your problem. Ensure everything is configured correctly.
  2. [Search our issue tracker] as another user might have already reported the same problem.
  3. [Search our discussion board] to see if other users are facing similar issues and find possible solutions.

Keep track of all search terms and relevant links; you'll need them in the bug report.


If you still haven't found a solution to your problem, create an issue. It's now likely that you've encountered something new. Read the following section to learn how to create a complete and helpful bug report.

Issue Template

We have created a new issue template to make the bug reporting process as simple as possible and more efficient for our community and us. It consists of the following parts:

Title

A good title is short and descriptive. It should be a one-sentence executive summary of the issue, so the impact and severity of the bug can be inferred from the title.

Example
Clear apply command fails with specific CRD
Wordy The apply command fails when used with a certain Custom Resource Definition
Unclear Command does not work
Useless Help

Context optional

Before describing the bug, you can provide additional context to help us understand what you were trying to achieve. Explain the circumstances under which the bug happens, and what you think might be relevant. Don't describe the bug here.

Bug Description

Provide a clear, focused, specific, and concise summary of the bug you encountered. Explain why you think this is a bug that should be reported, and not to one of its dependencies. Follow these principles:

  • Explain the what, not the how – don't explain how to reproduce the bug here, we're getting there. Focus on articulating the problem and its impact.
  • Keep it short and concise – if the bug can be precisely explained in one or two sentences, perfect. Don't inflate it.
  • One bug at a time – if you encounter several unrelated bugs, create separate issues for them.

Share links to relevant sections of our documentation and any related issues or discussions. This helps us improve our documentation and understand the problem better.

Reproduction

A minimal reproduction is essential for a well-written bug report, as it allows us to recreate the conditions necessary to inspect the bug. Follow the guide to create a reproduction:

[ Create reproduction][Create reproduction]{ .md-button .md-button--primary }

After creating the reproduction, you should have a .zip file, ideally not larger than 1 MB. Drag and drop the .zip file into the issue field, which will automatically upload it to GitHub.

Don't share links to repositories

While linking to a repository is a common practice, we currently don't support this. The reproduction, created using the built-in info plugin, contains all necessary environment information.

Steps to Reproduce

List specific steps to follow when running your reproduction to observe the bug. Keep the steps concise and ensure nothing is left out. Use simple language and focus on continuity.

Browser optional

If the bug only occurs in specific browsers, let us know which ones are affected. This field is optional, as it is only relevant for bugs that do not involve a crash when previewing or building your site.

Incognito Mode

Verify that the bug is not caused by a browser extension by switching to incognito mode. If the bug disappears, it is likely caused by an extension.

Checklist

Before submitting, ensure you have:

  • Followed this guide thoroughly
  • Provided all necessary information
  • Created a minimal reproduction

Thanks for following the guide and creating a high-quality bug report. We will take it from here.

\ No newline at end of file diff --git a/main/community/reporting-a-docs-issue/index.html b/main/community/reporting-a-docs-issue/index.html index 90759e7..4c44032 100644 --- a/main/community/reporting-a-docs-issue/index.html +++ b/main/community/reporting-a-docs-issue/index.html @@ -1 +1 @@ - Reporting a docs issue - Kyverno Envoy Plugin

Documentation Issues

The documentation includes extensive information on features, configurations, customizations, and more. If you have found an inconsistency or see room for improvement, please follow this guide to submit an issue on our issue tracker.

Issue Template

Reporting a documentation issue is usually less involved than reporting a bug, as we don't need a [reproduction]. Please thoroughly read this guide before creating a new documentation issue, and provide the following information as part of the issue:

Title

A good title should be a short, one-sentence description of the issue, containing all relevant information and keywords to simplify the search in our issue tracker.

Example
Clear Clarify resource templating setup
Unclear Missing information in the docs
Useless Help

Description

Provide a clear and concise summary of the inconsistency or issue you encountered in the documentation or the documentation section that needs improvement. Explain why you think the documentation should be adjusted and describe the severity of the issue:

  • Keep it short and concise – if the inconsistency or issue can be precisely explained in one or two sentences, perfect. Maintainers and future users will be grateful for having to read less.
  • One issue at a time – if you encounter several unrelated inconsistencies, please create separate issues for them.

Why we need this: describing the problem clearly and concisely is a prerequisite for improving our documentation – we need to understand what's wrong so we can fix it.

After you describe the documentation section that needs to be adjusted, share the link to this specific documentation section and other possibly related sections. Use anchor links (permanent links) where possible, as it simplifies discovery.

Why we need this: providing the links to the documentation helps us understand which sections of our documentation need to be adjusted, extended, or overhauled.

Proposed Change optional

Now that you have provided us with the description and links to the documentation sections, you can help us, maintainers, and the community by proposing an improvement. You can sketch out rough ideas or write a concrete proposal. This field is optional but very helpful.

Why we need this: an improvement proposal can be beneficial for other users who encounter the same issue, as they offer solutions before we maintainers can update the documentation.

Checklist

Thanks for following the guide and providing valuable feedback for our documentation – you are almost done. The checklist ensures that you have read this guide and have worked to your best knowledge to provide us with every piece of information we need to improve it.

We'll take it from here.

\ No newline at end of file + Reporting a docs issue - Kyverno Envoy Plugin

Documentation Issues

The documentation includes extensive information on features, configurations, customizations, and more. If you have found an inconsistency or see room for improvement, please follow this guide to submit an issue on our issue tracker.

Issue Template

Reporting a documentation issue is usually less involved than reporting a bug, as we don't need a [reproduction]. Please thoroughly read this guide before creating a new documentation issue, and provide the following information as part of the issue:

Title

A good title should be a short, one-sentence description of the issue, containing all relevant information and keywords to simplify the search in our issue tracker.

Example
Clear Clarify resource templating setup
Unclear Missing information in the docs
Useless Help

Description

Provide a clear and concise summary of the inconsistency or issue you encountered in the documentation or the documentation section that needs improvement. Explain why you think the documentation should be adjusted and describe the severity of the issue:

  • Keep it short and concise – if the inconsistency or issue can be precisely explained in one or two sentences, perfect. Maintainers and future users will be grateful for having to read less.
  • One issue at a time – if you encounter several unrelated inconsistencies, please create separate issues for them.

Why we need this: describing the problem clearly and concisely is a prerequisite for improving our documentation – we need to understand what's wrong so we can fix it.

After you describe the documentation section that needs to be adjusted, share the link to this specific documentation section and other possibly related sections. Use anchor links (permanent links) where possible, as it simplifies discovery.

Why we need this: providing the links to the documentation helps us understand which sections of our documentation need to be adjusted, extended, or overhauled.

Proposed Change optional

Now that you have provided us with the description and links to the documentation sections, you can help us, maintainers, and the community by proposing an improvement. You can sketch out rough ideas or write a concrete proposal. This field is optional but very helpful.

Why we need this: an improvement proposal can be beneficial for other users who encounter the same issue, as they offer solutions before we maintainers can update the documentation.

Checklist

Thanks for following the guide and providing valuable feedback for our documentation – you are almost done. The checklist ensures that you have read this guide and have worked to your best knowledge to provide us with every piece of information we need to improve it.

We'll take it from here.

\ No newline at end of file diff --git a/main/community/requesting-a-change/index.html b/main/community/requesting-a-change/index.html index 3c6d584..5cb8217 100644 --- a/main/community/requesting-a-change/index.html +++ b/main/community/requesting-a-change/index.html @@ -1 +1 @@ - Requesting a change - Kyverno Envoy Plugin

Change Requests

We value every idea or contribution from our community. Please follow this guide before submitting your change request in our public issue tracker. This helps us better understand the proposed change and how it will benefit our community.

Before Creating an Issue

Before you invest time in submitting a change request, answer these questions to determine if your idea is a good fit and matches the project's philosophy and tone.

It's Not a Bug, It's a Feature

Change requests suggest minor adjustments, new features, or influence the project's direction. They are not intended for reporting bugs. Refer to our bug reporting guide for that.

Look for Sources of Inspiration

If your idea is implemented in another tool or framework, collect information on its implementation. This helps us evaluate its fit more quickly.

Connect with Our Community

Our discussion board is the best place to connect with our community. Seeking input from other users helps implement features that benefit a larger number of users.

Start a discussion

Issue Template

After doing the preliminary work, create a change request. Follow these steps:

Title

A good title is short and descriptive, summarizing the idea so the potential impact and benefit can be inferred.

Example
Clear Support for resource templating
Wordy Add support for templating resources for easier testing
Unclear Improve templating
Useless Help

Context optional

Provide additional context to help us understand what you are trying to achieve. Explain the circumstances and relevant settings without describing the change request itself.

Description

Provide a detailed and clear description of your idea. Explain why your idea is relevant and should be implemented here, not in one of its dependencies.

  • Explain the what, not the why – focus on describing the change request precisely.
  • Keep it short and concise – be brief and to the point.
  • One idea at a time – if you have multiple ideas, open separate change requests for each.

Provide any relevant links to issues, discussions, or documentation sections related to your change request. This helps us gain additional context.

Use Cases

Explain how your change request would work from an author's and user's perspective. What is the expected impact, and why does it benefit other users? Would it potentially break existing functionality?

Visuals optional

If you have any visuals, such as sketches, screenshots, mockups, or external assets, present them in this section. If you have seen this change used in other tools, showcase and describe its implementation.

Checklist

Thanks for following the guide and creating a high-quality change request. The checklist ensures that you have read this guide and provided all necessary information for us to review your idea.

We'll take it from here.

Rejected Requests

Your change request got rejected? We're sorry for that. We understand it can be frustrating, but we always need to consider the needs of our entire community. If you're unsure why your change request was rejected, please ask for clarification.

We consider the following principles when evaluating change requests:

  • Alignment with the project's vision and tone
  • Compatibility with existing features and plugins
  • Compatibility with all screen sizes and browsers
  • Effort of implementation and maintenance
  • Usefulness to the majority of users
  • Simplicity and ease of use
  • Accessibility

If your idea was rejected, you can always implement it via [customization]. If you're unsure how or want to know if someone has already done it, get in touch with our community on the discussion board.

\ No newline at end of file + Requesting a change - Kyverno Envoy Plugin

Change Requests

We value every idea or contribution from our community. Please follow this guide before submitting your change request in our public issue tracker. This helps us better understand the proposed change and how it will benefit our community.

Before Creating an Issue

Before you invest time in submitting a change request, answer these questions to determine if your idea is a good fit and matches the project's philosophy and tone.

It's Not a Bug, It's a Feature

Change requests suggest minor adjustments, new features, or influence the project's direction. They are not intended for reporting bugs. Refer to our bug reporting guide for that.

Look for Sources of Inspiration

If your idea is implemented in another tool or framework, collect information on its implementation. This helps us evaluate its fit more quickly.

Connect with Our Community

Our discussion board is the best place to connect with our community. Seeking input from other users helps implement features that benefit a larger number of users.

Start a discussion

Issue Template

After doing the preliminary work, create a change request. Follow these steps:

Title

A good title is short and descriptive, summarizing the idea so the potential impact and benefit can be inferred.

Example
Clear Support for resource templating
Wordy Add support for templating resources for easier testing
Unclear Improve templating
Useless Help

Context optional

Provide additional context to help us understand what you are trying to achieve. Explain the circumstances and relevant settings without describing the change request itself.

Description

Provide a detailed and clear description of your idea. Explain why your idea is relevant and should be implemented here, not in one of its dependencies.

  • Explain the what, not the why – focus on describing the change request precisely.
  • Keep it short and concise – be brief and to the point.
  • One idea at a time – if you have multiple ideas, open separate change requests for each.

Provide any relevant links to issues, discussions, or documentation sections related to your change request. This helps us gain additional context.

Use Cases

Explain how your change request would work from an author's and user's perspective. What is the expected impact, and why does it benefit other users? Would it potentially break existing functionality?

Visuals optional

If you have any visuals, such as sketches, screenshots, mockups, or external assets, present them in this section. If you have seen this change used in other tools, showcase and describe its implementation.

Checklist

Thanks for following the guide and creating a high-quality change request. The checklist ensures that you have read this guide and provided all necessary information for us to review your idea.

We'll take it from here.

Rejected Requests

Your change request got rejected? We're sorry for that. We understand it can be frustrating, but we always need to consider the needs of our entire community. If you're unsure why your change request was rejected, please ask for clarification.

We consider the following principles when evaluating change requests:

  • Alignment with the project's vision and tone
  • Compatibility with existing features and plugins
  • Compatibility with all screen sizes and browsers
  • Effort of implementation and maintenance
  • Usefulness to the majority of users
  • Simplicity and ease of use
  • Accessibility

If your idea was rejected, you can always implement it via [customization]. If you're unsure how or want to know if someone has already done it, get in touch with our community on the discussion board.

\ No newline at end of file diff --git a/main/index.html b/main/index.html index e652a1b..ebebd34 100644 --- a/main/index.html +++ b/main/index.html @@ -1,4 +1,4 @@ - Kyverno Envoy Plugin - Policy based authz! - Kyverno Envoy Plugin

Kyverno Envoy plugin

Kyverno policy based authz... in a mesh !

Get started Learn more

Everything you would expect

  • Easy to install

    Install locally using a package manager like brew or nix, or simply download the binary from one of our releases. If you want to run using a Docker image, we have that too.

  • Easy to use

    Write tests in minutes, not hours. All it takes is a YAML file to define the steps of a test. Chainsaw will do the rest, no need to learn a programing language or write a single line of code!

  • Comprehensive reporting

    Understand and diagnose failures easily, thanks to a comprehensive output showing precisely what failed and why. Generate JUnit compatible reports to integrate with other test reporting tools.

  • Resource templating

    Kubernetes is all about resouces and tests need to work with resources. Chainsaw has built-in support for bindings, operation outputs and resource templating to describe complex test scenarios.

  • Positive testing

    Create, update, delete resources and assert your controller reconciles the desired and observed states in the expected way.

  • Negative testing

    Try submitting invalid resources, invalid changes, or other disallowed actions and make sure they are rejected.

  • Stay focused

    Focus on the software you are building, write test scenarios using YAML and let Chainsaw tell you what passes or not. Integrate in your CI pipeline to prevent regressions and release with better confidence.

  • Multi cluster

    Native support for tests involving multiple clusters, either static or dynamically created ones, make Chainsaw an excellent tool for testing highly complex environments and architectures.

\ No newline at end of file +

Kyverno Envoy plugin

Kyverno policy based authz... in a mesh !

Get started Learn more

Everything you would expect

  • Easy to install

    Install locally using a package manager like brew or nix, or simply download the binary from one of our releases. If you want to run using a Docker image, we have that too.

  • Easy to use

    Write tests in minutes, not hours. All it takes is a YAML file to define the steps of a test. Chainsaw will do the rest, no need to learn a programing language or write a single line of code!

  • Comprehensive reporting

    Understand and diagnose failures easily, thanks to a comprehensive output showing precisely what failed and why. Generate JUnit compatible reports to integrate with other test reporting tools.

  • Resource templating

    Kubernetes is all about resouces and tests need to work with resources. Chainsaw has built-in support for bindings, operation outputs and resource templating to describe complex test scenarios.

  • Positive testing

    Create, update, delete resources and assert your controller reconciles the desired and observed states in the expected way.

  • Negative testing

    Try submitting invalid resources, invalid changes, or other disallowed actions and make sure they are rejected.

  • Stay focused

    Focus on the software you are building, write test scenarios using YAML and let Chainsaw tell you what passes or not. Integrate in your CI pipeline to prevent regressions and release with better confidence.

  • Multi cluster

    Native support for tests involving multiple clusters, either static or dynamically created ones, make Chainsaw an excellent tool for testing highly complex environments and architectures.

\ No newline at end of file diff --git a/main/intro/index.html b/main/intro/index.html index 79f59ac..7689d45 100644 --- a/main/intro/index.html +++ b/main/intro/index.html @@ -1 +1 @@ - Introduction - Kyverno Envoy Plugin

Introduction

The Kyverno Envoy Plugin is a powerful tool that integrates the Kyverno-json policy engine with the Envoy proxy. It allows you to enforce Kyverno policies on incoming and outgoing traffic in a service mesh environment, providing an additional layer of security and control over your applications.

Overview

Envoy is a Layer 7 proxy and communication bus tailored for large-scale, modern service-oriented architectures. Starting from version 1.7.0, Envoy includes an External Authorization filter that interfaces with an authorization service to determine the legitimacy of incoming requests.

This functionality allows authorization decisions to be offloaded to an external service, which can access the request context. The request context includes details such as the origin and destination of the network activity, as well as specifics of the network request (e.g., HTTP request). This information enables the external service to make a well-informed decision regarding the authorization of the incoming request processed by Envoy.

What is Kyverno-Envoy-Plugin?

Kyverno-envoy plugin extends Kyverno-json with a gRPC server that implements Envoy External Authorization API. This allows you to enforce Kyverno policies on incoming and outgoing traffic in a service mesh environment, providing an additional layer of security and control over your applications. You can use this version of Kyverno to enforce fine-grained, context-aware access control policies with Envoy without modifying your microservice.

How does this work?

In addition to the Envoy sidecar, your application pods will include a kyverno-envoy component, either as a sidecar or as a separate pod. This kyverno-envoy will be configured to communicate with the Kyverno-envoy-plugin gRPC server. When Envoy receives an API request intended for your microservice, it consults the Kyverno-envoy-plugin server to determine whether the request should be permitted.

Performing policy evaluations locally with Envoy is advantageous, as it eliminates the need for an additional network hop for authorization checks, thus enhancing both performance and availability.

Info

The Kyverno-Envoy-Plugin is frequently deployed in Kubernetes environments as a sidecar container or as a separate pod. Additionally, it can be used in other environments as a standalone process running alongside Envoy.

Additional Resources

See the following pages on envoyproxy.io for more information on external authorization:

\ No newline at end of file + Introduction - Kyverno Envoy Plugin

Introduction

The Kyverno Envoy Plugin is a powerful tool that integrates the Kyverno-json policy engine with the Envoy proxy. It allows you to enforce Kyverno policies on incoming and outgoing traffic in a service mesh environment, providing an additional layer of security and control over your applications.

Overview

Envoy is a Layer 7 proxy and communication bus tailored for large-scale, modern service-oriented architectures. Starting from version 1.7.0, Envoy includes an External Authorization filter that interfaces with an authorization service to determine the legitimacy of incoming requests.

This functionality allows authorization decisions to be offloaded to an external service, which can access the request context. The request context includes details such as the origin and destination of the network activity, as well as specifics of the network request (e.g., HTTP request). This information enables the external service to make a well-informed decision regarding the authorization of the incoming request processed by Envoy.

What is Kyverno-Envoy-Plugin?

Kyverno-envoy plugin extends Kyverno-json with a gRPC server that implements Envoy External Authorization API. This allows you to enforce Kyverno policies on incoming and outgoing traffic in a service mesh environment, providing an additional layer of security and control over your applications. You can use this version of Kyverno to enforce fine-grained, context-aware access control policies with Envoy without modifying your microservice.

How does this work?

In addition to the Envoy sidecar, your application pods will include a kyverno-envoy component, either as a sidecar or as a separate pod. This kyverno-envoy will be configured to communicate with the Kyverno-envoy-plugin gRPC server. When Envoy receives an API request intended for your microservice, it consults the Kyverno-envoy-plugin server to determine whether the request should be permitted.

Performing policy evaluations locally with Envoy is advantageous, as it eliminates the need for an additional network hop for authorization checks, thus enhancing both performance and availability.

Info

The Kyverno-Envoy-Plugin is frequently deployed in Kubernetes environments as a sidecar container or as a separate pod. Additionally, it can be used in other environments as a standalone process running alongside Envoy.

Additional Resources

See the following pages on envoyproxy.io for more information on external authorization:

\ No newline at end of file diff --git a/main/jp/functions/index.html b/main/jp/functions/index.html index 5dad834..81d06e6 100644 --- a/main/jp/functions/index.html +++ b/main/jp/functions/index.html @@ -1 +1 @@ - Functions - Kyverno Envoy Plugin

Functions

built-in functions

Name Signature
abs abs(number)
avg avg(array[number])
ceil ceil(number)
contains contains(array\|string, any)
ends_with ends_with(string, string)
find_first find_first(string, string, number, number)
find_last find_last(string, string, number, number)
floor floor(number)
from_items from_items(array[array])
group_by group_by(array, expref)
items items(object)
join join(string, array[string])
keys keys(object)
length length(string\|array\|object)
lower lower(string)
map map(expref, array)
max max(array[number]\|array[string])
max_by max_by(array, expref)
merge merge(object)
min min(array[number]\|array[string])
min_by min_by(array, expref)
not_null not_null(any)
pad_left pad_left(string, number, string)
pad_right pad_right(string, number, string)
replace replace(string, string, string, number)
reverse reverse(array\|string)
sort sort(array[string]\|array[number])
sort_by sort_by(array, expref)
split split(string, string, number)
starts_with starts_with(string, string)
sum sum(array[number])
to_array to_array(any)
to_number to_number(any)
to_string to_string(any)
trim trim(string, string)
trim_left trim_left(string, string)
trim_right trim_right(string, string)
type type(any)
upper upper(string)
values values(object)
zip zip(array, array)

custom functions

Name Signature
at at(array, any)
concat concat(string, string)
json_parse json_parse(string)
wildcard wildcard(string, string)

kyverno functions

Name Signature
compare compare(string, string)
equal_fold equal_fold(string, string)
replace replace(string, string, string, number)
replace_all replace_all(string, string, string)
to_upper to_upper(string)
to_lower to_lower(string)
trim trim(string, string)
trim_prefix trim_prefix(string, string)
split split(string, string)
regex_replace_all regex_replace_all(string, string\|number, string\|number)
regex_replace_all_literal regex_replace_all_literal(string, string\|number, string\|number)
regex_match regex_match(string, string\|number)
pattern_match pattern_match(string, string\|number)
label_match label_match(object, object)
to_boolean to_boolean(string)
add add(any, any)
sum sum(array)
subtract subtract(any, any)
multiply multiply(any, any)
divide divide(any, any)
modulo modulo(any, any)
round round(number, number)
base64_decode base64_decode(string)
base64_encode base64_encode(string)
time_since time_since(string, string, string)
time_now time_now()
time_now_utc time_now_utc()
path_canonicalize path_canonicalize(string)
truncate truncate(string, number)
semver_compare semver_compare(string, string)
parse_json parse_json(string)
parse_yaml parse_yaml(string)
lookup lookup(object\|array, string\|number)
items items(object\|array, string, string)
object_from_lists object_from_lists(array, array)
random random(string)
x509_decode x509_decode(string)
time_to_cron time_to_cron(string)
time_add time_add(string, string)
time_parse time_parse(string, string)
time_utc time_utc(string)
time_diff time_diff(string, string)
time_before time_before(string, string)
time_after time_after(string, string)
time_between time_between(string, string, string)
time_truncate time_truncate(string, string)
\ No newline at end of file + Functions - Kyverno Envoy Plugin

Functions

built-in functions

Name Signature
abs abs(number)
avg avg(array[number])
ceil ceil(number)
contains contains(array\|string, any)
ends_with ends_with(string, string)
find_first find_first(string, string, number, number)
find_last find_last(string, string, number, number)
floor floor(number)
from_items from_items(array[array])
group_by group_by(array, expref)
items items(object)
join join(string, array[string])
keys keys(object)
length length(string\|array\|object)
lower lower(string)
map map(expref, array)
max max(array[number]\|array[string])
max_by max_by(array, expref)
merge merge(object)
min min(array[number]\|array[string])
min_by min_by(array, expref)
not_null not_null(any)
pad_left pad_left(string, number, string)
pad_right pad_right(string, number, string)
replace replace(string, string, string, number)
reverse reverse(array\|string)
sort sort(array[string]\|array[number])
sort_by sort_by(array, expref)
split split(string, string, number)
starts_with starts_with(string, string)
sum sum(array[number])
to_array to_array(any)
to_number to_number(any)
to_string to_string(any)
trim trim(string, string)
trim_left trim_left(string, string)
trim_right trim_right(string, string)
type type(any)
upper upper(string)
values values(object)
zip zip(array, array)

custom functions

Name Signature
at at(array, any)
concat concat(string, string)
json_parse json_parse(string)
wildcard wildcard(string, string)

kyverno functions

Name Signature
compare compare(string, string)
equal_fold equal_fold(string, string)
replace replace(string, string, string, number)
replace_all replace_all(string, string, string)
to_upper to_upper(string)
to_lower to_lower(string)
trim trim(string, string)
trim_prefix trim_prefix(string, string)
split split(string, string)
regex_replace_all regex_replace_all(string, string\|number, string\|number)
regex_replace_all_literal regex_replace_all_literal(string, string\|number, string\|number)
regex_match regex_match(string, string\|number)
pattern_match pattern_match(string, string\|number)
label_match label_match(object, object)
to_boolean to_boolean(string)
add add(any, any)
sum sum(array)
subtract subtract(any, any)
multiply multiply(any, any)
divide divide(any, any)
modulo modulo(any, any)
round round(number, number)
base64_decode base64_decode(string)
base64_encode base64_encode(string)
time_since time_since(string, string, string)
time_now time_now()
time_now_utc time_now_utc()
path_canonicalize path_canonicalize(string)
truncate truncate(string, number)
semver_compare semver_compare(string, string)
parse_json parse_json(string)
parse_yaml parse_yaml(string)
lookup lookup(object\|array, string\|number)
items items(object\|array, string, string)
object_from_lists object_from_lists(array, array)
random random(string)
x509_decode x509_decode(string)
time_to_cron time_to_cron(string)
time_add time_add(string, string)
time_parse time_parse(string, string)
time_utc time_utc(string)
time_diff time_diff(string, string)
time_before time_before(string, string)
time_after time_after(string, string)
time_between time_between(string, string, string)
time_truncate time_truncate(string, string)
\ No newline at end of file diff --git a/main/jp/index.html b/main/jp/index.html index 789c126..15fc514 100644 --- a/main/jp/index.html +++ b/main/jp/index.html @@ -1 +1 @@ - Overview - Kyverno Envoy Plugin

Overview

kyverno-json uses JMESPath community edition, a modern JMESPath implementation with lexical scopes support.

The current payload, policy and rule are always available using the following builtin bindings:

Binding Usage
$payload Current payload being analysed
$policy Current policy being executed
$rule Current rule being evaluated

Warning

No protection is made to prevent you from overriding those bindings.

\ No newline at end of file + Overview - Kyverno Envoy Plugin

Overview

kyverno-json uses JMESPath community edition, a modern JMESPath implementation with lexical scopes support.

The current payload, policy and rule are always available using the following builtin bindings:

Binding Usage
$payload Current payload being analysed
$policy Current policy being executed
$rule Current rule being evaluated

Warning

No protection is made to prevent you from overriding those bindings.

\ No newline at end of file diff --git a/main/performance/index.html b/main/performance/index.html index 86eea46..f6881be 100644 --- a/main/performance/index.html +++ b/main/performance/index.html @@ -1,4 +1,4 @@ - Performance - Kyverno Envoy Plugin

Performance

This page offers guidance and best practices for benchmarking the performance of the kyverno-envoy-plugin, helping users understand the associated overhead. It outlines an example setup for conducting benchmarks, various benchmarking scenarios, and key metrics to capture for assessing the impact of the kyverno-envoy-plugin.

Benchmark Setup

The benchmark setup consists of the following components:

Sample Application

The first component is a simple Go application that provides information of books in the library books collection and exposes APIs to get, create and delete books collection. Check this out for more information about the Go test application .

Envoy

The second component is the Envoy proxy, which runs alongside the example application. The Envoy configuration defines an external authorization filter envoy.ext_authz for a gRPC authorization server. The config uses Envoy's in-built gRPC client to make external gRPC calls.

static_resources:
+ Performance - Kyverno Envoy Plugin      

Performance

This page offers guidance and best practices for benchmarking the performance of the kyverno-envoy-plugin, helping users understand the associated overhead. It outlines an example setup for conducting benchmarks, various benchmarking scenarios, and key metrics to capture for assessing the impact of the kyverno-envoy-plugin.

Benchmark Setup

The benchmark setup consists of the following components:

Sample Application

The first component is a simple Go application that provides information of books in the library books collection and exposes APIs to get, create and delete books collection. Check this out for more information about the Go test application .

Envoy

The second component is the Envoy proxy, which runs alongside the example application. The Envoy configuration defines an external authorization filter envoy.ext_authz for a gRPC authorization server. The config uses Envoy's in-built gRPC client to make external gRPC calls.

static_resources:
   listeners:
   - address:
       socket_address:
@@ -184,4 +184,4 @@
 testapp-5955cd6f8b-dbvgd   envoy                  4m           70Mi
 testapp-5955cd6f8b-dbvgd   kyverno-envoy-plugin   1m           51Mi
 testapp-5955cd6f8b-dbvgd   test-application       1m           11Mi
-

Observations: - After the load test completed and the request volume returned to normal levels, the CPU and memory utilization of the kyverno-envoy-plugin container returned to their initial values. This indicates that the kyverno-envoy-plugin can efficiently handle the increased load during the test and release the additional resources when the load subsides.

Correlation with k6 results: - The k6 script simulated a load test scenario with 100 virtual users, ramping up over 30 seconds, staying at 100 users for 1 minute, and then ramping down over 30 seconds. - During the load test, when the request volume was at its peak (100 virtual users), the kyverno-envoy-plugin container experienced a significant increase in CPU utilization, reaching 895m. - This CPU utilization spike aligns with the increased processing demand on the kyverno-envoy-plugin to evaluate the incoming requests against the configured Kyverno policies. - The memory utilization increase during the load test was relatively modest, suggesting that the policy evaluation did not significantly impact the memory requirements of the kyverno-envoy-plugin.

\ No newline at end of file +

Observations: - After the load test completed and the request volume returned to normal levels, the CPU and memory utilization of the kyverno-envoy-plugin container returned to their initial values. This indicates that the kyverno-envoy-plugin can efficiently handle the increased load during the test and release the additional resources when the load subsides.

Correlation with k6 results: - The k6 script simulated a load test scenario with 100 virtual users, ramping up over 30 seconds, staying at 100 users for 1 minute, and then ramping down over 30 seconds. - During the load test, when the request volume was at its peak (100 virtual users), the kyverno-envoy-plugin container experienced a significant increase in CPU utilization, reaching 895m. - This CPU utilization spike aligns with the increased processing demand on the kyverno-envoy-plugin to evaluate the incoming requests against the configured Kyverno policies. - The memory utilization increase during the load test was relatively modest, suggesting that the policy evaluation did not significantly impact the memory requirements of the kyverno-envoy-plugin.

\ No newline at end of file diff --git a/main/policies/asserts/index.html b/main/policies/asserts/index.html index 4aaca44..2b1df24 100644 --- a/main/policies/asserts/index.html +++ b/main/policies/asserts/index.html @@ -1,4 +1,4 @@ - Assertion trees - Kyverno Envoy Plugin

Assertion trees

Assertion trees can be used to apply complex and dynamic conditional checks using JMESPath expressions.

Assert

An assert declaration contains an any or all list in which each entry contains a:

  • check: the assertion check
  • message: an optional message

A check can contain one or more JMESPath expressions. Expressions represent projections of selected data in the JSON payload and the result of this projection is passed to descendants for further analysis.

All comparisons happen in the leaves of the assertion tree.

A simple example:

This policy checks that a pod does not use the default service account:

apiVersion: json.kyverno.io/v1alpha1
+ Assertion trees - Kyverno Envoy Plugin      

Assertion trees

Assertion trees can be used to apply complex and dynamic conditional checks using JMESPath expressions.

Assert

An assert declaration contains an any or all list in which each entry contains a:

  • check: the assertion check
  • message: an optional message

A check can contain one or more JMESPath expressions. Expressions represent projections of selected data in the JSON payload and the result of this projection is passed to descendants for further analysis.

All comparisons happen in the leaves of the assertion tree.

A simple example:

This policy checks that a pod does not use the default service account:

apiVersion: json.kyverno.io/v1alpha1
 kind: ValidatingPolicy
 metadata:
   name: assert-sample
diff --git a/main/policies/authz-policy/index.html b/main/policies/authz-policy/index.html
index 6965e4e..4ae8d85 100644
--- a/main/policies/authz-policy/index.html
+++ b/main/policies/authz-policy/index.html
@@ -1,4 +1,4 @@
- Policy Reference - Kyverno Envoy Plugin      

Policy Reference

This page provides guidance on writing policies for request content processed by the kyverno-json validating policy, utilizing Envoy’s External Authorization filter.

Writing Policies

Let start with an example policy that restricts access to an endpoint based on user's role and permissions.

apiVersion: json.kyverno.io/v1alpha1
+ Policy Reference - Kyverno Envoy Plugin      

Policy Reference

This page provides guidance on writing policies for request content processed by the kyverno-json validating policy, utilizing Envoy’s External Authorization filter.

Writing Policies

Let start with an example policy that restricts access to an endpoint based on user's role and permissions.

apiVersion: json.kyverno.io/v1alpha1
 kind: ValidatingPolicy
 metadata:
     name: checkrequest
diff --git a/main/policies/policies/index.html b/main/policies/policies/index.html
index 70ff985..39976ac 100644
--- a/main/policies/policies/index.html
+++ b/main/policies/policies/index.html
@@ -1,4 +1,4 @@
- Policy Structure - Kyverno Envoy Plugin      

Policy Structure

Kyverno policies are Kubernetes resources and can be easily managed via Kubernetes APIs, GitOps workflows, and other existing tools.

Policies that apply to JSON payload have a few differences from Kyverno policies that are applied to Kubernetes resources at admission controls.

Resource Scope

Policies that apply to JSON payloads are always cluster-wide resources.

API Group and Kind

kyverno-json policies belong to the json.kyverno.io group and can only be of kind ValidatingPolicy.

apiVersion: json.kyverno.io/v1alpha1
+ Policy Structure - Kyverno Envoy Plugin      

Policy Structure

Kyverno policies are Kubernetes resources and can be easily managed via Kubernetes APIs, GitOps workflows, and other existing tools.

Policies that apply to JSON payload have a few differences from Kyverno policies that are applied to Kubernetes resources at admission controls.

Resource Scope

Policies that apply to JSON payloads are always cluster-wide resources.

API Group and Kind

kyverno-json policies belong to the json.kyverno.io group and can only be of kind ValidatingPolicy.

apiVersion: json.kyverno.io/v1alpha1
 kind: ValidatingPolicy
 metadata:
   name: test
diff --git a/main/quick-start/index.html b/main/quick-start/index.html
index c12e802..815fde9 100644
--- a/main/quick-start/index.html
+++ b/main/quick-start/index.html
@@ -1,4 +1,4 @@
- Quick Start - Kyverno Envoy Plugin      

Quick Start

This section presumes testing is conducted with Envoy version 1.10.0 or newer.

Required tools

  1. minikube
  2. kubectl

Create a local cluster

Start minikube cluster with the following command:

minikube start
+ Quick Start - Kyverno Envoy Plugin      

Quick Start

This section presumes testing is conducted with Envoy version 1.10.0 or newer.

Required tools

  1. minikube
  2. kubectl

Create a local cluster

Start minikube cluster with the following command:

minikube start
 

Install kyverno-envoy sidecar with application

Install application with envoy and kyverno-envoy-plugin as a sidecar container.

kubectl apply -f https://raw.githubusercontent.com/kyverno/kyverno-envoy-plugin/main/quick_start.yaml 
 
The applicaition.yaml manifest defines the following resource:

  • The Deployment includes an example Go application that provides information of books in the library books collection and exposes APIs to get, create and delete books collection. Check this out for more information about the Go test application .

  • The Deployment also includes a kyverno-envoy-plugin sidecar container in addition to the Envoy sidecar container. When Envoy recevies API request destined for the Go test applicaiton, it will check with kyverno-envoy-plugin to decide if the request should be allowed and the kyverno-envoy-plugin sidecar container is configured to query Kyverno-json engine for policy decisions on incoming requests.

  • A ConfigMap policy-config is used to pass the policy to kyverno-envoy-plugin sidecar in the namespace default where the application is deployed .

  • A ConfigMap envoy-config is used to pass an Envoy configuration with an External Authorization Filter to direct authorization checks to the kyverno-envoy-plugin sidecar.

  • The Deployment also includes an init container that install iptables rules to redirect all container traffic to the Envoy proxy sidecar container , more about init container can be found here

Make Test application accessible in the cluster .

kubectl expose deployment testapp --type=NodePort --name=testapp-service --port=8080
 

Set the SERVICE_URL environment variable to the service's IP/port.

minikube:

export SERVICE_PORT=$(kubectl get service testapp -o jsonpath='{.spec.ports[?(@.port==8080)].nodePort}')
diff --git a/main/reference/apis/policy.v1alpha1/index.html b/main/reference/apis/policy.v1alpha1/index.html
new file mode 100644
index 0000000..86ece22
--- /dev/null
+++ b/main/reference/apis/policy.v1alpha1/index.html
@@ -0,0 +1 @@
+ policy (v1alpha1) - Kyverno Envoy Plugin      

v1alpha1

Resource Types

AuthorizationPolicy

AuthorizationPolicy defines an authorization policy resource

Field Type Required Inline Description
apiVersion string ✅ envoy.kyverno.io/v1alpha1
kind string ✅ AuthorizationPolicy
metadata meta/v1.ObjectMeta ✅ No description provided.
spec AuthorizationPolicySpec ✅ No description provided.

Authorization

Appears in:

Authorization defines an authorization policy rule

Field Type Required Inline Description
expression string ✅

Expression represents the expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to CEL variables as well as some other useful variables: - 'object' - The object from the incoming request. (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkrequest) CEL expressions are expected to return an envoy CheckResponse (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkresponse).

AuthorizationPolicySpec

Appears in:

AuthorizationPolicySpec defines the spec of an authorization policy

Field Type Required Inline Description
failurePolicy admissionregistration/v1.FailurePolicyType

FailurePolicy defines how to handle failures for the policy. Failures can occur from CEL expression parse errors, type check errors, runtime errors and invalid or mis-configured policy definitions. FailurePolicy does not define how validations that evaluate to false are handled. Allowed values are Ignore or Fail. Defaults to Fail.

variables []admissionregistration/v1.Variable

Variables contain definitions of variables that can be used in composition of other expressions. Each variable is defined as a named CEL expression. The variables defined here will be available under variables in other expressions of the policy except MatchConditions because MatchConditions are evaluated before the rest of the policy. The expression of a variable can refer to other variables defined earlier in the list but not those after. Thus, Variables must be sorted by the order of first appearance and acyclic.

authorizations []Authorization

Authorizations contain CEL expressions which is used to apply the authorization.

\ No newline at end of file diff --git a/main/reference/index.html b/main/reference/index.html new file mode 100644 index 0000000..fc5b1e5 --- /dev/null +++ b/main/reference/index.html @@ -0,0 +1 @@ + Reference documentation - Kyverno Envoy Plugin

Reference documentation

Info

Select an item in the navigation menu to browse a specific page.

\ No newline at end of file diff --git a/main/reference/json-schemas/index.html b/main/reference/json-schemas/index.html new file mode 100644 index 0000000..4d3c038 --- /dev/null +++ b/main/reference/json-schemas/index.html @@ -0,0 +1,26 @@ + JSON schemas - Kyverno Envoy Plugin

JSON schemas

JSON schemas for the Kyverno Envoy Plugin are available:

They can be used to enable validation and autocompletion in your IDE.

VS code

In VS code, simply add a comment on top of your YAML resources.

AuthorizationPolicy

# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/kyverno-envoy-plugin/main/.schemas/json/authorizationpolicy-envoy-v1alpha1.json
+apiVersion: envoy.kyverno.io/v1alpha1
+kind: AuthorizationPolicy
+metadata:
+  name: demo-policy.example.com
+spec:
+  variables:
+  - name: force_authorized
+    expression: object.attributes.request.http.headers[?"x-force-authorized"].orValue("") in ["enabled", "true"]
+  - name: force_unauthenticated
+    expression: object.attributes.request.http.headers[?"x-force-unauthenticated"].orValue("") in ["enabled", "true"]
+  authorizations:
+  - expression: >
+      variables.force_authorized && !variables.force_unauthenticated
+      ? envoy
+          .Allowed()
+          .WithHeader("x-validated-by", "my-security-checkpoint")
+          .WithoutHeader("x-force-authorized")
+          .WithResponseHeader("x-add-custom-response-header", "added")
+          .Response()
+          .WithMetadata({"my-new-metadata": "my-new-value"})
+      : envoy
+          .Denied(variables.force_unauthenticated ? 401 : 403)
+          .WithBody(variables.force_unauthenticated ? "Authentication Failed" : "Unauthorized Request")
+          .Response()
+
\ No newline at end of file diff --git a/main/search/search_index.json b/main/search/search_index.json index b30f1f3..e35a975 100644 --- a/main/search/search_index.json +++ b/main/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"intro/","title":"Introduction","text":"

The Kyverno Envoy Plugin is a powerful tool that integrates the Kyverno-json policy engine with the Envoy proxy. It allows you to enforce Kyverno policies on incoming and outgoing traffic in a service mesh environment, providing an additional layer of security and control over your applications.

"},{"location":"intro/#overview","title":"Overview","text":"

Envoy is a Layer 7 proxy and communication bus tailored for large-scale, modern service-oriented architectures. Starting from version 1.7.0, Envoy includes an External Authorization filter that interfaces with an authorization service to determine the legitimacy of incoming requests.

This functionality allows authorization decisions to be offloaded to an external service, which can access the request context. The request context includes details such as the origin and destination of the network activity, as well as specifics of the network request (e.g., HTTP request). This information enables the external service to make a well-informed decision regarding the authorization of the incoming request processed by Envoy.

"},{"location":"intro/#what-is-kyverno-envoy-plugin","title":"What is Kyverno-Envoy-Plugin?","text":"

Kyverno-envoy plugin extends Kyverno-json with a gRPC server that implements Envoy External Authorization API. This allows you to enforce Kyverno policies on incoming and outgoing traffic in a service mesh environment, providing an additional layer of security and control over your applications. You can use this version of Kyverno to enforce fine-grained, context-aware access control policies with Envoy without modifying your microservice.

"},{"location":"intro/#how-does-this-work","title":"How does this work?","text":"

In addition to the Envoy sidecar, your application pods will include a kyverno-envoy component, either as a sidecar or as a separate pod. This kyverno-envoy will be configured to communicate with the Kyverno-envoy-plugin gRPC server. When Envoy receives an API request intended for your microservice, it consults the Kyverno-envoy-plugin server to determine whether the request should be permitted.

Performing policy evaluations locally with Envoy is advantageous, as it eliminates the need for an additional network hop for authorization checks, thus enhancing both performance and availability.

Info

The Kyverno-Envoy-Plugin is frequently deployed in Kubernetes environments as a sidecar container or as a separate pod. Additionally, it can be used in other environments as a standalone process running alongside Envoy.

"},{"location":"intro/#additional-resources","title":"Additional Resources","text":"

See the following pages on envoyproxy.io for more information on external authorization:

  • External Authorization to learn about the External Authorization filter.
  • Network and HTTP for details on configuring the External Authorization filter.
"},{"location":"jp/","title":"Overview","text":"

kyverno-json uses JMESPath community edition, a modern JMESPath implementation with lexical scopes support.

The current payload, policy and rule are always available using the following builtin bindings:

Binding Usage $payload Current payload being analysed $policy Current policy being executed $rule Current rule being evaluated

Warning

No protection is made to prevent you from overriding those bindings.

"},{"location":"performance/","title":"Performance","text":"

This page offers guidance and best practices for benchmarking the performance of the kyverno-envoy-plugin, helping users understand the associated overhead. It outlines an example setup for conducting benchmarks, various benchmarking scenarios, and key metrics to capture for assessing the impact of the kyverno-envoy-plugin.

"},{"location":"performance/#benchmark-setup","title":"Benchmark Setup","text":"

The benchmark setup consists of the following components:

"},{"location":"performance/#sample-application","title":"Sample Application","text":"

The first component is a simple Go application that provides information of books in the library books collection and exposes APIs to get, create and delete books collection. Check this out for more information about the Go test application .

"},{"location":"performance/#envoy","title":"Envoy","text":"

The second component is the Envoy proxy, which runs alongside the example application. The Envoy configuration defines an external authorization filter envoy.ext_authz for a gRPC authorization server. The config uses Envoy's in-built gRPC client to make external gRPC calls.

static_resources:\n  listeners:\n  - address:\n      socket_address:\n        address: 0.0.0.0\n        port_value: 8000\n    filter_chains:\n    - filters:\n      - name: envoy.filters.network.http_connection_manager\n        typed_config:\n          \"@type\": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n          codec_type: auto\n          stat_prefix: ingress_http\n          route_config:\n            name: local_route\n            virtual_hosts:\n            - name: backend\n              domains:\n              - \"*\"\n              routes:\n              - match:\n                  prefix: \"/\"\n                route:\n                  cluster: service\n          http_filters:\n          - name: envoy.ext_authz\n            typed_config:\n              \"@type\": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz\n              transport_api_version: V3\n              with_request_body:\n                max_request_bytes: 8192\n                allow_partial_message: true\n              failure_mode_allow: false\n              grpc_service:\n                google_grpc:\n                  target_uri: 127.0.0.1:9191\n                  stat_prefix: ext_authz\n                timeout: 0.5s\n          - name: envoy.filters.http.router\n            typed_config:\n              \"@type\": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n  clusters:\n  - name: service\n    connect_timeout: 0.25s\n    type: strict_dns\n    lb_policy: round_robin\n    load_assignment:\n      cluster_name: service\n      endpoints:\n      - lb_endpoints:\n        - endpoint:\n            address:\n              socket_address:\n                address: 127.0.0.1\n                port_value: 8080\nadmin:\n  access_log_path: \"/dev/null\"\n  address:\n    socket_address:\n      address: 0.0.0.0\n      port_value: 8001\nlayered_runtime:\n  layers:\n    - name: static_layer_0\n      static_layer:\n        envoy:\n          resource_limits:\n            listener:\n              example_listener_name:\n                connection_limit: 10000\n        overload:\n          global_downstream_max_connections: 50000\n
"},{"location":"performance/#kyverno-envoy-plugin","title":"Kyverno-envoy-plugin","text":"

The third component is the kyverno-envoy-plugin itself, which is configured to load and enforce Kyverno policies on incoming requests.

containers:\n- name: kyverno-envoy-plugin\n  image: sanskardevops/plugin:0.0.34\n  imagePullPolicy: IfNotPresent\n  ports:\n    - containerPort: 8181\n    - containerPort: 9000\n  volumeMounts:\n    - readOnly: true\n      mountPath: /policies\n      name: policy-files\n  args:\n    - \"serve\"\n    - \"--policy=/policies/policy.yaml\"\n    - \"--address=:9000\"\n    - \"--healthaddress=:8181\"\n  livenessProbe:\n    httpGet:\n      path: /health\n      scheme: HTTP\n      port: 8181\n    initialDelaySeconds: 5\n    periodSeconds: 5\n  readinessProbe:\n    httpGet:\n      path: /health\n      scheme: HTTP\n      port: 8181\n    initialDelaySeconds: 5\n    periodSeconds: 5  \n
"},{"location":"performance/#benchmark-scenarios","title":"Benchmark Scenarios","text":"

The following scenarios should be tested to compare the performance of the kyverno-envoy-plugin under different conditions:

  1. App Only: Requests are sent directly to the application, without Envoy or the kyverno-envoy-plugin.
  2. App and Envoy: Envoy is included in the request path, but the kyverno-envoy-plugin is not (i.e., Envoy External Authorization API is disabled).
  3. App, Envoy, and Kyverno (RBAC policy): Envoy External Authorization API is enabled, and a sample real-world RBAC policy is loaded into the kyverno-envoy-plugin.
"},{"location":"performance/#load-testing-with-k6","title":"Load Testing with k6","text":"

To perform load testing, we'll use the k6 tool. Follow these steps:

  1. Install k6: Install k6 on your machine by following the instructions on the official website: https://k6.io/docs/getting-started/installation/

  2. Write the k6 script: Below is the example k6 script.

import http from 'k6/http';\nimport { check, group, sleep } from 'k6';\n\nexport const options = {\n  stages: [\n    { duration: '30s', target: 100 }, // Ramp-up to 100 virtual users over 30 seconds\n    { duration: '1m', target: 100 }, // Stay at 100 virtual users for 1 minute\n    { duration: '30s', target: 0 }, // Ramp-down to 0 virtual users over 30 seconds\n  ],\n};\n\n/*\nReplace ip for every scenerio\nexport SERVICE_PORT=$(kubectl -n demo get service testapp -o jsonpath='{.spec.ports[?(@.port==8080)].nodePort}')\nexport SERVICE_HOST=$(minikube ip)\nexport SERVICE_URL=$SERVICE_HOST:$SERVICE_PORT\necho $SERVICE_URL\n\nhttp://192.168.49.2:31541\n\n*/\nconst BASE_URL = 'http://192.168.49.2:31541'; \n\nexport default function () {\n  group('GET /book with guest token', () => {\n    const res = http.get(`${BASE_URL}/book`, {\n      headers: { 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk' },\n    });\n    check(res, {\n      'is status 200': (r) => r.status === 200,\n    });\n  });\n\n  sleep(1); // Sleep for 1 second between iterations\n}\n
  1. Run the k6 test: Run the load test with the following command:

$ k6 run -f - <<EOF\nimport http from 'k6/http';\nimport { check, group, sleep } from 'k6';\n\nexport const options = {\n  stages: [\n    { duration: '30s', target: 100 }, // Ramp-up to 100 virtual users over 30 seconds\n    { duration: '1m', target: 100 }, // Stay at 100 virtual users for 1 minute\n    { duration: '30s', target: 0 }, // Ramp-down to 0 virtual users over 30 seconds\n  ],\n};\n\n\nconst BASE_URL = 'http://192.168.49.2:31700'; // Replace with your application URL \n\nexport default function () {\n  group('GET /book with guest token', () => {\n    const res = http.get(`${BASE_URL}/book`, {\n      headers: { 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk' },\n    });\n    check(res, {\n      'is status 200': (r) => r.status === 200,\n    });\n  });\n\n  sleep(1); // Sleep for 1 second between iterations\n}\nEOF\n
4. Analyze the results: Generate an json report with detailed insight by running:

k6 run --out json=report.json k6-script.js\n
5. ***Repeat for different scenarios**:

running (2m00.6s), 000/100 VUs, 9048 complete and 0 interrupted iterations default \u2713 [======================================] 000/100 VUs 2m0s ```

running (2m00.7s), 000/100 VUs, 9031 complete and 0 interrupted iterations default \u2713 [======================================] 000/100 VUs 2m0s ```

running (2m00.2s), 000/100 VUs, 8655 complete and 0 interrupted iterations default \u2713 [======================================] 000/100 VUs 2m0s ```

"},{"location":"performance/#app-only","title":"App only","text":"

In this case , request are sent directly to the sample application ie no Envoy and Kyverno-plugin in the request path . For this run this command to apply the sample applicaition and then test with k6

$ kubectl apply -f https://raw.githubusercontent.com/kyverno/kyverno-envoy-plugin/main/tests/performance-test/manifest/app.yaml\n
Results of the k6 when only application is applied ```bash

  /\\      |\u203e\u203e| /\u203e\u203e/   /\u203e\u203e/\n

/ / | |/ / / / / \\/ | ( / \u203e\u203e / | | | (\u203e) | / __________ || _ __/ .io

execution: local script: k6-script.js output: -

scenarios: (100.00%) 1 scenario, 100 max VUs, 2m30s max duration (incl. graceful stop): * default: Up to 100 looping VUs for 2m0s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)

\u2588 GET /book with guest token

\u2713 is status 200

checks.........................: 100.00% \u2713 9048 \u2717 0 data_received..................: 2.1 MB 18 kB/s data_sent......................: 2.6 MB 21 kB/s group_duration.................: avg=1.01ms min=166.46\u00b5s med=775.01\u00b5s max=36ms p(90)=1.72ms p(95)=2.31ms http_req_blocked...............: avg=15.08\u00b5s min=1.55\u00b5s med=6.54\u00b5s max=4.09ms p(90)=12.07\u00b5s p(95)=15.25\u00b5s http_req_connecting............: avg=4.58\u00b5s min=0s med=0s max=1.57ms p(90)=0s p(95)=0s http_req_duration..............: avg=745.73\u00b5s min=103.06\u00b5s med=549.17\u00b5s max=35.88ms p(90)=1.26ms p(95)=1.75ms { expected_response:true }...: avg=745.73\u00b5s min=103.06\u00b5s med=549.17\u00b5s max=35.88ms p(90)=1.26ms p(95)=1.75ms http_req_failed................: 0.00% \u2713 0 \u2717 9048 http_req_receiving.............: avg=119.69\u00b5s min=11.33\u00b5s med=77.78\u00b5s max=10.97ms p(90)=193.73\u00b5s p(95)=285.58\u00b5s http_req_sending...............: avg=41\u00b5s min=6.96\u00b5s med=31.12\u00b5s max=2.39ms p(90)=61.88\u00b5s p(95)=78.15\u00b5s http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s http_req_waiting...............: avg=585.04\u00b5s min=75.52\u00b5s med=407.87\u00b5s max=35.84ms p(90)=965.49\u00b5s p(95)=1.33ms http_reqs......................: 9048 75.050438/s iteration_duration.............: avg=1s min=1s med=1s max=1.06s p(90)=1s p(95)=1s iterations.....................: 9048 75.050438/s vus............................: 2 min=2 max=100 vus_max........................: 100 min=100 max=100

"},{"location":"performance/#app-and-envoy","title":"App and Envoy","text":"

In this case, Kyverno-envoy-plugin is not included in the path but Envoy is but Envoy External Authorization API disabled For this run this command to apply the sample application with envoy.

$ kubectl apply -f https://raw.githubusercontent.com/kyverno/kyverno-envoy-plugin/main/tests/performance-test/manifest/app-envoy.yaml\n

Results of k6 after applying sample-application with envoy. ```bash

  /\\      |\u203e\u203e| /\u203e\u203e/   /\u203e\u203e/\n

/ / | |/ / / / / \\/ | ( / \u203e\u203e / | | | (\u203e) | / __________ || _ __/ .io

execution: local script: k6-script.js output: -

scenarios: (100.00%) 1 scenario, 100 max VUs, 2m30s max duration (incl. graceful stop): * default: Up to 100 looping VUs for 2m0s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)

\u2588 GET /book with guest token

\u2713 is status 200

checks.........................: 100.00% \u2713 9031 \u2717 0 data_received..................: 2.5 MB 21 kB/s data_sent......................: 2.6 MB 21 kB/s group_duration.................: avg=2.66ms min=457.22\u00b5s med=1.8ms max=65.53ms p(90)=4.85ms p(95)=6.58ms http_req_blocked...............: avg=12.81\u00b5s min=1.52\u00b5s med=5.98\u00b5s max=2.41ms p(90)=11.84\u00b5s p(95)=13.9\u00b5s http_req_connecting............: avg=3.82\u00b5s min=0s med=0s max=2.34ms p(90)=0s p(95)=0s http_req_duration..............: avg=2.38ms min=383.7\u00b5s med=1.58ms max=65.22ms p(90)=4.36ms p(95)=5.92ms { expected_response:true }...: avg=2.38ms min=383.7\u00b5s med=1.58ms max=65.22ms p(90)=4.36ms p(95)=5.92ms http_req_failed................: 0.00% \u2713 0 \u2717 9031 http_req_receiving.............: avg=136.3\u00b5s min=12.53\u00b5s med=76.74\u00b5s max=12.75ms p(90)=183.23\u00b5s p(95)=272.91\u00b5s http_req_sending...............: avg=41.54\u00b5s min=6.58\u00b5s med=28.1\u00b5s max=4.15ms p(90)=59.62\u00b5s p(95)=74.85\u00b5s http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s http_req_waiting...............: avg=2.2ms min=349.23\u00b5s med=1.43ms max=65.08ms p(90)=4.05ms p(95)=5.52ms http_reqs......................: 9031 74.825497/s iteration_duration.............: avg=1s min=1s med=1s max=1.06s p(90)=1s p(95)=1s iterations.....................: 9031 74.825497/s vus............................: 3 min=3 max=100 vus_max........................: 100 min=100 max=100

"},{"location":"performance/#app-envoy-and-kyverno-envoy-plugin","title":"App, Envoy and Kyverno-envoy-plugin","text":"

In this case, performance measurements are observed with Envoy External Authorization API enabled and a sample real-world RBAC policy loaded into kyverno-envoy-plugin . For this apply this command to apply sample-application, envoy and kyverno-envoy-plugin

$ kubectl apply -f https://raw.githubusercontent.com/kyverno/kyverno-envoy-plugin/main/tests/performance-test/manifest/app-envoy-plugin.yaml\n

Results of k6 after applying sample-application, Envoy and kyverno-envoy-plugin . ```console

  /\\      |\u203e\u203e| /\u203e\u203e/   /\u203e\u203e/\n

/ / | |/ / / / / \\/ | ( / \u203e\u203e / | | | (\u203e) | / __________ || _ __/ .io

execution: local script: k6-script.js output: -

scenarios: (100.00%) 1 scenario, 100 max VUs, 2m30s max duration (incl. graceful stop): * default: Up to 100 looping VUs for 2m0s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)

\u2588 GET /book with guest token

\u2713 is status 200

checks.........................: 100.00% \u2713 8655 \u2717 0 data_received..................: 2.4 MB 20 kB/s data_sent......................: 2.4 MB 20 kB/s group_duration.................: avg=46.54ms min=4.59ms med=29.69ms max=337.79ms p(90)=109.35ms p(95)=140.51ms http_req_blocked...............: avg=11.88\u00b5s min=1.21\u00b5s med=4.15\u00b5s max=2.83ms p(90)=9.87\u00b5s p(95)=11.4\u00b5s http_req_connecting............: avg=4.98\u00b5s min=0s med=0s max=2.18ms p(90)=0s p(95)=0s http_req_duration..............: avg=46.37ms min=4.49ms med=29.49ms max=337.69ms p(90)=109.26ms p(95)=140.28ms { expected_response:true }...: avg=46.37ms min=4.49ms med=29.49ms max=337.69ms p(90)=109.26ms p(95)=140.28ms http_req_failed................: 0.00% \u2713 0 \u2717 8655 http_req_receiving.............: avg=65.19\u00b5s min=11.14\u00b5s med=56.47\u00b5s max=5.58ms p(90)=102.86\u00b5s p(95)=145.19\u00b5s http_req_sending...............: avg=30.35\u00b5s min=5.43\u00b5s med=18.48\u00b5s max=5.29ms p(90)=46.63\u00b5s p(95)=58\u00b5s http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s http_req_waiting...............: avg=46.27ms min=4.43ms med=29.42ms max=337.65ms p(90)=109.22ms p(95)=140.24ms http_reqs......................: 8655 71.999297/s iteration_duration.............: avg=1.04s min=1s med=1.03s max=1.33s p(90)=1.11s p(95)=1.14s iterations.....................: 8655 71.999297/s vus............................: 2 min=2 max=100 vus_max........................: 100 min=100 max=100

"},{"location":"performance/#measuring-performance","title":"Measuring Performance","text":"

The following metrics should be measured to evaluate the performance impact of the kyverno-envoy-plugin:

  • End-to-end latency The end-to-end latency represents the time taken for a request to complete, from the client sending the request to receiving the response. Based on the k6 results, the average end-to-end latency for the different scenarios is as follows:

  • App Only: avg=1.01ms (from group_duration or http_req_duration)

  • App and Envoy: avg=2.38ms (from http_req_duration)
  • App, Envoy, and Kyverno-envoy-plugin: avg=46.37ms (from http_req_duration)

  • Kyverno evaluation latency The Kyverno evaluation latency represents the time taken by the kyverno-envoy-plugin to evaluate the request against the configured policies. While the k6 results do not directly provide this metric, an estimate can be inferred by analyzing the differences in latency between the \"App and Envoy\" scenario and the \"App, Envoy, and Kyverno-envoy-plugin\" scenario.

The difference in average latency between these two scenarios is: 46.37ms - 2.38ms = 43.99ms

This difference can be attributed to the Kyverno evaluation latency and the gRPC server handler latency combined. Assuming the gRPC server handler latency is relatively small compared to the Kyverno evaluation latency, the estimated range for the Kyverno evaluation latency is around 40ms to 45ms.

  • Resource utilization Refers to CPU and memory usage of the Kyverno-Envoy-Plugin container , kubectl top utility can be laveraged to measure the resource utilization.

Get the resource utilization of the kyverno-envoy-plugin container using the following command:

$ kubectl top pod -n demo --containers\n

To monitor resource utilization overtime use the following command:

$ watch -n 1 \"kubectl top pod -n demo --containers\"\n

Now run the k6 script in different terminal window and observe the resource utilization of the kyverno-envoy-plugin container.

Initial resource utilization of the kyverno-envoy-plugin container:

POD                        NAME                   CPU(cores)   MEMORY(bytes)\ntestapp-5955cd6f8b-dbvgd   envoy                  4m           70Mi\ntestapp-5955cd6f8b-dbvgd   kyverno-envoy-plugin   1m           51Mi\ntestapp-5955cd6f8b-dbvgd   test-application       1m           11Mi\n

Resource utilization of the kyverno-envoy-plugin container after 100 requests:

POD                        NAME                   CPU(cores)   MEMORY(bytes)\ntestapp-5955cd6f8b-dbvgd   envoy                  110m         70Mi\ntestapp-5955cd6f8b-dbvgd   kyverno-envoy-plugin   895m         60Mi\ntestapp-5955cd6f8b-dbvgd   test-application       17m          15Mi\n

Observations:

  • The CPU utilization of the kyverno-envoy-plugin container increased significantly from 1m to 895m after receiving 100 requests during the load test.
  • The memory utilization also increased, but to a lesser extent, from 51Mi to 60Mi.

Resource utilization of the kyverno-envoy-plugin container after load completion:

POD                        NAME                   CPU(cores)   MEMORY(bytes)\ntestapp-5955cd6f8b-dbvgd   envoy                  4m           70Mi\ntestapp-5955cd6f8b-dbvgd   kyverno-envoy-plugin   1m           51Mi\ntestapp-5955cd6f8b-dbvgd   test-application       1m           11Mi\n

Observations: - After the load test completed and the request volume returned to normal levels, the CPU and memory utilization of the kyverno-envoy-plugin container returned to their initial values. This indicates that the kyverno-envoy-plugin can efficiently handle the increased load during the test and release the additional resources when the load subsides.

Correlation with k6 results: - The k6 script simulated a load test scenario with 100 virtual users, ramping up over 30 seconds, staying at 100 users for 1 minute, and then ramping down over 30 seconds. - During the load test, when the request volume was at its peak (100 virtual users), the kyverno-envoy-plugin container experienced a significant increase in CPU utilization, reaching 895m. - This CPU utilization spike aligns with the increased processing demand on the kyverno-envoy-plugin to evaluate the incoming requests against the configured Kyverno policies. - The memory utilization increase during the load test was relatively modest, suggesting that the policy evaluation did not significantly impact the memory requirements of the kyverno-envoy-plugin.

"},{"location":"quick-start/","title":"Quick Start","text":"

This section presumes testing is conducted with Envoy version 1.10.0 or newer.

"},{"location":"quick-start/#required-tools","title":"Required tools","text":"
  1. minikube
  2. kubectl
"},{"location":"quick-start/#create-a-local-cluster","title":"Create a local cluster","text":"

Start minikube cluster with the following command:

minikube start\n
"},{"location":"quick-start/#install-kyverno-envoy-sidecar-with-application","title":"Install kyverno-envoy sidecar with application","text":"

Install application with envoy and kyverno-envoy-plugin as a sidecar container.

kubectl apply -f https://raw.githubusercontent.com/kyverno/kyverno-envoy-plugin/main/quick_start.yaml \n
The applicaition.yaml manifest defines the following resource:

  • The Deployment includes an example Go application that provides information of books in the library books collection and exposes APIs to get, create and delete books collection. Check this out for more information about the Go test application .

  • The Deployment also includes a kyverno-envoy-plugin sidecar container in addition to the Envoy sidecar container. When Envoy recevies API request destined for the Go test applicaiton, it will check with kyverno-envoy-plugin to decide if the request should be allowed and the kyverno-envoy-plugin sidecar container is configured to query Kyverno-json engine for policy decisions on incoming requests.

  • A ConfigMap policy-config is used to pass the policy to kyverno-envoy-plugin sidecar in the namespace default where the application is deployed .

  • A ConfigMap envoy-config is used to pass an Envoy configuration with an External Authorization Filter to direct authorization checks to the kyverno-envoy-plugin sidecar.

  • The Deployment also includes an init container that install iptables rules to redirect all container traffic to the Envoy proxy sidecar container , more about init container can be found here

"},{"location":"quick-start/#make-test-application-accessible-in-the-cluster","title":"Make Test application accessible in the cluster .","text":"
kubectl expose deployment testapp --type=NodePort --name=testapp-service --port=8080\n
"},{"location":"quick-start/#set-the-service_url-environment-variable-to-the-services-ipport","title":"Set the SERVICE_URL environment variable to the service's IP/port.","text":"

minikube:

export SERVICE_PORT=$(kubectl get service testapp -o jsonpath='{.spec.ports[?(@.port==8080)].nodePort}')\nexport SERVICE_HOST=$(minikube ip)\nexport SERVICE_URL=$SERVICE_HOST:$SERVICE_PORT\necho $SERVICE_URL\n
"},{"location":"quick-start/#calling-the-sample-test-application-and-verify-the-authorization","title":"Calling the sample test application and verify the authorization","text":"

For convenience, we\u2019ll want to store Alice\u2019s and Bob\u2019s tokens in environment variables. Here bob is assigned the admin role and alice is assigned the guest role.

export ALICE_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk\"\nexport BOB_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6ImFkbWluIiwic3ViIjoiWVd4cFkyVT0ifQ.veMeVDYlulTdieeX-jxFZ_tCmqQ_K8rwx2OktUHv5Z0\"\n

The policy we passed to kyverno-envoy-plugin sidecar in the ConfigMap policy-config is configured to check the conditions of the incoming request and denies the request if the user is a guest and the request method is POST at the /book path.

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n    name: checkrequest\nspec:\n    rules:\n    - name: deny-guest-request-at-post\n        assert:\n        any:\n        - message: \"POST method calls at path /book are not allowed to guests users\"\n            check:\n            request:\n                http:\n                    method: POST\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book                             \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): guest\n                    path: /book               \n

Check for Alice which can get book but cannot create book.

curl -i -H \"Authorization: Bearer \"$ALICE_TOKEN\"\" http://$SERVICE_URL/book\n
curl -i -H \"Authorization: Bearer \"$ALICE_TOKEN\"\" -d '{\"bookname\":\"Harry Potter\", \"author\":\"J.K. Rowling\"}' -H \"Content-Type: application/json\" -X POST http://$SERVICE_URL/book\n
Check the Bob which can get book also create the book

curl -i -H \"Authorization: Bearer \"$BOB_TOKEN\"\" http://$SERVICE_URL/book\n
curl -i -H \"Authorization: Bearer \"$BOB_TOKEN\"\" -d '{\"bookname\":\"Harry Potter\", \"author\":\"J.K. Rowling\"}' -H \"Content-Type: application/json\" -X POST http://$SERVICE_URL/book\n

Check on logs

kubectl logs \"$(kubectl get pod -l app=testapp -o jsonpath={.items..metadata.name})\" -c kyverno-envoy-plugin -f\n
First , third and last request is passed but second request is failed.

sanskar@sanskar-HP-Laptop-15s-du1xxx:~$ kubectl logs \"$(kubectl get pod -l app=testapp -n demo -o jsonpath={.items..metadata.name})\" -n demo -c kyverno-envoy-plugin -f\nStarting HTTP server on Port 8000\nStarting GRPC server on Port 9000\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:11:42 Request passed the deny-guest-request-at-post policy rule.\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:22:11 Request violation: -> POST method calls at path /book are not allowed to guests users\n -> any[0].check.request.http.headers.authorization.(split(@, ' ')[1]).(jwt_decode(@ , 'secret').payload.role): Invalid value: \"guest\": Expected value: \"admin\"\n-> GET method call is allowed to both guest and admin users\n -> any[1].check.request.http.headers.authorization.(split(@, ' ')[1]).(jwt_decode(@ , 'secret').payload.role): Invalid value: \"guest\": Expected value: \"admin\"\n -> any[1].check.request.http.method: Invalid value: \"POST\": Expected value: \"GET\"\n-> GET method call is allowed to both guest and admin users\n -> any[2].check.request.http.method: Invalid value: \"POST\": Expected value: \"GET\"\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:23:13 Request passed the deny-guest-request-at-post policy rule.\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:23:55 Request passed the deny-guest-request-at-post policy rule.\n
"},{"location":"quick-start/#configuration","title":"Configuration","text":"

To deploy Kyverno-Envoy include the following container in your kubernetes Deployments:

- name: kyverno-envoy-plugin\n  image: sanskardevops/plugin:0.0.34\n  imagePullPolicy: IfNotPresent\n  ports:\n    - containerPort: 8181\n    - containerPort: 9000\n  volumeMounts:\n    - readOnly: true\n  args:\n    - \"serve\"\n    - \"--policy=/policies/policy.yaml\"\n    - \"--address=:9000\"\n    - \"--healthaddress=:8181\"\n  livenessProbe:\n    httpGet:\n      path: /health\n      scheme: HTTP\n      port: 8181\n    initialDelaySeconds: 5\n    periodSeconds: 5\n  readinessProbe:\n    httpGet:\n      path: /health\n      scheme: HTTP\n      port: 8181\n    initialDelaySeconds: 5\n    periodSeconds: 5  \n
"},{"location":"community/","title":"Community","text":"

The Kyverno Envoy Plugin has a growing community and we would definitely love to see you join and contribute.

Everyone is welcome to make suggestions, report bugs, open feature requests, contribute code or docs, participate in discussions, write blogs or anything that can benefit the project.

The Kyverno Envoy Plugin is built and maintained under the Kyverno umbrella but decisions are Community driven Everyone's voice matters

"},{"location":"community/#slack-channel","title":"Slack channel","text":"

Join our slack channel #kyverno to meet with users, contributors and maintainers.

"},{"location":"community/#roadmap","title":"RoadMap","text":"

For detailed information on our planned features and upcoming updates, please view our Roadmap.

"},{"location":"community/#contributing","title":"Contributing","text":"

Please read the contributing guide for details around:

  1. Code of Conduct
  2. Code Culture
  3. Details on how to contribute
"},{"location":"community/#adopters","title":"Adopters","text":"

If you are using the Kyverno Envoy Plugin and want to share it publicly we always appreciate a bit of support. Pull requests to the ADOPTERS LIST will put a smile on our faces

"},{"location":"community/contribute/","title":"Contributing","text":"

Kyverno Envoy Plugin, developed by Kyverno, is an advanced end-to-end testing tool for Kubernetes. Our community plays a crucial role in shaping the project by reporting bugs, suggesting features, and improving documentation.

We aim to make our issue tracker, discussion board, and documentation well-structured and easy to navigate. By following our guidelines, you can help us address your requests efficiently.

"},{"location":"community/contribute/#how-you-can-contribute","title":"How you can contribute","text":"

We appreciate your efforts in reporting bugs, requesting features, and engaging in discussions. Here's how you can contribute:

"},{"location":"community/contribute/#creating-an-issue","title":"Creating an issue","text":"
  • Something is not working?

    Report a bug by creating an issue with a reproduction

    Report a bug

  • Missing information in our docs?

    Report missing information or potential inconsistencies in our documentation

    Report a docs issue

  • Want to submit an idea?

    Propose a change, feature request, or suggest an improvement

    Request a change

  • Have a question or need help?

    Ask a question on our discussion board and get in touch with our community

    Ask a question

"},{"location":"community/contribute/#contributing_1","title":"Contributing","text":"
  • Want to create a pull request?

    Learn how to create a comprehensive and useful pull request (PR)

    Create a pull request

"},{"location":"community/contribute/#checklist","title":"Checklist","text":"

Before interacting within the project, please consider the following questions to ensure you're using the correct issue template and providing all necessary information.

Issues, discussions, and comments are forever

Please note that everything you write is permanent and will remain for everyone to read \u2013 forever. Therefore, please always be nice and constructive, follow our contribution guidelines, and comply with our Code of Conduct.

"},{"location":"community/contribute/#before-creating-an-issue","title":"Before creating an issue","text":"
  • Are you using the appropriate issue template, or is there another issue template that better fits the context of your request?
  • Have you checked if a similar bug report or change request has already been created, or have you stumbled upon something that might be related?
  • Did you fill out every field as requested and provide all additional information needed to comprehend your request?
"},{"location":"community/contribute/#before-asking-a-question","title":"Before asking a question","text":"
  • Is the topic a question for our discussion board, or is it a bug report or change request that should be raised on our issue tracker?
  • Is there an open discussion on the topic of your request? If the answer is yes, does your question match the direction of the discussion, or should you open a new discussion?
  • Did you provide our community with all the necessary information to understand your question and help you quickly, or can you make it easier to help you?
"},{"location":"community/contribute/#before-commenting","title":"Before commenting","text":"
  • Is your comment relevant to the topic of the current page, post, issue, or discussion, or is it better to create a new issue or discussion?
  • Does your comment add value to the conversation? Is it constructive and respectful to our community and maintainers? Could you just use a reaction instead?
"},{"location":"community/contribute/#rights-and-responsibilities","title":"Rights and responsibilities","text":"

As maintainers, we are entrusted with the responsibility to moderate communication within our community, including the authority to close, remove, reject, or edit issues, discussions, comments, commits, and to block users who do not align with our contribution guidelines and our Code of Conduct. This role requires us to be actively involved in maintaining the integrity and positive atmosphere of our community. Upholding these standards decisively ensures a respectful and inclusive environment for all members.

"},{"location":"community/contribute/#code-of-conduct","title":"Code of Conduct","text":"

Our Code of Conduct outlines the expectation for all community members to treat one another with respect, employing inclusive and welcoming language. Our commitment is to foster a positive and supportive environment, free of inappropriate, offensive, or harmful behavior.

We take any violations seriously and will take appropriate action in response to uphold these values.1

"},{"location":"community/contribute/#incomplete-issues-and-duplicates","title":"Incomplete issues and duplicates","text":"

We have invested significant time and effort in the setup of our contribution process, ensuring that we assess the essential requirements for reviewing and responding to issues effectively. Each field in our issue templates is thoughtfully designed to help us fully understand your concerns and the nature of your matter. We encourage all members to utilize the search function before submitting new issues or starting discussions to help avoid duplicates. Your cooperation is crucial in keeping our community's discussions constructive and organized.

  • Mandatory completion of issue templates: We need all of the information required in our issue templates because it ensures that every user and maintainer, regardless of their experience, can understand the content and severity of your bug report or change request.

  • Closing incomplete issues: We reserve the right to close issues lacking essential information, such as but not limited to [minimal reproductions] or those not adhering to the quality standards and requirements specified in our issue templates. Such issues can be reopened once the missing information has been provided.

  • Handling duplicates: To maintain organized and efficient communication within our issue tracker and discussion board, we reserve the right to close any duplicated issues or lock duplicated discussions. Opening multiple channels to ask the same question or report the same issue across different forums hinders our ability to manage and address community concerns effectively. This approach is vital for efficient time management, as duplicated questions can consume the time of multiple team members simultaneously. Ensuring that each issue or discussion is unique and progresses with new information helps us to maintain focus and support our community.

    We further reserve the right to immediately close discussions or issues that are reopened without providing new information or simply because users have not yet received a response to their issue/question, as the issue is marked as incomplete.

  • Limitations of automated tools: While we believe in the value and efficiency that automated tools bring to identifying potential issues (such as those identified by Lighthouse, Accessibility tools, and others), simply submitting an issue generated by these tools does not constitute a complete bug report. These tools sometimes produce verbose outputs and may include false positives, which necessitate a critical evaluation. You are of course welcome to attach generated reports to your issue. However, this does not substitute the requirement for a minimal reproduction or a thorough discussion of the findings. We reserve the right to mark these issues as incomplete and close them. This practice ensures that we are addressing genuine concerns with precision and clarity, rather than navigating through extensive automated outputs.

  1. Warning and blocking policy: Given the increasing popularity of our project and our commitment to a healthy community, we've defined clear guidelines on how we proceed with violations:

    1.1. First warning: Users displaying repeated inappropriate, offensive, or harmful behavior will receive a first warning. This warning serves as a formal notice that their behavior is not in alignment with our community standards and Code of Conduct. The first warning is permanent.

    1.2. Second warning and opportunity for resolution: If the behavior persists, a second warning will be issued. Upon receiving the second warning, the user will be given a 5-day period for reflection, during which they are encouraged to publicly explain or apologize for their actions. This period is designed to offer an opportunity for openly clearing out any misunderstanding.

    1.3. Blocking: Should there be no response or improvement in behavior following the second warning, we reserve the right to block the user from the community and repository. Blocking is considered a last resort, used only when absolutely necessary to protect the community's integrity and positive atmosphere.

    Blocking has been an exceptionally rare necessity in our overwhelmingly positive community, highlighting our preference for constructive dialogue and mutual respect. It aims to protect our community members and team.\u00a0\u21a9

"},{"location":"community/making-a-pull-request/","title":"Pull Requests","text":"

You can contribute by making a pull request that will be reviewed by maintainers and integrated into the main repository when the changes made are approved. You can contribute bug fixes, documentation changes, or new functionalities.

Considering a pull request

Before deciding to spend effort on making changes and creating a pull request, please discuss what you intend to do. If you are responding to what you think might be a bug, please issue a bug report first. If you intend to work on documentation, create a documentation issue. If you want to work on a new feature, please create a change request.

Keep in mind the guidance given and let people advise you. It might be that there are easier solutions to the problem you perceive and want to address. It might be that what you want to achieve can already be done by configuration or [customization].

"},{"location":"community/making-a-pull-request/#learning-about-pull-requests","title":"Learning about pull requests","text":"

Pull requests are a concept layered on top of Git by services that provide Git hosting. Before you consider making a pull request, you should familiarize yourself with the documentation on GitHub, the service we are using. The following articles are of particular importance:

  1. Forking a repository
  2. Creating a pull request from a fork
  3. Creating a pull request

Note that they provide tailored documentation for different operating systems and different ways of interacting with GitHub. We do our best in the documentation here to describe the process as it applies but cannot cover all possible combinations of tools and ways of doing things. It is also important that you understand the concept of a pull-request in general before continuing.

"},{"location":"community/making-a-pull-request/#pull-request-process","title":"Pull request process","text":"

In the following, we describe the general process for making pull requests. The aim here is to provide the 30k ft overview before describing details later on.

"},{"location":"community/making-a-pull-request/#preparing-changes-and-draft-pr","title":"Preparing changes and draft PR","text":"

The diagram below describes what typically happens to repositories in the process or preparing a pull request. We will be discussing the review-revise process below. It is important that you understand the overall process first before you worry about specific commands. This is why we cover this first before providing instructions below.

sequenceDiagram\n  autonumber\n\n  participant upstream\n  participant PR\n  participant fork\n  participant local\n\n  upstream ->> fork: fork on GitHub\n  fork ->> local: clone to local\n  local ->> local: branch\n  loop prepare\n    loop push\n      loop edit\n        local ->> local: commit\n      end\n      local ->> fork: push\n    end\n    upstream ->> fork: merge in any changes\n    fork ->>+ PR: create draft PR\n    PR ->> PR: review your changes\n  end
  1. Fork the Repository: Fork the upstream repository on GitHub to create your own copy.
  2. Clone to Local: Clone your fork to your local machine.
  3. Create a Branch: Create a topic branch for your changes.
  4. Set Up Development Environment: Follow the instructions to set up a development environment.
  5. Iterate and Commit: Make incremental changes and commit them with meaningful messages.
  6. Push Regularly: Push your commits to your fork regularly.
  7. Merge Changes from Upstream: Regularly merge changes from the original upstream repository to avoid conflicts.
  8. Create a Draft Pull Request: Once satisfied with your changes, create a draft pull request for early feedback.
  9. Review and Revise: Review your work critically, address feedback, and refine your changes.
"},{"location":"community/making-a-pull-request/#finalizing","title":"Finalizing","text":"

Once you are happy with your changes, you can move to the next step, finalizing your pull request and asking for a more formal and detailed review. The diagram below shows the process:

sequenceDiagram\n  autonumber\n  participant upstream\n  participant PR\n  participant fork\n  participant local\n\n  activate PR\n  PR ->> PR: finalize PR\n  loop review\n    loop discuss\n      PR ->> PR: request review\n      PR ->> PR: discussion\n      local ->> fork: push further changes\n    end\n    PR ->> upstream: merge (and squash)\n    deactivate PR\n    fork ->> fork: delete branch\n    upstream ->> fork: pull\n    local ->> local: delete branch\n    fork ->> local: pull\n  end\n
  1. Finalize PR: Signal that your changes are ready for review.
  2. Request Review: Ask the maintainer to review your changes.
  3. Discuss and Revise: Engage in discussions, make necessary revisions, and iterate.
  4. Merge and Squash: Once approved, the maintainer will merge and possibly squash your commits.
  5. Clean Up: Delete the branch used for the PR from both your fork and local clone.
"},{"location":"community/reporting-a-bug/","title":"Bug Reports","text":"

If you think you have discovered a bug, you can help us by submitting an issue in our public issue tracker, following this guide.

"},{"location":"community/reporting-a-bug/#before-creating-an-issue","title":"Before Creating an Issue","text":"

With numerous users, issues are created regularly. The maintainers of this project strive to address bugs promptly. By following this guide, you will know exactly what information we need to help you quickly.

Please do the following before creating an issue:

"},{"location":"community/reporting-a-bug/#upgrade-to-latest-version","title":"Upgrade to Latest Version","text":"

Chances are that the bug you discovered was already fixed in a subsequent version. Before reporting an issue, ensure that you're running the latest version.

Bug fixes are not backported

Please understand that only bugs that occur in the latest version will be addressed. Also, to reduce duplicate efforts, fixes cannot always be backported to earlier versions.

"},{"location":"community/reporting-a-bug/#remove-customizations","title":"Remove Customizations","text":"

If you're using customizations like additional configurations, remove them before reporting a bug. We can't offer official support for bugs that might hide in your overrides, so make sure to omit custom settings from your configuration files.

Don't be shy to ask on our discussion board for help if you run into problems.

"},{"location":"community/reporting-a-bug/#search-for-solutions","title":"Search for Solutions","text":"

At this stage, we know that the problem persists in the latest version and is not caused by any of your customizations. However, the problem might result from a small typo or a syntactical error in a configuration file.

Before creating a bug report, save time for us and yourself by doing some research:

  1. Search our documentation for relevant sections related to your problem. Ensure everything is configured correctly.
  2. [Search our issue tracker] as another user might have already reported the same problem.
  3. [Search our discussion board] to see if other users are facing similar issues and find possible solutions.

Keep track of all search terms and relevant links; you'll need them in the bug report.

If you still haven't found a solution to your problem, create an issue. It's now likely that you've encountered something new. Read the following section to learn how to create a complete and helpful bug report.

"},{"location":"community/reporting-a-bug/#issue-template","title":"Issue Template","text":"

We have created a new issue template to make the bug reporting process as simple as possible and more efficient for our community and us. It consists of the following parts:

  • Title
  • Context optional
  • Bug Description
  • Related Links
  • Reproduction
  • Steps to Reproduce
  • Browser optional
  • Checklist
"},{"location":"community/reporting-a-bug/#title","title":"Title","text":"

A good title is short and descriptive. It should be a one-sentence executive summary of the issue, so the impact and severity of the bug can be inferred from the title.

Example Clear apply command fails with specific CRD Wordy The apply command fails when used with a certain Custom Resource Definition Unclear Command does not work Useless Help"},{"location":"community/reporting-a-bug/#context","title":"Context optional","text":"

Before describing the bug, you can provide additional context to help us understand what you were trying to achieve. Explain the circumstances under which the bug happens, and what you think might be relevant. Don't describe the bug here.

"},{"location":"community/reporting-a-bug/#bug-description","title":"Bug Description","text":"

Provide a clear, focused, specific, and concise summary of the bug you encountered. Explain why you think this is a bug that should be reported, and not to one of its dependencies. Follow these principles:

  • Explain the what, not the how \u2013 don't explain how to reproduce the bug here, we're getting there. Focus on articulating the problem and its impact.
  • Keep it short and concise \u2013 if the bug can be precisely explained in one or two sentences, perfect. Don't inflate it.
  • One bug at a time \u2013 if you encounter several unrelated bugs, create separate issues for them.
"},{"location":"community/reporting-a-bug/#related-links","title":"Related Links","text":"

Share links to relevant sections of our documentation and any related issues or discussions. This helps us improve our documentation and understand the problem better.

"},{"location":"community/reporting-a-bug/#reproduction","title":"Reproduction","text":"

A minimal reproduction is essential for a well-written bug report, as it allows us to recreate the conditions necessary to inspect the bug. Follow the guide to create a reproduction:

[ Create reproduction][Create reproduction]{ .md-button .md-button--primary }

After creating the reproduction, you should have a .zip file, ideally not larger than 1 MB. Drag and drop the .zip file into the issue field, which will automatically upload it to GitHub.

Don't share links to repositories

While linking to a repository is a common practice, we currently don't support this. The reproduction, created using the built-in info plugin, contains all necessary environment information.

"},{"location":"community/reporting-a-bug/#steps-to-reproduce","title":"Steps to Reproduce","text":"

List specific steps to follow when running your reproduction to observe the bug. Keep the steps concise and ensure nothing is left out. Use simple language and focus on continuity.

"},{"location":"community/reporting-a-bug/#browser","title":"Browser optional","text":"

If the bug only occurs in specific browsers, let us know which ones are affected. This field is optional, as it is only relevant for bugs that do not involve a crash when previewing or building your site.

Incognito Mode

Verify that the bug is not caused by a browser extension by switching to incognito mode. If the bug disappears, it is likely caused by an extension.

"},{"location":"community/reporting-a-bug/#checklist","title":"Checklist","text":"

Before submitting, ensure you have:

  • Followed this guide thoroughly
  • Provided all necessary information
  • Created a minimal reproduction

Thanks for following the guide and creating a high-quality bug report. We will take it from here.

"},{"location":"community/reporting-a-docs-issue/","title":"Documentation Issues","text":"

The documentation includes extensive information on features, configurations, customizations, and more. If you have found an inconsistency or see room for improvement, please follow this guide to submit an issue on our issue tracker.

"},{"location":"community/reporting-a-docs-issue/#issue-template","title":"Issue Template","text":"

Reporting a documentation issue is usually less involved than reporting a bug, as we don't need a [reproduction]. Please thoroughly read this guide before creating a new documentation issue, and provide the following information as part of the issue:

  • Title
  • Description
  • Related Links
  • Proposed Change optional
  • Checklist
"},{"location":"community/reporting-a-docs-issue/#title","title":"Title","text":"

A good title should be a short, one-sentence description of the issue, containing all relevant information and keywords to simplify the search in our issue tracker.

Example Clear Clarify resource templating setup Unclear Missing information in the docs Useless Help"},{"location":"community/reporting-a-docs-issue/#description","title":"Description","text":"

Provide a clear and concise summary of the inconsistency or issue you encountered in the documentation or the documentation section that needs improvement. Explain why you think the documentation should be adjusted and describe the severity of the issue:

  • Keep it short and concise \u2013 if the inconsistency or issue can be precisely explained in one or two sentences, perfect. Maintainers and future users will be grateful for having to read less.
  • One issue at a time \u2013 if you encounter several unrelated inconsistencies, please create separate issues for them.

Why we need this: describing the problem clearly and concisely is a prerequisite for improving our documentation \u2013 we need to understand what's wrong so we can fix it.

"},{"location":"community/reporting-a-docs-issue/#related-links","title":"Related Links","text":"

After you describe the documentation section that needs to be adjusted, share the link to this specific documentation section and other possibly related sections. Use anchor links (permanent links) where possible, as it simplifies discovery.

Why we need this: providing the links to the documentation helps us understand which sections of our documentation need to be adjusted, extended, or overhauled.

"},{"location":"community/reporting-a-docs-issue/#proposed-change","title":"Proposed Change optional","text":"

Now that you have provided us with the description and links to the documentation sections, you can help us, maintainers, and the community by proposing an improvement. You can sketch out rough ideas or write a concrete proposal. This field is optional but very helpful.

Why we need this: an improvement proposal can be beneficial for other users who encounter the same issue, as they offer solutions before we maintainers can update the documentation.

"},{"location":"community/reporting-a-docs-issue/#checklist","title":"Checklist","text":"

Thanks for following the guide and providing valuable feedback for our documentation \u2013 you are almost done. The checklist ensures that you have read this guide and have worked to your best knowledge to provide us with every piece of information we need to improve it.

We'll take it from here.

"},{"location":"community/requesting-a-change/","title":"Change Requests","text":"

We value every idea or contribution from our community. Please follow this guide before submitting your change request in our public issue tracker. This helps us better understand the proposed change and how it will benefit our community.

"},{"location":"community/requesting-a-change/#before-creating-an-issue","title":"Before Creating an Issue","text":"

Before you invest time in submitting a change request, answer these questions to determine if your idea is a good fit and matches the project's philosophy and tone.

"},{"location":"community/requesting-a-change/#its-not-a-bug-its-a-feature","title":"It's Not a Bug, It's a Feature","text":"

Change requests suggest minor adjustments, new features, or influence the project's direction. They are not intended for reporting bugs. Refer to our bug reporting guide for that.

"},{"location":"community/requesting-a-change/#look-for-sources-of-inspiration","title":"Look for Sources of Inspiration","text":"

If your idea is implemented in another tool or framework, collect information on its implementation. This helps us evaluate its fit more quickly.

"},{"location":"community/requesting-a-change/#connect-with-our-community","title":"Connect with Our Community","text":"

Our discussion board is the best place to connect with our community. Seeking input from other users helps implement features that benefit a larger number of users.

Start a discussion

"},{"location":"community/requesting-a-change/#issue-template","title":"Issue Template","text":"

After doing the preliminary work, create a change request. Follow these steps:

  • Title
  • Context optional
  • Description
  • Related Links
  • Use Cases
  • Visuals optional
  • Checklist
"},{"location":"community/requesting-a-change/#title","title":"Title","text":"

A good title is short and descriptive, summarizing the idea so the potential impact and benefit can be inferred.

Example Clear Support for resource templating Wordy Add support for templating resources for easier testing Unclear Improve templating Useless Help"},{"location":"community/requesting-a-change/#context","title":"Context optional","text":"

Provide additional context to help us understand what you are trying to achieve. Explain the circumstances and relevant settings without describing the change request itself.

"},{"location":"community/requesting-a-change/#description","title":"Description","text":"

Provide a detailed and clear description of your idea. Explain why your idea is relevant and should be implemented here, not in one of its dependencies.

  • Explain the what, not the why \u2013 focus on describing the change request precisely.
  • Keep it short and concise \u2013 be brief and to the point.
  • One idea at a time \u2013 if you have multiple ideas, open separate change requests for each.
"},{"location":"community/requesting-a-change/#related-links","title":"Related Links","text":"

Provide any relevant links to issues, discussions, or documentation sections related to your change request. This helps us gain additional context.

"},{"location":"community/requesting-a-change/#use-cases","title":"Use Cases","text":"

Explain how your change request would work from an author's and user's perspective. What is the expected impact, and why does it benefit other users? Would it potentially break existing functionality?

"},{"location":"community/requesting-a-change/#visuals","title":"Visuals optional","text":"

If you have any visuals, such as sketches, screenshots, mockups, or external assets, present them in this section. If you have seen this change used in other tools, showcase and describe its implementation.

"},{"location":"community/requesting-a-change/#checklist","title":"Checklist","text":"

Thanks for following the guide and creating a high-quality change request. The checklist ensures that you have read this guide and provided all necessary information for us to review your idea.

We'll take it from here.

"},{"location":"community/requesting-a-change/#rejected-requests","title":"Rejected Requests","text":"

Your change request got rejected? We're sorry for that. We understand it can be frustrating, but we always need to consider the needs of our entire community. If you're unsure why your change request was rejected, please ask for clarification.

We consider the following principles when evaluating change requests:

  • Alignment with the project's vision and tone
  • Compatibility with existing features and plugins
  • Compatibility with all screen sizes and browsers
  • Effort of implementation and maintenance
  • Usefulness to the majority of users
  • Simplicity and ease of use
  • Accessibility

If your idea was rejected, you can always implement it via [customization]. If you're unsure how or want to know if someone has already done it, get in touch with our community on the discussion board.

"},{"location":"jp/functions/","title":"Functions","text":""},{"location":"jp/functions/#built-in-functions","title":"built-in functions","text":"Name Signature abs abs(number) avg avg(array[number]) ceil ceil(number) contains contains(array\\|string, any) ends_with ends_with(string, string) find_first find_first(string, string, number, number) find_last find_last(string, string, number, number) floor floor(number) from_items from_items(array[array]) group_by group_by(array, expref) items items(object) join join(string, array[string]) keys keys(object) length length(string\\|array\\|object) lower lower(string) map map(expref, array) max max(array[number]\\|array[string]) max_by max_by(array, expref) merge merge(object) min min(array[number]\\|array[string]) min_by min_by(array, expref) not_null not_null(any) pad_left pad_left(string, number, string) pad_right pad_right(string, number, string) replace replace(string, string, string, number) reverse reverse(array\\|string) sort sort(array[string]\\|array[number]) sort_by sort_by(array, expref) split split(string, string, number) starts_with starts_with(string, string) sum sum(array[number]) to_array to_array(any) to_number to_number(any) to_string to_string(any) trim trim(string, string) trim_left trim_left(string, string) trim_right trim_right(string, string) type type(any) upper upper(string) values values(object) zip zip(array, array)"},{"location":"jp/functions/#custom-functions","title":"custom functions","text":"Name Signature at at(array, any) concat concat(string, string) json_parse json_parse(string) wildcard wildcard(string, string)"},{"location":"jp/functions/#kyverno-functions","title":"kyverno functions","text":"Name Signature compare compare(string, string) equal_fold equal_fold(string, string) replace replace(string, string, string, number) replace_all replace_all(string, string, string) to_upper to_upper(string) to_lower to_lower(string) trim trim(string, string) trim_prefix trim_prefix(string, string) split split(string, string) regex_replace_all regex_replace_all(string, string\\|number, string\\|number) regex_replace_all_literal regex_replace_all_literal(string, string\\|number, string\\|number) regex_match regex_match(string, string\\|number) pattern_match pattern_match(string, string\\|number) label_match label_match(object, object) to_boolean to_boolean(string) add add(any, any) sum sum(array) subtract subtract(any, any) multiply multiply(any, any) divide divide(any, any) modulo modulo(any, any) round round(number, number) base64_decode base64_decode(string) base64_encode base64_encode(string) time_since time_since(string, string, string) time_now time_now() time_now_utc time_now_utc() path_canonicalize path_canonicalize(string) truncate truncate(string, number) semver_compare semver_compare(string, string) parse_json parse_json(string) parse_yaml parse_yaml(string) lookup lookup(object\\|array, string\\|number) items items(object\\|array, string, string) object_from_lists object_from_lists(array, array) random random(string) x509_decode x509_decode(string) time_to_cron time_to_cron(string) time_add time_add(string, string) time_parse time_parse(string, string) time_utc time_utc(string) time_diff time_diff(string, string) time_before time_before(string, string) time_after time_after(string, string) time_between time_between(string, string, string) time_truncate time_truncate(string, string)"},{"location":"policies/asserts/","title":"Assertion trees","text":"

Assertion trees can be used to apply complex and dynamic conditional checks using JMESPath expressions.

"},{"location":"policies/asserts/#assert","title":"Assert","text":"

An assert declaration contains an any or all list in which each entry contains a:

  • check: the assertion check
  • message: an optional message

A check can contain one or more JMESPath expressions. Expressions represent projections of selected data in the JSON payload and the result of this projection is passed to descendants for further analysis.

All comparisons happen in the leaves of the assertion tree.

A simple example:

This policy checks that a pod does not use the default service account:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: assert-sample\nspec:\n  rules:\n    - name: foo-bar\n      match:\n        all:\n        - apiVersion: v1\n          kind: Pod\n      assert:\n        all:\n        - message: \"serviceAccountName 'default' is not allowed\"\n          check:\n            spec:\n              (serviceAccountName == 'default'): false\n

A detailed example:

Given the input payload below:

foo:\n  baz: true\n  bar: 4\n  bat: 6\n

It is possible to write a validation rule like this:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar-4\n      validate:\n        assert:\n          all:\n          - message: \"...\"\n            check:\n              # project field `foo` onto itself, the content of `foo` becomes the current object for descendants\n              foo:\n\n                # evaluate expression `(bar > `3`)`, the boolean result becomes the current object for descendants\n                # the `true` leaf is compared with the current value `true`\n                (bar > `3`): true\n\n                # evaluate expression `(!baz)`, the boolean result becomes the current object for descendants\n                # the leaf `false` is compared with the current value `false`\n                (!baz): false\n\n                # evaluate expression `(bar + bat)`, the numeric result becomes the current object for descendants\n                # the leaf `10` is compared with the current value `10`\n                (bar + bat): 10\n
"},{"location":"policies/asserts/#iterating-with-projection-modifiers","title":"Iterating with Projection Modifiers","text":"

Assertion tree expressions support modifiers to influence the way projected values are processed.

The ~ modifier applies to arrays and maps, it mean the input array or map elements will be processed individually by descendants.

When the ~ modifier is not used, descendants receive the whole array, not each individual element.

Consider the following input document:

foo:\n  bar:\n  - 1\n  - 2\n  - 3\n

The policy below does not use the ~ modifier and foo.bar array is compared against the expected array:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar\n      validate:\n        assert:\n          all:\n          - foo:\n              # the content of the `bar` field will be compared against `[1, 2, 3]`\n              bar:\n              - 1\n              - 2\n              - 3\n

With the ~ modifier, we can apply descendant assertions to all elements in the array individually. The policy below ensures that all elements in the input array are < 5:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar\n      validate:\n        assert:\n          all:\n          - foo:\n              # with the `~` modifier all elements in the `[1, 2, 3]` array are processed individually and passed to descendants\n              ~.bar:\n                # the expression `(@ < `5`)` is evaluated for every element and the result is expected to be `true`\n                (@ < `5`): true\n

The ~ modifier supports binding the index of the element being processed to a named binding with the following syntax ~index_name.bar. When this is used, we can access the element index in descendants with $index_name.

When used with a map, the named binding receives the key of the element being processed.

"},{"location":"policies/asserts/#explicit-bindings","title":"Explicit bindings","text":"

Sometimes it can be useful to refer to a parent node in the assertion tree.

This is possible to add an explicit binding at every node in the tree by appending the ->binding_name to the key.

Given the input document:

foo:\n  bar: 4\n  bat: 6\n

The following policy will compute a sum and bind the result to the sum binding. A descendant can then use $sum and use it:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar\n      validate:\n        assert:\n          all:\n          - foo:\n              # evaluate expression `(bar + bat)` and bind it to `sum`\n              (bar + bat)->sum:\n                # get the `$sum` binding and compare it against `10`\n                ($sum): 10\n

All binding are available to descendants, if a descendant creates a binding with a name that already exists the binding will be overridden for descendants only and it doesn't affect the bindings at upper levels in the tree.

In other words, a node in the tree always sees bindings that are defined in the parents and if a name is reused, the first binding with the given name wins when winding up the tree.

As a consequence, the policy below will evaluate to true:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar\n      validate:\n        assert:\n          all:\n          - foo:\n              (bar + bat)->sum:\n                ($sum + $sum)->sum:\n                  ($sum): 20\n                ($sum): 10\n

Finally, we can always access the current payload, policy and rule being evaluated using the built-in $payload, $policy and $rule bindings. No protection is made to prevent you from overriding those bindings though.

"},{"location":"policies/asserts/#escaping-projection","title":"Escaping projection","text":"

It can be necessary to prevent a projection under certain circumstances.

Consider the following document:

foo:\n  (bar): 4\n  (baz):\n  - 1\n  - 2\n  - 3\n

Here the (bar) key conflict with the projection syntax. To workaround this situation, you can escape a projection by surrounding it with \\ characters like this:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar\n      validate:\n        assert:\n          all:\n          - foo:\n              \\(bar)\\: 10\n

In this case, the leading and trailing \\ characters will be erased and the projection won't be applied.

Note that it's still possible to use the ~ modifier or to create a named binding with and escaped projection.

Keys like this are perfectly valid:

  • ~index.\\baz\\
  • \\baz\\@foo
  • ~index.\\baz\\@foo
"},{"location":"policies/authz-policy/","title":"Policy Reference","text":"

This page provides guidance on writing policies for request content processed by the kyverno-json validating policy, utilizing Envoy\u2019s External Authorization filter.

"},{"location":"policies/authz-policy/#writing-policies","title":"Writing Policies","text":"

Let start with an example policy that restricts access to an endpoint based on user's role and permissions.

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n    name: checkrequest\nspec:\n    rules:\n    - name: deny-guest-request-at-post\n        assert:\n        any:\n        - message: \"POST method calls at path /book are not allowed to guests users\"\n            check:\n            request:\n                http:\n                    method: POST\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book                             \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): guest\n                    path: /book  \n

The above policy uses the jwt_decode builtin function to parse and verify the JWT containing information about the user making the request. it uses other builtins like split, base64_decode, campare, contains etc kyverno has many different function which can be used in policy.

Sample input recevied by kyverno-json validating policy is shown below:

{\n  \"source\": {\n    \"address\": {\n      \"socketAddress\": {\n        \"address\": \"10.244.1.10\",\n        \"portValue\": 59252\n      }\n    }\n  },\n  \"destination\": {\n    \"address\": {\n      \"socketAddress\": {\n        \"address\": \"10.244.1.4\",\n        \"portValue\": 8080\n      }\n    }\n  },\n  \"request\": {\n    \"time\": \"2024-04-09T07:42:29.634453Z\",\n    \"http\": {\n      \"id\": \"14694995155993896575\",\n      \"method\": \"GET\",\n      \"headers\": {\n        \":authority\": \"testapp.demo.svc.cluster.local:8080\",\n        \":method\": \"GET\",\n        \":path\": \"/book\",\n        \":scheme\": \"http\",\n        \"authorization\": \"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk\",\n        \"user-agent\": \"Wget\",\n        \"x-forwarded-proto\": \"http\",\n        \"x-request-id\": \"27cd2724-e0f4-4a69-a1b1-9a94edfa31bb\"\n      },\n      \"path\": \"/book\",\n      \"host\": \"echo.demo.svc.cluster.local:8080\",\n      \"scheme\": \"http\",\n      \"protocol\": \"HTTP/1.1\"\n    }\n  },\n  \"metadataContext\": {},\n  \"routeMetadataContext\": {}\n}\n

With the help of assertion tree, we can write policies that can be used to validate the request content.

An assert declaration contains an any or all list in which each entry contains a check and a message. The check contains a JMESPath expression that is evaluated against the request content. The message is a string that is returned when the check fails. A check can contain one or more JMESPath expressions. Expressions represent projections of seleted data in the JSON payload and the result of this projection is passed to descendants for futher analysis. All comparisons happen in the leaves of the assertion tree.

For more detail checkout Policy Structure and Assertion trees.

  • HTTP method request.http.method
  • Request path request.http.path
  • Authorization header request.http.headers.authorization

when we decode this above mentioned JWT token in the request payload we get payload.role guest:

{\n  \"exp\": 2241081539,\n  \"nbf\": 1514851139,\n  \"role\": \"guest\",\n  \"sub\": \"YWxpY2U=\"\n}\n
With the input value above, the answer is:
true\n

"},{"location":"policies/policies/","title":"Policy Structure","text":"

Kyverno policies are Kubernetes resources and can be easily managed via Kubernetes APIs, GitOps workflows, and other existing tools.

Policies that apply to JSON payload have a few differences from Kyverno policies that are applied to Kubernetes resources at admission controls.

"},{"location":"policies/policies/#resource-scope","title":"Resource Scope","text":"

Policies that apply to JSON payloads are always cluster-wide resources.

"},{"location":"policies/policies/#api-group-and-kind","title":"API Group and Kind","text":"

kyverno-json policies belong to the json.kyverno.io group and can only be of kind ValidatingPolicy.

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar-4\n      validate:\n        assert:\n          all:\n          - foo:\n              bar: 4\n
"},{"location":"policies/policies/#policy-rules","title":"Policy Rules","text":"

A policy can have multiple rules, and rules are processed in order. Evaluation stops at the first rule that fails.

"},{"location":"policies/policies/#match-and-exclude","title":"Match and Exclude","text":"

Policies that apply to JSON payloads use assertion trees in both the match/exclude declarations as well as the validate rule declaration.

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: required-s3-tags\nspec:\n  rules:\n    - name: require-team-tag\n      identifier: address\n      match:\n        any:\n        - type: aws_s3_bucket\n      exclude:\n        any:\n        - name: bypass-me\n      validate:\n        assert:\n          all:\n          - values:\n              tags:\n                Team: ?*\n

In the example above, every resource having type: aws_s3_bucket will match, and payloads having name: bypass-me will be excluded.

"},{"location":"policies/policies/#identifying-payload-entries","title":"Identifying Payload Entries","text":"

A policy rule can contain an optional identifier which declares the path to the payload element that uniquely identifies each entry.

"},{"location":"policies/policies/#context-entries","title":"Context Entries","text":"

A policy rule can contain optional context entries that are made available to the rule via bindings:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: required-s3-tags\nspec:\n  rules:\n    - name: require-team-tag\n      match:\n        any:\n        - type: aws_s3_bucket\n      context:\n      # creates a `expectedTeam` binding automatically\n      - name: expectedTeam\n        variable: Kyverno\n      validate:\n        message: Bucket `{{ name }}` does not have the required Team tag {{ $expectedTeam }}\n        assert:\n          all:\n          - values:\n              tags:\n                # use the `$expectedTeam` binding coming from the context\n                Team: ($expectedTeam)\n
"},{"location":"policies/policies/#no-foreach-pattern-operators-anchors-or-wildcards","title":"No forEach, pattern operators, anchors, or wildcards","text":"

The use of assertion trees addresses some features of Kyverno policies that apply to Kubernetes resources.

Specifically, forEach, pattern operators, anchors, or wildcards are not supported for policies that apply to JSON resources. Instead, assertion trees with JMESPath expressions are used to achieve the same powerful features.

"},{"location":"tutorials/istio/","title":"Istio","text":"

Istio is an open source service mesh for managing the different microservices that make up a cloud-native application. Istio provides a mechanism to use a service as an external authorizer with the AuthorizationPolicy API.

This tutorial shows how Istio\u2019s AuthorizationPolicy can be configured to delegate authorization decisions to Kyverno-envoy-plugin.

"},{"location":"tutorials/istio/#prerequisites","title":"Prerequisites","text":"

This tutorial requires Kubernetes 1.20 or later. To run the tutorial locally ensure you start a cluster with Kubernetes version 1.20+, we recommend using minikube or KIND.

The tutorial also requries istio v1.19.0 or later. To install istio, follow the instructions here or run the below script it will create a kind cluster and install istio

#!/bin/bash\n\nKIND_IMAGE=kindest/node:v1.29.2\nISTIO_REPO=https://istio-release.storage.googleapis.com/charts\nISTIO_NS=istio-system\n\n# Create Kind cluster\nkind create cluster --image $KIND_IMAGE --wait 1m --config - <<EOF\nkind: Cluster\napiVersion: kind.x-k8s.io/v1alpha4\nnodes:\n  - role: control-plane\n    kubeadmConfigPatches:\n      - |-\n        kind: InitConfiguration\n        nodeRegistration:\n          kubeletExtraArgs:\n            node-labels: \"ingress-ready=true\"\n    extraPortMappings:\n      - containerPort: 80\n        hostPort: 80\n        protocol: TCP\n      - containerPort: 443\n        hostPort: 443\n        protocol: TCP\n  - role: worker\nEOF\n\n# Install Istio components\nhelm upgrade --install istio-base       --namespace $ISTIO_NS           --create-namespace --wait --repo $ISTIO_REPO base\nhelm upgrade --install istiod           --namespace $ISTIO_NS           --create-namespace --wait --repo $ISTIO_REPO istiod\n
The tutorial requires admission controller in the kyverno-envoy-sidecar-injector namespace that automatically injects the kyverno-envoy-plugin sidecar into pods in namespaces labelled with kyverno-envoy-sidecar/injection=enabled. To install the sidecar-injector admission controller then checkout the installation guide.

"},{"location":"tutorials/istio/#creating-a-simple-authorization-policy","title":"Creating a simple authorization policy","text":"

This tutorial assumes you have some basic knowledge of validatingPolicy and assertion trees. In summary the policy below does the following:

  • Checks that the JWT token is valid
  • Checks that the action is allowed based on the token payload role and the request path
  • Guests have read-only access to the /book endpoint, admins can create users too as long as the name is not the same as the admin's name.
apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n    name: checkrequest\nspec:\n    rules:\n    - name: deny-guest-request-at-post\n        assert:\n        any:\n        - message: \"POST method calls at path /book are not allowed to guests users\"\n            check:\n            request:\n                http:\n                    method: POST\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book                             \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): guest\n                    path: /book \n
"},{"location":"tutorials/istio/#deploying-the-application","title":"Deploying the application","text":"

Create a namespace called demo and label it with istio-injection=enabled to enable sidecar injection:

$ kubectl apply -f - <<EOF\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: demo\n  labels:\n    istio-injection: enabled\nEOF\n

First we need to apply kyverno policy configmap this policy will be passed to kyverno-envoy-sidecar injector admission controller:

$ kubectl apply -f - <<EOF\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: policy-files\n  namespace: demo\ndata:\n  policy.yaml: |\n    apiVersion: json.kyverno.io/v1alpha1\n    kind: ValidatingPolicy\n    metadata:\n      name: checkrequest\n    spec:\n      rules:\n        - name: deny-guest-request-at-post\n          assert:\n            any:\n            - message: \"POST method calls at path /book are not allowed to guests users\"\n              check:\n                request:\n                    http:\n                        method: POST\n                        headers:\n                            authorization:\n                                (split(@, ' ')[1]):\n                                    (jwt_decode(@ , 'secret').payload.role): admin\n                        path: /book                             \n            - message: \"GET method call is allowed to both guest and admin users\"\n              check:\n                request:\n                    http:\n                        method: GET\n                        headers:\n                            authorization:\n                                (split(@, ' ')[1]):\n                                    (jwt_decode(@ , 'secret').payload.role): admin\n                        path: /book \n            - message: \"GET method call is allowed to both guest and admin users\"\n              check:\n                request:\n                    http:\n                        method: GET\n                        headers:\n                            authorization:\n                                (split(@, ' ')[1]):\n                                    (jwt_decode(@ , 'secret').payload.role): guest\n                        path: /book               \nEOF                   \n

Deploy the sample application which provides information about books in a collection and exposes APIs to get, create and delete Book resources at /book endpoint and make it accessible in the cluster, and enable sidecar injection of the kyverno-envoy-plugin sidecar by adding the kyverno-envoy-sidecar/injection: enabled label to the deployment:

$ kubectl apply -f - <<EOF\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: testapp\n  namespace: demo\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: testapp\n  template:\n    metadata:\n      labels:\n        kyverno-envoy-sidecar/injection: enabled\n        app: testapp\n    spec:\n      containers:\n      - name: testapp\n        image: sanskardevops/test-application:0.0.1\n        ports:\n        - containerPort: 8080\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: testapp\n  namespace: demo\nspec:\n  type: ClusterIP\n  selector:\n    app: testapp\n  ports:\n  - port: 8080\n    targetPort: 8080\nEOF\n
Check that their should be three containers should be running in the pod.

$ kubectl -n demo  get all\nNAME                        READY   STATUS    RESTARTS   AGE\npod/echo-55c77757f4-w6979   3/3     Running   0          3h59m\n\nNAME           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE\nservice/echo   ClusterIP   10.96.110.173   <none>        8080/TCP   4h5m\n\nNAME                   READY   UP-TO-DATE   AVAILABLE   AGE\ndeployment.apps/echo   1/1     1            1           3h59m\n\nNAME                              DESIRED   CURRENT   READY   AGE\nreplicaset.apps/echo-55c77757f4   1         1         1       3h59m\n
"},{"location":"tutorials/istio/#serviceentry","title":"ServiceEntry","text":"

ServiceEntry to registor the kyverno-envoy-plugin sidecar as external authorizer and ServiceEntry to allow Istio to find the Kyverno-Envoy-Plugin sidecar.

kubectl apply -f ./manifests/service-entry.yaml\n
apiVersion: networking.istio.io/v1beta1\nkind: ServiceEntry\nmetadata:\n  name: kyverno-ext-authz-grpc-local\nspec:\n  hosts:\n  - \"kyverno-ext-authz-grpc.local\"\n  # The service name to be used in the extension provider in the mesh config.\n  endpoints:\n  - address: \"127.0.0.1\"\n  ports:\n  - name: grpc\n    number: 9000\n    # The port number to be used in the extension provider in the mesh config.\n    protocol: GRPC\n  resolution: STATIC\n

"},{"location":"tutorials/istio/#register-authorization-provider","title":"Register authorization provider","text":"

Edit the mesh configmap to register authorization provider with the following command:

kubectl edit configmap istio -n istio-system \n

In the editor, add the extension provider definitions to the mesh configmap.

  data:\n    mesh: |-   \n      extensionProviders:\n      - name: \"kyverno-ext-authz-grpc\"\n        envoyExtAuthzGrpc:\n          service: \"kyverno-ext-authz-grpc.local\"\n          port: \"9000\"\n
"},{"location":"tutorials/istio/#authorization-policy","title":"Authorization policy","text":"

AuthorizationPolicy to direct authorization checks to the Kyverno-Envoy-Plugin sidecar.

$ kubectl apply -f - <<EOF\napiVersion: security.istio.io/v1\nkind: AuthorizationPolicy\nmetadata:\n  name: kyverno-ext-authz-grpc\n  namespace: demo\nspec:\n  action: CUSTOM\n  provider:\n    # The provider name must match the extension provider defined in the mesh config.\n    name: kyverno-ext-authz-grpc\n  rules:\n  # The rules specify when to trigger the external authorizer.\n  - to:\n    # Allowed all path except /healthz\n    - operation:\n        notPaths: [\"/healthz\"]\nEOF \n
This policy configures an external service for authorization. Note that the service is not specified directly in the policy but using a provider.name field.

"},{"location":"tutorials/istio/#verify-the-authorization","title":"Verify the authorization","text":"

For convenience, we\u2019ll want to store Alice\u2019s and Bob\u2019s tokens in environment variables. Here bob is assigned the admin role and alice is assigned the guest role.

export ALICE_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk\"\nexport BOB_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6ImFkbWluIiwic3ViIjoiWVd4cFkyVT0ifQ.veMeVDYlulTdieeX-jxFZ_tCmqQ_K8rwx2OktUHv5Z0\"\n

Check for Alice which can get book but cannot create book.

kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$ALICE_TOKEN\"\" --output-document - testapp.demo.svc.cluster.local:8080/book\n

kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$ALICE_TOKEN\"\" --post-data='{\"bookname\":\"Harry Potter\", \"author\":\"J.K. Rowling\"}' --output-document - testapp.demo.svc.cluster.local:8080/book\n
Check the Bob which can get book also create the book

kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$BOB_TOKEN\"\" --output-document - testapp.demo.svc.cluster.local:8080/book\n
kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$BOB_TOKEN\"\" --post-data='{\"bookname\":\"Harry Potter\", \"author\":\"J.K. Rowling\"}' --output-document - testapp.demo.svc.cluster.local:8080/book\n

Check on kyverno-envoy-plugin container logs

kubectl logs \"$(kubectl get pod -l app=testapp -n demo -o jsonpath={.items..metadata.name})\" -n demo -c ext-authz -f\n

"},{"location":"tutorials/istio/#wrap-up","title":"Wrap Up","text":"

Congratulations on completing the tutorial!

This tutorial demonstrated how to configure Istio\u2019s EnvoyFilter to utilize the kyverno-envoy-plugin as an external authorization service.

Additionally, the tutorial provided an example policy using the kyverno-envoy-plugin that returns a boolean decision to determine whether a request should be permitted.

Further details about the tutorial can be found here.

"},{"location":"tutorials/mtls-istio/","title":"Istio mTLS","text":"

Istio is an open source service mesh for managing the different microservices that make up a cloud-native application. Istio provides a mechanism to use a service as an external authorizer with the AuthorizationPolicy API.

The kyverno-envoy-plugin is a custom Envoy filter that is used to intercept the incoming request to the service and validate the request using the kyverno engine.

In this tutorial we will create a two simple microservices which are going to make external authorization to a single kyverno-envoy-plugin service as a separate pod in the mesh. With this tutorial we are going to understand how to use multiple microservices to make authorization decisions to a single ext-authz server.

To handle multiple different requests effectively, we leverage the match/exclude declarations to route the specific authz-request to the appropriate validating policy within the Kyverno engine. This approach allows us to execute the right validating policy for each request, enabling efficient and targeted request processing.

"},{"location":"tutorials/mtls-istio/#example-policy","title":"Example Policy","text":"

The following policies will be executed by the kyverno-envoy-plugin to validate incoming requests made specifically to the testapp-1 service. By leveraging the match declarations, we ensure that these policies are executed only when the incoming request is destined for the testapp-1 service. This targeted approach allows us to apply the appropriate validation rules and policies based on the specific service being accessed.

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test-policy\nspec:\n  rules:\n    - name: deny-external-calls-testapp-1\n      match:\n        any:\n        - request:\n            http:\n                host: 'testapp-1.demo.svc.cluster.local:8080'\n      assert:\n        all:\n        - message: \"The GET method is restricted to the /book path.\"\n          check:\n            request:\n                http:\n                    method: 'GET'\n                    path: '/book'\n
To execute the policy when the incoming request is made to testapp-2 service we need to use the match declarations.

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test-policy\nspec:\n  rules:\n    - name: deny-external-calls-testapp-2\n      match:\n        any:\n        - request:\n            http:\n                host: 'testapp-2.demo.svc.cluster.local:8080'\n      assert:\n        all:\n        - message: \"The GET method is restricted to the /movies path.\"\n          check:\n            request:\n                http:\n                    method: 'GET'\n                    path: '/movie'   \n
The example json request for above payload will be like below.

{\n  \"source\": {\n    \"address\": {\n      \"socketAddress\": {\n        \"address\": \"10.244.0.71\",\n        \"portValue\": 33880\n      }\n    }\n  },\n  \"destination\": {\n    \"address\": {\n      \"socketAddress\": {\n        \"address\": \"10.244.0.65\",\n        \"portValue\": 8080\n      }\n    }\n  },\n  \"request\": {\n    \"time\": \"2024-05-20T07:52:01.566887Z\",\n    \"http\": {\n      \"id\": \"5415544797791892902\",\n      \"method\": \"GET\",\n      \"headers\": {\n        \":authority\": \"testapp-2.demo.svc.cluster.local:8080\",\n        \":method\": \"GET\",\n        \":path\": \"/movie\",\n        \":scheme\": \"http\",\n        \"user-agent\": \"Wget\",\n        \"x-forwarded-proto\": \"http\",\n        \"x-request-id\": \"a3ad9f03-c9cd-4eab-97d1-83e90e0cee1b\"\n      },\n      \"path\": \"/movie\",\n      \"host\": \"testapp-2.demo.svc.cluster.local:8080\",\n      \"scheme\": \"http\",\n      \"protocol\": \"HTTP/1.1\"\n    }\n  },\n  \"metadataContext\": {},\n  \"routeMetadataContext\": {}\n}\n

To enhance security, we can implement Mutual TLS (mTLS) for peer authentication between test services and kyverno-envoy-plugin. Since we are currently using JSON request data to validate incoming requests, there is a potential risk of this data being tampered with during transit. Implementing mTLS would ensure that communication between services is encrypted and authenticated, mitigating the risk of unauthorized data modification.

apiVersion: security.istio.io/v1beta1\nkind: PeerAuthentication\nmetadata:\n  name: mtls-demo\n  namespace: demo\nspec:\n  mtls:\n    mode: STRICT\n---\napiVersion: security.istio.io/v1beta1\nkind: PeerAuthentication\nmetadata:\n  name: mtls-testapp-1\n  namespace: demo\nspec:\n  selector:\n    matchLabels:\n      app: testapp-1\n  mtls:\n    mode: STRICT\n  portLevelMtls:\n    8080:\n      mode: PERMISSIVE\n---\napiVersion: security.istio.io/v1beta1\nkind: PeerAuthentication\nmetadata:\n  name: mtls-testapp-2\n  namespace: demo\nspec:\n  selector:\n    matchLabels:\n      app: testapp-2\n  mtls:\n    mode: STRICT\n  portLevelMtls:\n    8080:\n      mode: PERMISSIVE\n
"},{"location":"tutorials/mtls-istio/#demo-instructions","title":"Demo instructions","text":""},{"location":"tutorials/mtls-istio/#required-tools","title":"Required tools","text":"
  1. kind
  2. kubectl
  3. helm
"},{"location":"tutorials/mtls-istio/#create-a-local-cluster-and-install-istio","title":"Create a local cluster and install Istio","text":"

The tutorial also requries istio v1.19.0 or later. To install istio, follow the instructions here or run the below script it will create a kind cluster and install istio

#!/bin/bash\n\nKIND_IMAGE=kindest/node:v1.29.2\nISTIO_REPO=https://istio-release.storage.googleapis.com/charts\nISTIO_NS=istio-system\n\n# Create Kind cluster\nkind create cluster --image $KIND_IMAGE --wait 1m --config - <<EOF\nkind: Cluster\napiVersion: kind.x-k8s.io/v1alpha4\nnodes:\n  - role: control-plane\n    kubeadmConfigPatches:\n      - |-\n        kind: InitConfiguration\n        nodeRegistration:\n          kubeletExtraArgs:\n            node-labels: \"ingress-ready=true\"\n    extraPortMappings:\n      - containerPort: 80\n        hostPort: 80\n        protocol: TCP\n      - containerPort: 443\n        hostPort: 443\n        protocol: TCP\n  - role: worker\nEOF\n\n# Install Istio components\nhelm upgrade --install istio-base       --namespace $ISTIO_NS           --create-namespace --wait --repo $ISTIO_REPO base\nhelm upgrade --install istiod           --namespace $ISTIO_NS           --create-namespace --wait --repo $ISTIO_REPO istiod\n
"},{"location":"tutorials/mtls-istio/#sample-applications","title":"Sample applications","text":"

Manifests for the sample applications are available in test-application-1.yaml and test-application-2.yaml. The sample app testapp-1 provides information about books in a collection and exposes APIs to get, create and delete Book resources. The sample app testapp-2 provides information about movies in a collection and exposes APIs to get, create and delete Movie resources.

$ kubectl apply -f - <<EOF\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: demo\n  labels:\n    istio-injection: enabled\nEOF\n
# test-application-1.yaml\n# Deploy sample application testapp-1 \n$ kubectl apply -f - <<EOF\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: testapp-1\n  namespace: demo\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: testapp-1\n  template:\n    metadata:\n      labels:\n        app: testapp-1\n    spec:\n      containers:\n      - name: testapp-1\n        image: sanskardevops/test-application:0.0.1\n        ports:\n        - containerPort: 8080\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: testapp-1\n  namespace: demo\nspec:\n  type: NodePort\n  selector:\n    app: testapp-1\n  ports:\n  - port: 8080\n    targetPort: 8080\nEOF\n
# test-application-2.yaml\n# Deploy sample application testapp-2\n$ kubectl apply -f - <<EOF\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: testapp-2\n  namespace: demo\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: testapp-2\n  template:\n    metadata:\n      labels:\n        app: testapp-2\n    spec:\n      containers:\n      - name: testapp-2\n        image: sanskardevops/test-application-1:0.0.3\n        ports:\n        - containerPort: 8080\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: testapp-2\n  namespace: demo\nspec:\n  type: ClusterIP\n  selector:\n    app: testapp-2\n  ports:\n  - port: 8080\n    targetPort: 8080\nEOF\n
"},{"location":"tutorials/mtls-istio/#calling-the-sample-applications","title":"Calling the sample applications","text":"

We are going to call the sample applications using a pod in the cluster.

$ kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --output-document - testapp-1.demo.svc.cluster.local:8080/book\n
[{\"id\":\"1\",\"bookname\":\"Harry Potter\",\"author\":\"J.K. Rowling\"},{\"id\":\"2\",\"bookname\":\"Animal Farm\",\"author\":\"George Orwell\"}]\npod \"test\" deleted\n
$ kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --output-document - testapp-2.demo.svc.cluster.local:8080/movie\n
[{\"id\":\"1\",\"Moviename\":\"Inception\",\"Actor\":\"Leonardo DiCaprio\"},{\"id\":\"2\",\"Moviename\":\"Batman\",\"Actor\":\"Jack Nicholson\"}]\npod \"test\" deleted\n
"},{"location":"tutorials/mtls-istio/#register-authorization-provider","title":"Register authorization provider","text":"

Edit the mesh configmap to register authorization provider with the following command:

$ kubectl edit configmap istio -n istio-system\n
In the editor, add the extension provider definitions to the mesh configmap.

  data:\n    mesh: |-   \n      extensionProviders:\n      - name: \"kyverno-ext-authz-grpc\"\n        envoyExtAuthzGrpc:\n          service: \"ext-authz.demo.svc.cluster.local\"\n          port: \"9000\"\n
"},{"location":"tutorials/mtls-istio/#authorization-policy","title":"Authorization policy","text":"

Now we can deploy an istio AuthorizationPolicy: AuthorizationPolicy to tell Istio to use kyverno-envoy-plugin as the Authz Server

$ kubectl apply -f - <<EOF\napiVersion: security.istio.io/v1\nkind: AuthorizationPolicy\nmetadata:\n  name: kyverno-ext-authz-grpc\n  namespace: demo\nspec:\n  action: CUSTOM\n  provider:\n    # The provider name must match the extension provider defined in the mesh config.\n    name: kyverno-ext-authz-grpc\n  rules:\n  # The rules specify when to trigger the external authorizer.\n  - to:\n    - operation:\n        paths: [\"/book\",\"/movie\"]\nEOF        \n

This policy configures an external service for authorization. Note that the service is not specified directly in the policy but using a provider.name field. The rules specify that requests to paths /book and /movies.

"},{"location":"tutorials/mtls-istio/#authorization-service-deployment","title":"Authorization service deployment","text":"

The deployment manifest of the authorization service is available in ext-auth-server.yaml. This deployment require policy through configmap .

Apply the policy configmap with the following command.

$ kubectl apply -f - <<EOF\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: policy-files\n  namespace: demo\ndata:\n  policy1.yaml: |\n    apiVersion: json.kyverno.io/v1alpha1\n    kind: ValidatingPolicy\n    metadata:\n      name: test-policy\n    spec:\n      rules:\n        - name: deny-external-calls-testapp-1\n          match:\n            any:\n            - request:\n                http:\n                    host: 'testapp-1.demo.svc.cluster.local:8080'\n          assert:\n            all:\n            - message: \"The GET method is restricted to the /book path.\"\n              check:\n                request:\n                    http:\n                        method: 'GET'\n                        path: '/book'\n  policy2.yaml: |\n    apiVersion: json.kyverno.io/v1alpha1\n    kind: ValidatingPolicy\n    metadata:\n      name: test-policy\n    spec:\n      rules:\n        - name: deny-external-calls-testapp-2\n          match:\n            any:\n            - request:\n                http:\n                    host: 'testapp-2.demo.svc.cluster.local:8080'\n          assert:\n            all:\n            - message: \"The GET method is restricted to the /movies path.\"\n              check:\n                request:\n                    http:\n                        method: 'GET'\n                        path: '/movie'                \nEOF                        \n
# ext-auth-server.yaml\n# Deploy the kyverno external authorizer server\n$ kubectl apply -f - <<EOF\napiVersion: v1\nkind: Service\nmetadata:\n  name: ext-authz\n  labels:\n    app: ext-authz\n  namespace: demo  \nspec:\n  ports:\n  - name: http\n    port: 8000\n    targetPort: 8000\n  - name: grpc\n    port: 9000\n    targetPort: 9000\n  selector:\n    app: ext-authz\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: ext-authz\n  namespace: demo \nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: ext-authz \n  template:\n    metadata:\n      labels:\n        app: ext-authz\n    spec:\n      containers:\n      - image: sanskardevops/plugin:0.0.29\n        imagePullPolicy: IfNotPresent\n        name: ext-authz\n        ports:\n        - containerPort: 8000\n        - containerPort: 9000\n        args:\n        - \"serve\"\n        - \"--policy=/policies/policy1.yaml\"\n        - \"--policy=/policies/policy2.yaml\"\n        volumeMounts:\n        - name: policy-files\n          mountPath: /policies\n      volumes:\n      - name: policy-files\n        configMap:\n          name: policy-files\nEOF\n

Verify the sample external authorizer is up and running:

$ kubectl logs \"$(kubectl get pod -l app=ext-authz -n demo -o jsonpath={.items..metadata.name})\" -n demo -c ext-authz -f\n

Starting GRPC server on Port 9000\nStarting HTTP server on Port 8000\n
"},{"location":"tutorials/mtls-istio/#apply-peerauthentication-policy","title":"Apply PeerAuthentication Policy","text":"

Apply the PeerAuthentication policy to enable mTLS for the sample applications and external authorizer.

$ kubectl apply -f - <<EOF\napiVersion: security.istio.io/v1beta1\nkind: PeerAuthentication\nmetadata:\n  name: mtls-demo\n  namespace: demo\nspec:\n  mtls:\n    mode: STRICT\n---\napiVersion: security.istio.io/v1beta1\nkind: PeerAuthentication\nmetadata:\n  name: mtls-testapp-1\n  namespace: demo\nspec:\n  selector:\n    matchLabels:\n      app: testapp-1\n  mtls:\n    mode: STRICT\n  portLevelMtls:\n    8080:\n      mode: PERMISSIVE\n---\napiVersion: security.istio.io/v1beta1\nkind: PeerAuthentication\nmetadata:\n  name: mtls-testapp-2\n  namespace: demo\nspec:\n  selector:\n    matchLabels:\n      app: testapp-2\n  mtls:\n    mode: STRICT\n  portLevelMtls:\n    8080:\n      mode: PERMISSIVE\nEOF\n
"},{"location":"tutorials/mtls-istio/#test-the-sample-applications","title":"Test the sample applications","text":"

Check on the logs of the sample applications to see that the requests are accepted and rejected

Check on GET request on testapp-1 which is allowed according to policy deny-external-calls-testapp-1

$ kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --output-document - testapp-1.demo.svc.cluster.local:8080/book\n
[{\"id\":\"1\",\"bookname\":\"Harry Potter\",\"author\":\"J.K. Rowling\"},{\"id\":\"2\",\"bookname\":\"Animal Farm\",\"author\":\"George Orwell\"}]\npod \"test\" deleted\n

Check on GET request on testapp-2 which is allowed according to policy deny-external-calls-testapp-2

$ kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --output-document - testapp-2.demo.svc.cluster.local:8080/movie\n
[{\"id\":\"1\",\"Moviename\":\"Inception\",\"Actor\":\"Leonardo DiCaprio\"},{\"id\":\"2\",\"Moviename\":\"Batman\",\"Actor\":\"Jack Nicholson\"}]\npod \"test\" deleted\n

Check logs of external authorizer to see that the requests are which policy was executed for a perticular request .

$ kubectl logs \"$(kubectl get pod -l app=ext-authz -n demo -o jsonpath={.items..metadata.name})\" -n demo -c ext-authz -f\n

Starting GRPC server on Port 9000\nStarting HTTP server on Port 8000\n2024/05/21 07:41:33 Request is initialized in kyvernojson engine .\n2024/05/21 07:41:33 Request passed the deny-external-calls-testapp-1 policy rule.\n2024/05/21 07:42:22 Request is initialized in kyvernojson engine .\n2024/05/21 07:42:22 Request passed the deny-external-calls-testapp-2 policy rule.\n
First request was directed to testapp-1 which was allowed by the policy deny-external-calls-testapp-1 and the second request was directed to testapp-2 which was allowed by the policy deny-external-calls-testapp-2.

"},{"location":"tutorials/mtls-istio/#wrap-up","title":"Wrap Up","text":"

Congratulations on completing the tutorial!

This tutorial demonstrated how to configure Istio's AuthorizationPolicy to utilize the kyverno-envoy-plugin as an separate pod external authorization service. By leveraging the power of Kyverno's policy engine, you can enforce fine-grained authorization rules across your microservices within the Istio service mesh.

Additionally, the tutorial showcased the use of mTLS (Mutual TLS) to secure communication between services and the kyverno-envoy-plugin, ensuring end-to-end encryption and authentication.

The combination of Istio's AuthorizationPolicy and the kyverno-envoy-plugin provides a flexible and powerful solution for implementing custom authorization logic in your cloud-native applications. By following this tutorial, you've gained hands-on experience in configuring and deploying this solution, setting the stage for further exploration and customization to meet your specific requirements.

We hope this tutorial has been informative and has provided you with a solid foundation for integrating the kyverno-envoy-plugin into your Istio service mesh environment. Feel free to explore the project's documentation and community resources for further assistance and to stay updated with the latest developments.

"},{"location":"tutorials/standalone-envoy/","title":"Standalone Envoy","text":"

The tutorial shows how Envoy's External Authorization filter can be used with Kyverno as an authorization service to enforce security policies over API requests received by Envoy.

"},{"location":"tutorials/standalone-envoy/#overview","title":"Overview","text":"

In this tutorial we'll see how to use Kyverno-envoy-plugin as an External Authorization service for the Envoy proxy. The goal of the demo to show user how kyverno-envoy-plugin will work with standalone envoy and how it can be used to enforce policies to the traffic between services. The Kyverno-envoy-plugin allows configuring these Envoy proxies to query Kyverno-json for policy decisions on incoming requests. The kyverno-envoy-plugin is cofigured as a static binary and can be run as a sidecar container in the same pod as the application.

We'll do this by:

  • Running a local Kubernetes cluster
  • Creating a simple authorization policy in ValidatingPolicy
  • Deploying a sample application with Envoy and kyverno-envoy-plugin sidecars
  • Run some sample requests to see the policy in action

Note that other than the HTTP client and bundle server, all components are co-located in the same pod.

"},{"location":"tutorials/standalone-envoy/#demo-instructions","title":"Demo instructions","text":""},{"location":"tutorials/standalone-envoy/#required-tools","title":"Required tools","text":"
  1. kind
  2. kubectl

{{< info >}} If you haven't used kind before, you can find installation instructions in the project documentation. {{</ info >}}

"},{"location":"tutorials/standalone-envoy/#running-a-local-kubernetes-cluster","title":"Running a local Kubernetes cluster","text":"

To start a local kubernetes cluster to run our demo, we'll be using kind. In order to use the kind command, you\u2019ll need to have Docker installed on your machine.

Create a cluster with the following command:

$ kind create cluster --name kyverno-tutorial --image kindest/node:v1.29.2\nCreating cluster \"kyverno-tutorial\" ...\n \u2713 Ensuring node image (kindest/node:v1.29.2) \ud83d\uddbc\n \u2713 Preparing nodes \ud83d\udce6  \n \u2713 Writing configuration \ud83d\udcdc \n \u2713 Starting control-plane \ud83d\udd79\ufe0f \n \u2713 Installing CNI \ud83d\udd0c \n \u2713 Installing StorageClass \ud83d\udcbe \nSet kubectl context to \"kind-kyverno-tutorial\"\nYou can now use your cluster with:\n\nkubectl cluster-info --context kind-kyverno-tutorial\n\nThanks for using kind! \ud83d\ude0a\n

Listing the cluster nodes, should show something like this:

$ kubectl get nodes\nNAME                             STATUS   ROLES           AGE   VERSION\nkyverno-tutorial-control-plane   Ready    control-plane   79s   v1.29.2\n
"},{"location":"tutorials/standalone-envoy/#creating-a-simple-authorization-policy","title":"Creating a simple authorization policy","text":"

This tutorial assumes you have some basic knowledge of validatingPolicy and assertion trees. In summary the policy below does the following:

  • Checks that the JWT token is valid
  • Checks that the action is allowed based on the token payload role and the request path
  • Guests have read-only access to the /book endpoint, admins can create users too as long as the name is not the same as the admin's name.
apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n    name: checkrequest\nspec:\n    rules:\n    - name: deny-guest-request-at-post\n        assert:\n        any:\n        - message: \"POST method calls at path /book are not allowed to guests users\"\n            check:\n            request:\n                http:\n                    method: POST\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book                             \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): guest\n                    path: /book \n

Create a file called policy.yaml with the above content and store it in a configMap:

$ kubectl create configmap policy --from-file=policy.yaml\n
"},{"location":"tutorials/standalone-envoy/#deploying-an-application-with-envoy-and-kyverno-envoy-plugin-sidecars","title":"Deploying an application with Envoy and Kyverno-Envoy-Plugin sidecars","text":"

In this tutorial, we are manually configuring the Envoy proxy sidecar to intermediate HTTP traffic from clients and our application. Envoy will consult Kyverno-Envoy-Plugin to make authorization decisions for each request by sending CheckRequest gRPC messages over a gRPC connection.

We will use the following Envoy configuration to achieve this. In summary, this configures Envoy to:

  • Listen on Port 7000 for HTTP traffic
  • Consult Kyverno-Envoy-Plugin at 127.0.0.1:9000 for authorization decisions and deny failing requests
  • Forward request to the application at 127.0.0.1:8080 if ok.
    static_resources:\n      listeners:\n      - address:\n          socket_address:\n            address: 0.0.0.0\n            port_value: 7000\n        filter_chains:\n        - filters:\n          - name: envoy.filters.network.http_connection_manager\n            typed_config:\n              \"@type\": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n              codec_type: auto\n              stat_prefix: ingress_http\n              route_config:\n                name: local_route\n                virtual_hosts:\n                - name: backend\n                  domains:\n                  - \"*\"\n                  routes:\n                  - match:\n                      prefix: \"/\"\n                    route:\n                      cluster: service\n              http_filters:\n              - name: envoy.ext_authz\n                typed_config:\n                  \"@type\": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz\n                  transport_api_version: V3\n                  with_request_body:\n                    max_request_bytes: 8192\n                    allow_partial_message: true\n                  failure_mode_allow: false\n                  grpc_service:\n                    google_grpc:\n                      target_uri: 127.0.0.1:9000\n                      stat_prefix: ext_authz\n                    timeout: 0.5s\n              - name: envoy.filters.http.router\n                typed_config:\n                  \"@type\": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n      clusters:\n      - name: service\n        connect_timeout: 0.25s\n        type: strict_dns\n        lb_policy: round_robin\n        load_assignment:\n          cluster_name: service\n          endpoints:\n          - lb_endpoints:\n            - endpoint:\n                address:\n                  socket_address:\n                    address: 127.0.0.1\n                    port_value: 8080\n    admin:\n      access_log_path: \"/dev/null\"\n      address:\n        socket_address:\n          address: 0.0.0.0\n          port_value: 8001\n    layered_runtime:\n      layers:\n        - name: static_layer_0\n          static_layer:\n            envoy:\n              resource_limits:\n                listener:\n                  example_listener_name:\n                    connection_limit: 10000\n            overload:\n              global_downstream_max_connections: 50000\n

Create a ConfigMap containing the above configuration by running:

$ kubectl create configmap proxy-config --from-file envoy.yaml \n
Our application will be configured using a Deployment and Service. There are few things to note:

  • The pods have an initContainer that configures the iptables rules to redirect traffic to the Envoy Proxy sidecar.
  • The test-application container is simple go application stores book information in-memory state.
  • The envoy container is configured to use proxy-config ConfigMap as the Envoy configuration we created earlier
  • The kyverno-envoy-plugin container is configured to use policy ConfigMap as the Kyverno policy we created earlier
# test-application.yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: testapp\n  namespace: demo\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: testapp\n  template:\n    metadata:\n      labels:\n        app: testapp\n    spec:\n      initContainers:\n        - name: proxy-init\n          image: sanskardevops/proxyinit:latest\n          # Configure the iptables bootstrap script to redirect traffic to the\n          # Envoy proxy on port 8000, specify that Envoy will be running as user\n          # 1111, and that we want to exclude port 8181 from the proxy for the Kyverno health checks.\n          # These values must match up with the configuration\n          # defined below for the \"envoy\" and \"kyverno-envoy-plugin\" containers.\n          args: [\"-p\", \"7000\", \"-u\", \"1111\", -w, \"8181\"]\n          securityContext:\n            capabilities:\n              add:\n                - NET_ADMIN\n            runAsNonRoot: false\n            runAsUser: 0\n      containers:\n        - name: test-application\n          image: sanskardevops/test-application:0.0.1\n          ports:\n            - containerPort: 8080\n        - name: envoy\n          image: envoyproxy/envoy:v1.30-latest\n          securityContext:\n            runAsUser: 1111\n          imagePullPolicy: IfNotPresent\n          volumeMounts:\n            - readOnly: true\n              mountPath: /config\n              name: proxy-config\n          args:\n            - \"envoy\"\n            - \"--config-path\"\n            - \"/config/envoy.yaml\"\n        - name: kyverno-envoy-plugin\n          image: sanskardevops/plugin:0.0.34\n          imagePullPolicy: IfNotPresent\n          ports:\n            - containerPort: 8181\n            - containerPort: 9000\n          volumeMounts:\n            - readOnly: true\n              mountPath: /policies\n              name: policy-files\n          args:\n            - \"serve\"\n            - \"--policy=/policies/policy.yaml\"\n            - \"--address=:9000\"\n            - \"--healthaddress=:8181\"\n          livenessProbe:\n            httpGet:\n              path: /health\n              scheme: HTTP\n              port: 8181\n            initialDelaySeconds: 5\n            periodSeconds: 5\n          readinessProbe:\n            httpGet:\n              path: /health\n              scheme: HTTP\n              port: 8181\n            initialDelaySeconds: 5\n            periodSeconds: 5  \n      volumes:\n        - name: proxy-config\n          configMap:\n            name: proxy-config\n        - name: policy-files\n          configMap:\n            name: policy-files\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: testapp\n  namespace: demo\nspec:\n  type: ClusterIP\n  selector:\n    app: testapp\n  ports:\n  - port: 8080\n    targetPort: 8080      \n

Deploy the application and Kubernetes Service to the cluster with:

$ kubectl apply -f test-application.yaml\n
Check that everything is working by listing the pod and make sure all three pods are running ok.

$ kubectl get pods\nNAME                         READY   STATUS    RESTARTS   AGE\ntestapp-74b4bc88-5d4wh       3/3     Running   0          1m\n
"},{"location":"tutorials/standalone-envoy/#policy-in-action","title":"Policy in action","text":"

For convenience, we\u2019ll want to store Alice\u2019s and Bob\u2019s tokens in environment variables. Here bob is assigned the admin role and alice is assigned the guest role.

export ALICE_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk\"\nexport BOB_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6ImFkbWluIiwic3ViIjoiWVd4cFkyVT0ifQ.veMeVDYlulTdieeX-jxFZ_tCmqQ_K8rwx2OktUHv5Z0\"\n

Check for Alice which can get book but cannot create book.

kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$ALICE_TOKEN\"\" --output-document - testapp.demo.svc.cluster.local:8080/book\n
kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$ALICE_TOKEN\"\" --post-data='{\"bookname\":\"Harry Potter\", \"author\":\"J.K. Rowling\"}' --output-document - testapp.demo.svc.cluster.local:8080/book\n
Check the Bob which can get book also create the book

kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$BOB_TOKEN\"\" --output-document - testapp.demo.svc.cluster.local:8080/book\n
kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$BOB_TOKEN\"\" --post-data='{\"bookname\":\"Harry Potter\", \"author\":\"J.K. Rowling\"}' --output-document - testapp.demo.svc.cluster.local:8080/book\n

Check on logs

kubectl logs \"$(kubectl get pod -l app=testapp -n demo -o jsonpath={.items..metadata.name})\" -n demo -c kyverno-envoy-plugin -f\n
First , third and last request is passed but second request is failed.

sanskar@sanskar-HP-Laptop-15s-du1xxx:~$ kubectl logs \"$(kubectl get pod -l app=testapp -n demo -o jsonpath={.items..metadata.name})\" -n demo -c kyverno-envoy-plugin -f\nStarting HTTP server on Port 8000\nStarting GRPC server on Port 9000\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:11:42 Request passed the deny-guest-request-at-post policy rule.\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:22:11 Request violation: -> POST method calls at path /book are not allowed to guests users\n -> any[0].check.request.http.headers.authorization.(split(@, ' ')[1]).(jwt_decode(@ , 'secret').payload.role): Invalid value: \"guest\": Expected value: \"admin\"\n-> GET method call is allowed to both guest and admin users\n -> any[1].check.request.http.headers.authorization.(split(@, ' ')[1]).(jwt_decode(@ , 'secret').payload.role): Invalid value: \"guest\": Expected value: \"admin\"\n -> any[1].check.request.http.method: Invalid value: \"POST\": Expected value: \"GET\"\n-> GET method call is allowed to both guest and admin users\n -> any[2].check.request.http.method: Invalid value: \"POST\": Expected value: \"GET\"\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:23:13 Request passed the deny-guest-request-at-post policy rule.\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:23:55 Request passed the deny-guest-request-at-post policy rule.\n
"},{"location":"tutorials/standalone-envoy/#cleanup","title":"Cleanup","text":"

Delete the cluster by running:

$ kind delete cluster --name kyverno-tutorial\n

"},{"location":"tutorials/standalone-envoy/#wrap-up","title":"Wrap Up","text":"

Congratulations on completing the tutorial!

In this tutorial, you learned how to utilize the kyverno-envoy-plugin as an external authorization service to enforce custom policies through Envoy\u2019s external authorization filter.

The tutorial also included an example policy using kyverno-envoy-plugin that returns a boolean decision indicating whether a request should be permitted.

Moreover, Envoy\u2019s external authorization filter supports the inclusion of optional response headers and body content that can be sent to either the downstream client or upstream server. An example of a rule that not only determines request authorization but also provides optional response headers, body content, and HTTP status is available here.

"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"intro/","title":"Introduction","text":"

The Kyverno Envoy Plugin is a powerful tool that integrates the Kyverno-json policy engine with the Envoy proxy. It allows you to enforce Kyverno policies on incoming and outgoing traffic in a service mesh environment, providing an additional layer of security and control over your applications.

"},{"location":"intro/#overview","title":"Overview","text":"

Envoy is a Layer 7 proxy and communication bus tailored for large-scale, modern service-oriented architectures. Starting from version 1.7.0, Envoy includes an External Authorization filter that interfaces with an authorization service to determine the legitimacy of incoming requests.

This functionality allows authorization decisions to be offloaded to an external service, which can access the request context. The request context includes details such as the origin and destination of the network activity, as well as specifics of the network request (e.g., HTTP request). This information enables the external service to make a well-informed decision regarding the authorization of the incoming request processed by Envoy.

"},{"location":"intro/#what-is-kyverno-envoy-plugin","title":"What is Kyverno-Envoy-Plugin?","text":"

Kyverno-envoy plugin extends Kyverno-json with a gRPC server that implements Envoy External Authorization API. This allows you to enforce Kyverno policies on incoming and outgoing traffic in a service mesh environment, providing an additional layer of security and control over your applications. You can use this version of Kyverno to enforce fine-grained, context-aware access control policies with Envoy without modifying your microservice.

"},{"location":"intro/#how-does-this-work","title":"How does this work?","text":"

In addition to the Envoy sidecar, your application pods will include a kyverno-envoy component, either as a sidecar or as a separate pod. This kyverno-envoy will be configured to communicate with the Kyverno-envoy-plugin gRPC server. When Envoy receives an API request intended for your microservice, it consults the Kyverno-envoy-plugin server to determine whether the request should be permitted.

Performing policy evaluations locally with Envoy is advantageous, as it eliminates the need for an additional network hop for authorization checks, thus enhancing both performance and availability.

Info

The Kyverno-Envoy-Plugin is frequently deployed in Kubernetes environments as a sidecar container or as a separate pod. Additionally, it can be used in other environments as a standalone process running alongside Envoy.

"},{"location":"intro/#additional-resources","title":"Additional Resources","text":"

See the following pages on envoyproxy.io for more information on external authorization:

  • External Authorization to learn about the External Authorization filter.
  • Network and HTTP for details on configuring the External Authorization filter.
"},{"location":"jp/","title":"Overview","text":"

kyverno-json uses JMESPath community edition, a modern JMESPath implementation with lexical scopes support.

The current payload, policy and rule are always available using the following builtin bindings:

Binding Usage $payload Current payload being analysed $policy Current policy being executed $rule Current rule being evaluated

Warning

No protection is made to prevent you from overriding those bindings.

"},{"location":"performance/","title":"Performance","text":"

This page offers guidance and best practices for benchmarking the performance of the kyverno-envoy-plugin, helping users understand the associated overhead. It outlines an example setup for conducting benchmarks, various benchmarking scenarios, and key metrics to capture for assessing the impact of the kyverno-envoy-plugin.

"},{"location":"performance/#benchmark-setup","title":"Benchmark Setup","text":"

The benchmark setup consists of the following components:

"},{"location":"performance/#sample-application","title":"Sample Application","text":"

The first component is a simple Go application that provides information of books in the library books collection and exposes APIs to get, create and delete books collection. Check this out for more information about the Go test application .

"},{"location":"performance/#envoy","title":"Envoy","text":"

The second component is the Envoy proxy, which runs alongside the example application. The Envoy configuration defines an external authorization filter envoy.ext_authz for a gRPC authorization server. The config uses Envoy's in-built gRPC client to make external gRPC calls.

static_resources:\n  listeners:\n  - address:\n      socket_address:\n        address: 0.0.0.0\n        port_value: 8000\n    filter_chains:\n    - filters:\n      - name: envoy.filters.network.http_connection_manager\n        typed_config:\n          \"@type\": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n          codec_type: auto\n          stat_prefix: ingress_http\n          route_config:\n            name: local_route\n            virtual_hosts:\n            - name: backend\n              domains:\n              - \"*\"\n              routes:\n              - match:\n                  prefix: \"/\"\n                route:\n                  cluster: service\n          http_filters:\n          - name: envoy.ext_authz\n            typed_config:\n              \"@type\": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz\n              transport_api_version: V3\n              with_request_body:\n                max_request_bytes: 8192\n                allow_partial_message: true\n              failure_mode_allow: false\n              grpc_service:\n                google_grpc:\n                  target_uri: 127.0.0.1:9191\n                  stat_prefix: ext_authz\n                timeout: 0.5s\n          - name: envoy.filters.http.router\n            typed_config:\n              \"@type\": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n  clusters:\n  - name: service\n    connect_timeout: 0.25s\n    type: strict_dns\n    lb_policy: round_robin\n    load_assignment:\n      cluster_name: service\n      endpoints:\n      - lb_endpoints:\n        - endpoint:\n            address:\n              socket_address:\n                address: 127.0.0.1\n                port_value: 8080\nadmin:\n  access_log_path: \"/dev/null\"\n  address:\n    socket_address:\n      address: 0.0.0.0\n      port_value: 8001\nlayered_runtime:\n  layers:\n    - name: static_layer_0\n      static_layer:\n        envoy:\n          resource_limits:\n            listener:\n              example_listener_name:\n                connection_limit: 10000\n        overload:\n          global_downstream_max_connections: 50000\n
"},{"location":"performance/#kyverno-envoy-plugin","title":"Kyverno-envoy-plugin","text":"

The third component is the kyverno-envoy-plugin itself, which is configured to load and enforce Kyverno policies on incoming requests.

containers:\n- name: kyverno-envoy-plugin\n  image: sanskardevops/plugin:0.0.34\n  imagePullPolicy: IfNotPresent\n  ports:\n    - containerPort: 8181\n    - containerPort: 9000\n  volumeMounts:\n    - readOnly: true\n      mountPath: /policies\n      name: policy-files\n  args:\n    - \"serve\"\n    - \"--policy=/policies/policy.yaml\"\n    - \"--address=:9000\"\n    - \"--healthaddress=:8181\"\n  livenessProbe:\n    httpGet:\n      path: /health\n      scheme: HTTP\n      port: 8181\n    initialDelaySeconds: 5\n    periodSeconds: 5\n  readinessProbe:\n    httpGet:\n      path: /health\n      scheme: HTTP\n      port: 8181\n    initialDelaySeconds: 5\n    periodSeconds: 5  \n
"},{"location":"performance/#benchmark-scenarios","title":"Benchmark Scenarios","text":"

The following scenarios should be tested to compare the performance of the kyverno-envoy-plugin under different conditions:

  1. App Only: Requests are sent directly to the application, without Envoy or the kyverno-envoy-plugin.
  2. App and Envoy: Envoy is included in the request path, but the kyverno-envoy-plugin is not (i.e., Envoy External Authorization API is disabled).
  3. App, Envoy, and Kyverno (RBAC policy): Envoy External Authorization API is enabled, and a sample real-world RBAC policy is loaded into the kyverno-envoy-plugin.
"},{"location":"performance/#load-testing-with-k6","title":"Load Testing with k6","text":"

To perform load testing, we'll use the k6 tool. Follow these steps:

  1. Install k6: Install k6 on your machine by following the instructions on the official website: https://k6.io/docs/getting-started/installation/

  2. Write the k6 script: Below is the example k6 script.

import http from 'k6/http';\nimport { check, group, sleep } from 'k6';\n\nexport const options = {\n  stages: [\n    { duration: '30s', target: 100 }, // Ramp-up to 100 virtual users over 30 seconds\n    { duration: '1m', target: 100 }, // Stay at 100 virtual users for 1 minute\n    { duration: '30s', target: 0 }, // Ramp-down to 0 virtual users over 30 seconds\n  ],\n};\n\n/*\nReplace ip for every scenerio\nexport SERVICE_PORT=$(kubectl -n demo get service testapp -o jsonpath='{.spec.ports[?(@.port==8080)].nodePort}')\nexport SERVICE_HOST=$(minikube ip)\nexport SERVICE_URL=$SERVICE_HOST:$SERVICE_PORT\necho $SERVICE_URL\n\nhttp://192.168.49.2:31541\n\n*/\nconst BASE_URL = 'http://192.168.49.2:31541'; \n\nexport default function () {\n  group('GET /book with guest token', () => {\n    const res = http.get(`${BASE_URL}/book`, {\n      headers: { 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk' },\n    });\n    check(res, {\n      'is status 200': (r) => r.status === 200,\n    });\n  });\n\n  sleep(1); // Sleep for 1 second between iterations\n}\n
  1. Run the k6 test: Run the load test with the following command:

$ k6 run -f - <<EOF\nimport http from 'k6/http';\nimport { check, group, sleep } from 'k6';\n\nexport const options = {\n  stages: [\n    { duration: '30s', target: 100 }, // Ramp-up to 100 virtual users over 30 seconds\n    { duration: '1m', target: 100 }, // Stay at 100 virtual users for 1 minute\n    { duration: '30s', target: 0 }, // Ramp-down to 0 virtual users over 30 seconds\n  ],\n};\n\n\nconst BASE_URL = 'http://192.168.49.2:31700'; // Replace with your application URL \n\nexport default function () {\n  group('GET /book with guest token', () => {\n    const res = http.get(`${BASE_URL}/book`, {\n      headers: { 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk' },\n    });\n    check(res, {\n      'is status 200': (r) => r.status === 200,\n    });\n  });\n\n  sleep(1); // Sleep for 1 second between iterations\n}\nEOF\n
4. Analyze the results: Generate an json report with detailed insight by running:

k6 run --out json=report.json k6-script.js\n
5. ***Repeat for different scenarios**:

running (2m00.6s), 000/100 VUs, 9048 complete and 0 interrupted iterations default \u2713 [======================================] 000/100 VUs 2m0s ```

running (2m00.7s), 000/100 VUs, 9031 complete and 0 interrupted iterations default \u2713 [======================================] 000/100 VUs 2m0s ```

running (2m00.2s), 000/100 VUs, 8655 complete and 0 interrupted iterations default \u2713 [======================================] 000/100 VUs 2m0s ```

"},{"location":"performance/#app-only","title":"App only","text":"

In this case , request are sent directly to the sample application ie no Envoy and Kyverno-plugin in the request path . For this run this command to apply the sample applicaition and then test with k6

$ kubectl apply -f https://raw.githubusercontent.com/kyverno/kyverno-envoy-plugin/main/tests/performance-test/manifest/app.yaml\n
Results of the k6 when only application is applied ```bash

  /\\      |\u203e\u203e| /\u203e\u203e/   /\u203e\u203e/\n

/ / | |/ / / / / \\/ | ( / \u203e\u203e / | | | (\u203e) | / __________ || _ __/ .io

execution: local script: k6-script.js output: -

scenarios: (100.00%) 1 scenario, 100 max VUs, 2m30s max duration (incl. graceful stop): * default: Up to 100 looping VUs for 2m0s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)

\u2588 GET /book with guest token

\u2713 is status 200

checks.........................: 100.00% \u2713 9048 \u2717 0 data_received..................: 2.1 MB 18 kB/s data_sent......................: 2.6 MB 21 kB/s group_duration.................: avg=1.01ms min=166.46\u00b5s med=775.01\u00b5s max=36ms p(90)=1.72ms p(95)=2.31ms http_req_blocked...............: avg=15.08\u00b5s min=1.55\u00b5s med=6.54\u00b5s max=4.09ms p(90)=12.07\u00b5s p(95)=15.25\u00b5s http_req_connecting............: avg=4.58\u00b5s min=0s med=0s max=1.57ms p(90)=0s p(95)=0s http_req_duration..............: avg=745.73\u00b5s min=103.06\u00b5s med=549.17\u00b5s max=35.88ms p(90)=1.26ms p(95)=1.75ms { expected_response:true }...: avg=745.73\u00b5s min=103.06\u00b5s med=549.17\u00b5s max=35.88ms p(90)=1.26ms p(95)=1.75ms http_req_failed................: 0.00% \u2713 0 \u2717 9048 http_req_receiving.............: avg=119.69\u00b5s min=11.33\u00b5s med=77.78\u00b5s max=10.97ms p(90)=193.73\u00b5s p(95)=285.58\u00b5s http_req_sending...............: avg=41\u00b5s min=6.96\u00b5s med=31.12\u00b5s max=2.39ms p(90)=61.88\u00b5s p(95)=78.15\u00b5s http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s http_req_waiting...............: avg=585.04\u00b5s min=75.52\u00b5s med=407.87\u00b5s max=35.84ms p(90)=965.49\u00b5s p(95)=1.33ms http_reqs......................: 9048 75.050438/s iteration_duration.............: avg=1s min=1s med=1s max=1.06s p(90)=1s p(95)=1s iterations.....................: 9048 75.050438/s vus............................: 2 min=2 max=100 vus_max........................: 100 min=100 max=100

"},{"location":"performance/#app-and-envoy","title":"App and Envoy","text":"

In this case, Kyverno-envoy-plugin is not included in the path but Envoy is but Envoy External Authorization API disabled For this run this command to apply the sample application with envoy.

$ kubectl apply -f https://raw.githubusercontent.com/kyverno/kyverno-envoy-plugin/main/tests/performance-test/manifest/app-envoy.yaml\n

Results of k6 after applying sample-application with envoy. ```bash

  /\\      |\u203e\u203e| /\u203e\u203e/   /\u203e\u203e/\n

/ / | |/ / / / / \\/ | ( / \u203e\u203e / | | | (\u203e) | / __________ || _ __/ .io

execution: local script: k6-script.js output: -

scenarios: (100.00%) 1 scenario, 100 max VUs, 2m30s max duration (incl. graceful stop): * default: Up to 100 looping VUs for 2m0s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)

\u2588 GET /book with guest token

\u2713 is status 200

checks.........................: 100.00% \u2713 9031 \u2717 0 data_received..................: 2.5 MB 21 kB/s data_sent......................: 2.6 MB 21 kB/s group_duration.................: avg=2.66ms min=457.22\u00b5s med=1.8ms max=65.53ms p(90)=4.85ms p(95)=6.58ms http_req_blocked...............: avg=12.81\u00b5s min=1.52\u00b5s med=5.98\u00b5s max=2.41ms p(90)=11.84\u00b5s p(95)=13.9\u00b5s http_req_connecting............: avg=3.82\u00b5s min=0s med=0s max=2.34ms p(90)=0s p(95)=0s http_req_duration..............: avg=2.38ms min=383.7\u00b5s med=1.58ms max=65.22ms p(90)=4.36ms p(95)=5.92ms { expected_response:true }...: avg=2.38ms min=383.7\u00b5s med=1.58ms max=65.22ms p(90)=4.36ms p(95)=5.92ms http_req_failed................: 0.00% \u2713 0 \u2717 9031 http_req_receiving.............: avg=136.3\u00b5s min=12.53\u00b5s med=76.74\u00b5s max=12.75ms p(90)=183.23\u00b5s p(95)=272.91\u00b5s http_req_sending...............: avg=41.54\u00b5s min=6.58\u00b5s med=28.1\u00b5s max=4.15ms p(90)=59.62\u00b5s p(95)=74.85\u00b5s http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s http_req_waiting...............: avg=2.2ms min=349.23\u00b5s med=1.43ms max=65.08ms p(90)=4.05ms p(95)=5.52ms http_reqs......................: 9031 74.825497/s iteration_duration.............: avg=1s min=1s med=1s max=1.06s p(90)=1s p(95)=1s iterations.....................: 9031 74.825497/s vus............................: 3 min=3 max=100 vus_max........................: 100 min=100 max=100

"},{"location":"performance/#app-envoy-and-kyverno-envoy-plugin","title":"App, Envoy and Kyverno-envoy-plugin","text":"

In this case, performance measurements are observed with Envoy External Authorization API enabled and a sample real-world RBAC policy loaded into kyverno-envoy-plugin . For this apply this command to apply sample-application, envoy and kyverno-envoy-plugin

$ kubectl apply -f https://raw.githubusercontent.com/kyverno/kyverno-envoy-plugin/main/tests/performance-test/manifest/app-envoy-plugin.yaml\n

Results of k6 after applying sample-application, Envoy and kyverno-envoy-plugin . ```console

  /\\      |\u203e\u203e| /\u203e\u203e/   /\u203e\u203e/\n

/ / | |/ / / / / \\/ | ( / \u203e\u203e / | | | (\u203e) | / __________ || _ __/ .io

execution: local script: k6-script.js output: -

scenarios: (100.00%) 1 scenario, 100 max VUs, 2m30s max duration (incl. graceful stop): * default: Up to 100 looping VUs for 2m0s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)

\u2588 GET /book with guest token

\u2713 is status 200

checks.........................: 100.00% \u2713 8655 \u2717 0 data_received..................: 2.4 MB 20 kB/s data_sent......................: 2.4 MB 20 kB/s group_duration.................: avg=46.54ms min=4.59ms med=29.69ms max=337.79ms p(90)=109.35ms p(95)=140.51ms http_req_blocked...............: avg=11.88\u00b5s min=1.21\u00b5s med=4.15\u00b5s max=2.83ms p(90)=9.87\u00b5s p(95)=11.4\u00b5s http_req_connecting............: avg=4.98\u00b5s min=0s med=0s max=2.18ms p(90)=0s p(95)=0s http_req_duration..............: avg=46.37ms min=4.49ms med=29.49ms max=337.69ms p(90)=109.26ms p(95)=140.28ms { expected_response:true }...: avg=46.37ms min=4.49ms med=29.49ms max=337.69ms p(90)=109.26ms p(95)=140.28ms http_req_failed................: 0.00% \u2713 0 \u2717 8655 http_req_receiving.............: avg=65.19\u00b5s min=11.14\u00b5s med=56.47\u00b5s max=5.58ms p(90)=102.86\u00b5s p(95)=145.19\u00b5s http_req_sending...............: avg=30.35\u00b5s min=5.43\u00b5s med=18.48\u00b5s max=5.29ms p(90)=46.63\u00b5s p(95)=58\u00b5s http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s http_req_waiting...............: avg=46.27ms min=4.43ms med=29.42ms max=337.65ms p(90)=109.22ms p(95)=140.24ms http_reqs......................: 8655 71.999297/s iteration_duration.............: avg=1.04s min=1s med=1.03s max=1.33s p(90)=1.11s p(95)=1.14s iterations.....................: 8655 71.999297/s vus............................: 2 min=2 max=100 vus_max........................: 100 min=100 max=100

"},{"location":"performance/#measuring-performance","title":"Measuring Performance","text":"

The following metrics should be measured to evaluate the performance impact of the kyverno-envoy-plugin:

  • End-to-end latency The end-to-end latency represents the time taken for a request to complete, from the client sending the request to receiving the response. Based on the k6 results, the average end-to-end latency for the different scenarios is as follows:

  • App Only: avg=1.01ms (from group_duration or http_req_duration)

  • App and Envoy: avg=2.38ms (from http_req_duration)
  • App, Envoy, and Kyverno-envoy-plugin: avg=46.37ms (from http_req_duration)

  • Kyverno evaluation latency The Kyverno evaluation latency represents the time taken by the kyverno-envoy-plugin to evaluate the request against the configured policies. While the k6 results do not directly provide this metric, an estimate can be inferred by analyzing the differences in latency between the \"App and Envoy\" scenario and the \"App, Envoy, and Kyverno-envoy-plugin\" scenario.

The difference in average latency between these two scenarios is: 46.37ms - 2.38ms = 43.99ms

This difference can be attributed to the Kyverno evaluation latency and the gRPC server handler latency combined. Assuming the gRPC server handler latency is relatively small compared to the Kyverno evaluation latency, the estimated range for the Kyverno evaluation latency is around 40ms to 45ms.

  • Resource utilization Refers to CPU and memory usage of the Kyverno-Envoy-Plugin container , kubectl top utility can be laveraged to measure the resource utilization.

Get the resource utilization of the kyverno-envoy-plugin container using the following command:

$ kubectl top pod -n demo --containers\n

To monitor resource utilization overtime use the following command:

$ watch -n 1 \"kubectl top pod -n demo --containers\"\n

Now run the k6 script in different terminal window and observe the resource utilization of the kyverno-envoy-plugin container.

Initial resource utilization of the kyverno-envoy-plugin container:

POD                        NAME                   CPU(cores)   MEMORY(bytes)\ntestapp-5955cd6f8b-dbvgd   envoy                  4m           70Mi\ntestapp-5955cd6f8b-dbvgd   kyverno-envoy-plugin   1m           51Mi\ntestapp-5955cd6f8b-dbvgd   test-application       1m           11Mi\n

Resource utilization of the kyverno-envoy-plugin container after 100 requests:

POD                        NAME                   CPU(cores)   MEMORY(bytes)\ntestapp-5955cd6f8b-dbvgd   envoy                  110m         70Mi\ntestapp-5955cd6f8b-dbvgd   kyverno-envoy-plugin   895m         60Mi\ntestapp-5955cd6f8b-dbvgd   test-application       17m          15Mi\n

Observations:

  • The CPU utilization of the kyverno-envoy-plugin container increased significantly from 1m to 895m after receiving 100 requests during the load test.
  • The memory utilization also increased, but to a lesser extent, from 51Mi to 60Mi.

Resource utilization of the kyverno-envoy-plugin container after load completion:

POD                        NAME                   CPU(cores)   MEMORY(bytes)\ntestapp-5955cd6f8b-dbvgd   envoy                  4m           70Mi\ntestapp-5955cd6f8b-dbvgd   kyverno-envoy-plugin   1m           51Mi\ntestapp-5955cd6f8b-dbvgd   test-application       1m           11Mi\n

Observations: - After the load test completed and the request volume returned to normal levels, the CPU and memory utilization of the kyverno-envoy-plugin container returned to their initial values. This indicates that the kyverno-envoy-plugin can efficiently handle the increased load during the test and release the additional resources when the load subsides.

Correlation with k6 results: - The k6 script simulated a load test scenario with 100 virtual users, ramping up over 30 seconds, staying at 100 users for 1 minute, and then ramping down over 30 seconds. - During the load test, when the request volume was at its peak (100 virtual users), the kyverno-envoy-plugin container experienced a significant increase in CPU utilization, reaching 895m. - This CPU utilization spike aligns with the increased processing demand on the kyverno-envoy-plugin to evaluate the incoming requests against the configured Kyverno policies. - The memory utilization increase during the load test was relatively modest, suggesting that the policy evaluation did not significantly impact the memory requirements of the kyverno-envoy-plugin.

"},{"location":"quick-start/","title":"Quick Start","text":"

This section presumes testing is conducted with Envoy version 1.10.0 or newer.

"},{"location":"quick-start/#required-tools","title":"Required tools","text":"
  1. minikube
  2. kubectl
"},{"location":"quick-start/#create-a-local-cluster","title":"Create a local cluster","text":"

Start minikube cluster with the following command:

minikube start\n
"},{"location":"quick-start/#install-kyverno-envoy-sidecar-with-application","title":"Install kyverno-envoy sidecar with application","text":"

Install application with envoy and kyverno-envoy-plugin as a sidecar container.

kubectl apply -f https://raw.githubusercontent.com/kyverno/kyverno-envoy-plugin/main/quick_start.yaml \n
The applicaition.yaml manifest defines the following resource:

  • The Deployment includes an example Go application that provides information of books in the library books collection and exposes APIs to get, create and delete books collection. Check this out for more information about the Go test application .

  • The Deployment also includes a kyverno-envoy-plugin sidecar container in addition to the Envoy sidecar container. When Envoy recevies API request destined for the Go test applicaiton, it will check with kyverno-envoy-plugin to decide if the request should be allowed and the kyverno-envoy-plugin sidecar container is configured to query Kyverno-json engine for policy decisions on incoming requests.

  • A ConfigMap policy-config is used to pass the policy to kyverno-envoy-plugin sidecar in the namespace default where the application is deployed .

  • A ConfigMap envoy-config is used to pass an Envoy configuration with an External Authorization Filter to direct authorization checks to the kyverno-envoy-plugin sidecar.

  • The Deployment also includes an init container that install iptables rules to redirect all container traffic to the Envoy proxy sidecar container , more about init container can be found here

"},{"location":"quick-start/#make-test-application-accessible-in-the-cluster","title":"Make Test application accessible in the cluster .","text":"
kubectl expose deployment testapp --type=NodePort --name=testapp-service --port=8080\n
"},{"location":"quick-start/#set-the-service_url-environment-variable-to-the-services-ipport","title":"Set the SERVICE_URL environment variable to the service's IP/port.","text":"

minikube:

export SERVICE_PORT=$(kubectl get service testapp -o jsonpath='{.spec.ports[?(@.port==8080)].nodePort}')\nexport SERVICE_HOST=$(minikube ip)\nexport SERVICE_URL=$SERVICE_HOST:$SERVICE_PORT\necho $SERVICE_URL\n
"},{"location":"quick-start/#calling-the-sample-test-application-and-verify-the-authorization","title":"Calling the sample test application and verify the authorization","text":"

For convenience, we\u2019ll want to store Alice\u2019s and Bob\u2019s tokens in environment variables. Here bob is assigned the admin role and alice is assigned the guest role.

export ALICE_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk\"\nexport BOB_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6ImFkbWluIiwic3ViIjoiWVd4cFkyVT0ifQ.veMeVDYlulTdieeX-jxFZ_tCmqQ_K8rwx2OktUHv5Z0\"\n

The policy we passed to kyverno-envoy-plugin sidecar in the ConfigMap policy-config is configured to check the conditions of the incoming request and denies the request if the user is a guest and the request method is POST at the /book path.

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n    name: checkrequest\nspec:\n    rules:\n    - name: deny-guest-request-at-post\n        assert:\n        any:\n        - message: \"POST method calls at path /book are not allowed to guests users\"\n            check:\n            request:\n                http:\n                    method: POST\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book                             \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): guest\n                    path: /book               \n

Check for Alice which can get book but cannot create book.

curl -i -H \"Authorization: Bearer \"$ALICE_TOKEN\"\" http://$SERVICE_URL/book\n
curl -i -H \"Authorization: Bearer \"$ALICE_TOKEN\"\" -d '{\"bookname\":\"Harry Potter\", \"author\":\"J.K. Rowling\"}' -H \"Content-Type: application/json\" -X POST http://$SERVICE_URL/book\n
Check the Bob which can get book also create the book

curl -i -H \"Authorization: Bearer \"$BOB_TOKEN\"\" http://$SERVICE_URL/book\n
curl -i -H \"Authorization: Bearer \"$BOB_TOKEN\"\" -d '{\"bookname\":\"Harry Potter\", \"author\":\"J.K. Rowling\"}' -H \"Content-Type: application/json\" -X POST http://$SERVICE_URL/book\n

Check on logs

kubectl logs \"$(kubectl get pod -l app=testapp -o jsonpath={.items..metadata.name})\" -c kyverno-envoy-plugin -f\n
First , third and last request is passed but second request is failed.

sanskar@sanskar-HP-Laptop-15s-du1xxx:~$ kubectl logs \"$(kubectl get pod -l app=testapp -n demo -o jsonpath={.items..metadata.name})\" -n demo -c kyverno-envoy-plugin -f\nStarting HTTP server on Port 8000\nStarting GRPC server on Port 9000\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:11:42 Request passed the deny-guest-request-at-post policy rule.\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:22:11 Request violation: -> POST method calls at path /book are not allowed to guests users\n -> any[0].check.request.http.headers.authorization.(split(@, ' ')[1]).(jwt_decode(@ , 'secret').payload.role): Invalid value: \"guest\": Expected value: \"admin\"\n-> GET method call is allowed to both guest and admin users\n -> any[1].check.request.http.headers.authorization.(split(@, ' ')[1]).(jwt_decode(@ , 'secret').payload.role): Invalid value: \"guest\": Expected value: \"admin\"\n -> any[1].check.request.http.method: Invalid value: \"POST\": Expected value: \"GET\"\n-> GET method call is allowed to both guest and admin users\n -> any[2].check.request.http.method: Invalid value: \"POST\": Expected value: \"GET\"\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:23:13 Request passed the deny-guest-request-at-post policy rule.\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:23:55 Request passed the deny-guest-request-at-post policy rule.\n
"},{"location":"quick-start/#configuration","title":"Configuration","text":"

To deploy Kyverno-Envoy include the following container in your kubernetes Deployments:

- name: kyverno-envoy-plugin\n  image: sanskardevops/plugin:0.0.34\n  imagePullPolicy: IfNotPresent\n  ports:\n    - containerPort: 8181\n    - containerPort: 9000\n  volumeMounts:\n    - readOnly: true\n  args:\n    - \"serve\"\n    - \"--policy=/policies/policy.yaml\"\n    - \"--address=:9000\"\n    - \"--healthaddress=:8181\"\n  livenessProbe:\n    httpGet:\n      path: /health\n      scheme: HTTP\n      port: 8181\n    initialDelaySeconds: 5\n    periodSeconds: 5\n  readinessProbe:\n    httpGet:\n      path: /health\n      scheme: HTTP\n      port: 8181\n    initialDelaySeconds: 5\n    periodSeconds: 5  \n
"},{"location":"community/","title":"Community","text":"

The Kyverno Envoy Plugin has a growing community and we would definitely love to see you join and contribute.

Everyone is welcome to make suggestions, report bugs, open feature requests, contribute code or docs, participate in discussions, write blogs or anything that can benefit the project.

The Kyverno Envoy Plugin is built and maintained under the Kyverno umbrella but decisions are Community driven Everyone's voice matters

"},{"location":"community/#slack-channel","title":"Slack channel","text":"

Join our slack channel #kyverno to meet with users, contributors and maintainers.

"},{"location":"community/#roadmap","title":"RoadMap","text":"

For detailed information on our planned features and upcoming updates, please view our Roadmap.

"},{"location":"community/#contributing","title":"Contributing","text":"

Please read the contributing guide for details around:

  1. Code of Conduct
  2. Code Culture
  3. Details on how to contribute
"},{"location":"community/#adopters","title":"Adopters","text":"

If you are using the Kyverno Envoy Plugin and want to share it publicly we always appreciate a bit of support. Pull requests to the ADOPTERS LIST will put a smile on our faces

"},{"location":"community/contribute/","title":"Contributing","text":"

Kyverno Envoy Plugin, developed by Kyverno, is an advanced end-to-end testing tool for Kubernetes. Our community plays a crucial role in shaping the project by reporting bugs, suggesting features, and improving documentation.

We aim to make our issue tracker, discussion board, and documentation well-structured and easy to navigate. By following our guidelines, you can help us address your requests efficiently.

"},{"location":"community/contribute/#how-you-can-contribute","title":"How you can contribute","text":"

We appreciate your efforts in reporting bugs, requesting features, and engaging in discussions. Here's how you can contribute:

"},{"location":"community/contribute/#creating-an-issue","title":"Creating an issue","text":"
  • Something is not working?

    Report a bug by creating an issue with a reproduction

    Report a bug

  • Missing information in our docs?

    Report missing information or potential inconsistencies in our documentation

    Report a docs issue

  • Want to submit an idea?

    Propose a change, feature request, or suggest an improvement

    Request a change

  • Have a question or need help?

    Ask a question on our discussion board and get in touch with our community

    Ask a question

"},{"location":"community/contribute/#contributing_1","title":"Contributing","text":"
  • Want to create a pull request?

    Learn how to create a comprehensive and useful pull request (PR)

    Create a pull request

"},{"location":"community/contribute/#checklist","title":"Checklist","text":"

Before interacting within the project, please consider the following questions to ensure you're using the correct issue template and providing all necessary information.

Issues, discussions, and comments are forever

Please note that everything you write is permanent and will remain for everyone to read \u2013 forever. Therefore, please always be nice and constructive, follow our contribution guidelines, and comply with our Code of Conduct.

"},{"location":"community/contribute/#before-creating-an-issue","title":"Before creating an issue","text":"
  • Are you using the appropriate issue template, or is there another issue template that better fits the context of your request?
  • Have you checked if a similar bug report or change request has already been created, or have you stumbled upon something that might be related?
  • Did you fill out every field as requested and provide all additional information needed to comprehend your request?
"},{"location":"community/contribute/#before-asking-a-question","title":"Before asking a question","text":"
  • Is the topic a question for our discussion board, or is it a bug report or change request that should be raised on our issue tracker?
  • Is there an open discussion on the topic of your request? If the answer is yes, does your question match the direction of the discussion, or should you open a new discussion?
  • Did you provide our community with all the necessary information to understand your question and help you quickly, or can you make it easier to help you?
"},{"location":"community/contribute/#before-commenting","title":"Before commenting","text":"
  • Is your comment relevant to the topic of the current page, post, issue, or discussion, or is it better to create a new issue or discussion?
  • Does your comment add value to the conversation? Is it constructive and respectful to our community and maintainers? Could you just use a reaction instead?
"},{"location":"community/contribute/#rights-and-responsibilities","title":"Rights and responsibilities","text":"

As maintainers, we are entrusted with the responsibility to moderate communication within our community, including the authority to close, remove, reject, or edit issues, discussions, comments, commits, and to block users who do not align with our contribution guidelines and our Code of Conduct. This role requires us to be actively involved in maintaining the integrity and positive atmosphere of our community. Upholding these standards decisively ensures a respectful and inclusive environment for all members.

"},{"location":"community/contribute/#code-of-conduct","title":"Code of Conduct","text":"

Our Code of Conduct outlines the expectation for all community members to treat one another with respect, employing inclusive and welcoming language. Our commitment is to foster a positive and supportive environment, free of inappropriate, offensive, or harmful behavior.

We take any violations seriously and will take appropriate action in response to uphold these values.1

"},{"location":"community/contribute/#incomplete-issues-and-duplicates","title":"Incomplete issues and duplicates","text":"

We have invested significant time and effort in the setup of our contribution process, ensuring that we assess the essential requirements for reviewing and responding to issues effectively. Each field in our issue templates is thoughtfully designed to help us fully understand your concerns and the nature of your matter. We encourage all members to utilize the search function before submitting new issues or starting discussions to help avoid duplicates. Your cooperation is crucial in keeping our community's discussions constructive and organized.

  • Mandatory completion of issue templates: We need all of the information required in our issue templates because it ensures that every user and maintainer, regardless of their experience, can understand the content and severity of your bug report or change request.

  • Closing incomplete issues: We reserve the right to close issues lacking essential information, such as but not limited to [minimal reproductions] or those not adhering to the quality standards and requirements specified in our issue templates. Such issues can be reopened once the missing information has been provided.

  • Handling duplicates: To maintain organized and efficient communication within our issue tracker and discussion board, we reserve the right to close any duplicated issues or lock duplicated discussions. Opening multiple channels to ask the same question or report the same issue across different forums hinders our ability to manage and address community concerns effectively. This approach is vital for efficient time management, as duplicated questions can consume the time of multiple team members simultaneously. Ensuring that each issue or discussion is unique and progresses with new information helps us to maintain focus and support our community.

    We further reserve the right to immediately close discussions or issues that are reopened without providing new information or simply because users have not yet received a response to their issue/question, as the issue is marked as incomplete.

  • Limitations of automated tools: While we believe in the value and efficiency that automated tools bring to identifying potential issues (such as those identified by Lighthouse, Accessibility tools, and others), simply submitting an issue generated by these tools does not constitute a complete bug report. These tools sometimes produce verbose outputs and may include false positives, which necessitate a critical evaluation. You are of course welcome to attach generated reports to your issue. However, this does not substitute the requirement for a minimal reproduction or a thorough discussion of the findings. We reserve the right to mark these issues as incomplete and close them. This practice ensures that we are addressing genuine concerns with precision and clarity, rather than navigating through extensive automated outputs.

  1. Warning and blocking policy: Given the increasing popularity of our project and our commitment to a healthy community, we've defined clear guidelines on how we proceed with violations:

    1.1. First warning: Users displaying repeated inappropriate, offensive, or harmful behavior will receive a first warning. This warning serves as a formal notice that their behavior is not in alignment with our community standards and Code of Conduct. The first warning is permanent.

    1.2. Second warning and opportunity for resolution: If the behavior persists, a second warning will be issued. Upon receiving the second warning, the user will be given a 5-day period for reflection, during which they are encouraged to publicly explain or apologize for their actions. This period is designed to offer an opportunity for openly clearing out any misunderstanding.

    1.3. Blocking: Should there be no response or improvement in behavior following the second warning, we reserve the right to block the user from the community and repository. Blocking is considered a last resort, used only when absolutely necessary to protect the community's integrity and positive atmosphere.

    Blocking has been an exceptionally rare necessity in our overwhelmingly positive community, highlighting our preference for constructive dialogue and mutual respect. It aims to protect our community members and team.\u00a0\u21a9

"},{"location":"community/making-a-pull-request/","title":"Pull Requests","text":"

You can contribute by making a pull request that will be reviewed by maintainers and integrated into the main repository when the changes made are approved. You can contribute bug fixes, documentation changes, or new functionalities.

Considering a pull request

Before deciding to spend effort on making changes and creating a pull request, please discuss what you intend to do. If you are responding to what you think might be a bug, please issue a bug report first. If you intend to work on documentation, create a documentation issue. If you want to work on a new feature, please create a change request.

Keep in mind the guidance given and let people advise you. It might be that there are easier solutions to the problem you perceive and want to address. It might be that what you want to achieve can already be done by configuration or [customization].

"},{"location":"community/making-a-pull-request/#learning-about-pull-requests","title":"Learning about pull requests","text":"

Pull requests are a concept layered on top of Git by services that provide Git hosting. Before you consider making a pull request, you should familiarize yourself with the documentation on GitHub, the service we are using. The following articles are of particular importance:

  1. Forking a repository
  2. Creating a pull request from a fork
  3. Creating a pull request

Note that they provide tailored documentation for different operating systems and different ways of interacting with GitHub. We do our best in the documentation here to describe the process as it applies but cannot cover all possible combinations of tools and ways of doing things. It is also important that you understand the concept of a pull-request in general before continuing.

"},{"location":"community/making-a-pull-request/#pull-request-process","title":"Pull request process","text":"

In the following, we describe the general process for making pull requests. The aim here is to provide the 30k ft overview before describing details later on.

"},{"location":"community/making-a-pull-request/#preparing-changes-and-draft-pr","title":"Preparing changes and draft PR","text":"

The diagram below describes what typically happens to repositories in the process or preparing a pull request. We will be discussing the review-revise process below. It is important that you understand the overall process first before you worry about specific commands. This is why we cover this first before providing instructions below.

sequenceDiagram\n  autonumber\n\n  participant upstream\n  participant PR\n  participant fork\n  participant local\n\n  upstream ->> fork: fork on GitHub\n  fork ->> local: clone to local\n  local ->> local: branch\n  loop prepare\n    loop push\n      loop edit\n        local ->> local: commit\n      end\n      local ->> fork: push\n    end\n    upstream ->> fork: merge in any changes\n    fork ->>+ PR: create draft PR\n    PR ->> PR: review your changes\n  end
  1. Fork the Repository: Fork the upstream repository on GitHub to create your own copy.
  2. Clone to Local: Clone your fork to your local machine.
  3. Create a Branch: Create a topic branch for your changes.
  4. Set Up Development Environment: Follow the instructions to set up a development environment.
  5. Iterate and Commit: Make incremental changes and commit them with meaningful messages.
  6. Push Regularly: Push your commits to your fork regularly.
  7. Merge Changes from Upstream: Regularly merge changes from the original upstream repository to avoid conflicts.
  8. Create a Draft Pull Request: Once satisfied with your changes, create a draft pull request for early feedback.
  9. Review and Revise: Review your work critically, address feedback, and refine your changes.
"},{"location":"community/making-a-pull-request/#finalizing","title":"Finalizing","text":"

Once you are happy with your changes, you can move to the next step, finalizing your pull request and asking for a more formal and detailed review. The diagram below shows the process:

sequenceDiagram\n  autonumber\n  participant upstream\n  participant PR\n  participant fork\n  participant local\n\n  activate PR\n  PR ->> PR: finalize PR\n  loop review\n    loop discuss\n      PR ->> PR: request review\n      PR ->> PR: discussion\n      local ->> fork: push further changes\n    end\n    PR ->> upstream: merge (and squash)\n    deactivate PR\n    fork ->> fork: delete branch\n    upstream ->> fork: pull\n    local ->> local: delete branch\n    fork ->> local: pull\n  end\n
  1. Finalize PR: Signal that your changes are ready for review.
  2. Request Review: Ask the maintainer to review your changes.
  3. Discuss and Revise: Engage in discussions, make necessary revisions, and iterate.
  4. Merge and Squash: Once approved, the maintainer will merge and possibly squash your commits.
  5. Clean Up: Delete the branch used for the PR from both your fork and local clone.
"},{"location":"community/reporting-a-bug/","title":"Bug Reports","text":"

If you think you have discovered a bug, you can help us by submitting an issue in our public issue tracker, following this guide.

"},{"location":"community/reporting-a-bug/#before-creating-an-issue","title":"Before Creating an Issue","text":"

With numerous users, issues are created regularly. The maintainers of this project strive to address bugs promptly. By following this guide, you will know exactly what information we need to help you quickly.

Please do the following before creating an issue:

"},{"location":"community/reporting-a-bug/#upgrade-to-latest-version","title":"Upgrade to Latest Version","text":"

Chances are that the bug you discovered was already fixed in a subsequent version. Before reporting an issue, ensure that you're running the latest version.

Bug fixes are not backported

Please understand that only bugs that occur in the latest version will be addressed. Also, to reduce duplicate efforts, fixes cannot always be backported to earlier versions.

"},{"location":"community/reporting-a-bug/#remove-customizations","title":"Remove Customizations","text":"

If you're using customizations like additional configurations, remove them before reporting a bug. We can't offer official support for bugs that might hide in your overrides, so make sure to omit custom settings from your configuration files.

Don't be shy to ask on our discussion board for help if you run into problems.

"},{"location":"community/reporting-a-bug/#search-for-solutions","title":"Search for Solutions","text":"

At this stage, we know that the problem persists in the latest version and is not caused by any of your customizations. However, the problem might result from a small typo or a syntactical error in a configuration file.

Before creating a bug report, save time for us and yourself by doing some research:

  1. Search our documentation for relevant sections related to your problem. Ensure everything is configured correctly.
  2. [Search our issue tracker] as another user might have already reported the same problem.
  3. [Search our discussion board] to see if other users are facing similar issues and find possible solutions.

Keep track of all search terms and relevant links; you'll need them in the bug report.

If you still haven't found a solution to your problem, create an issue. It's now likely that you've encountered something new. Read the following section to learn how to create a complete and helpful bug report.

"},{"location":"community/reporting-a-bug/#issue-template","title":"Issue Template","text":"

We have created a new issue template to make the bug reporting process as simple as possible and more efficient for our community and us. It consists of the following parts:

  • Title
  • Context optional
  • Bug Description
  • Related Links
  • Reproduction
  • Steps to Reproduce
  • Browser optional
  • Checklist
"},{"location":"community/reporting-a-bug/#title","title":"Title","text":"

A good title is short and descriptive. It should be a one-sentence executive summary of the issue, so the impact and severity of the bug can be inferred from the title.

Example Clear apply command fails with specific CRD Wordy The apply command fails when used with a certain Custom Resource Definition Unclear Command does not work Useless Help"},{"location":"community/reporting-a-bug/#context","title":"Context optional","text":"

Before describing the bug, you can provide additional context to help us understand what you were trying to achieve. Explain the circumstances under which the bug happens, and what you think might be relevant. Don't describe the bug here.

"},{"location":"community/reporting-a-bug/#bug-description","title":"Bug Description","text":"

Provide a clear, focused, specific, and concise summary of the bug you encountered. Explain why you think this is a bug that should be reported, and not to one of its dependencies. Follow these principles:

  • Explain the what, not the how \u2013 don't explain how to reproduce the bug here, we're getting there. Focus on articulating the problem and its impact.
  • Keep it short and concise \u2013 if the bug can be precisely explained in one or two sentences, perfect. Don't inflate it.
  • One bug at a time \u2013 if you encounter several unrelated bugs, create separate issues for them.
"},{"location":"community/reporting-a-bug/#related-links","title":"Related Links","text":"

Share links to relevant sections of our documentation and any related issues or discussions. This helps us improve our documentation and understand the problem better.

"},{"location":"community/reporting-a-bug/#reproduction","title":"Reproduction","text":"

A minimal reproduction is essential for a well-written bug report, as it allows us to recreate the conditions necessary to inspect the bug. Follow the guide to create a reproduction:

[ Create reproduction][Create reproduction]{ .md-button .md-button--primary }

After creating the reproduction, you should have a .zip file, ideally not larger than 1 MB. Drag and drop the .zip file into the issue field, which will automatically upload it to GitHub.

Don't share links to repositories

While linking to a repository is a common practice, we currently don't support this. The reproduction, created using the built-in info plugin, contains all necessary environment information.

"},{"location":"community/reporting-a-bug/#steps-to-reproduce","title":"Steps to Reproduce","text":"

List specific steps to follow when running your reproduction to observe the bug. Keep the steps concise and ensure nothing is left out. Use simple language and focus on continuity.

"},{"location":"community/reporting-a-bug/#browser","title":"Browser optional","text":"

If the bug only occurs in specific browsers, let us know which ones are affected. This field is optional, as it is only relevant for bugs that do not involve a crash when previewing or building your site.

Incognito Mode

Verify that the bug is not caused by a browser extension by switching to incognito mode. If the bug disappears, it is likely caused by an extension.

"},{"location":"community/reporting-a-bug/#checklist","title":"Checklist","text":"

Before submitting, ensure you have:

  • Followed this guide thoroughly
  • Provided all necessary information
  • Created a minimal reproduction

Thanks for following the guide and creating a high-quality bug report. We will take it from here.

"},{"location":"community/reporting-a-docs-issue/","title":"Documentation Issues","text":"

The documentation includes extensive information on features, configurations, customizations, and more. If you have found an inconsistency or see room for improvement, please follow this guide to submit an issue on our issue tracker.

"},{"location":"community/reporting-a-docs-issue/#issue-template","title":"Issue Template","text":"

Reporting a documentation issue is usually less involved than reporting a bug, as we don't need a [reproduction]. Please thoroughly read this guide before creating a new documentation issue, and provide the following information as part of the issue:

  • Title
  • Description
  • Related Links
  • Proposed Change optional
  • Checklist
"},{"location":"community/reporting-a-docs-issue/#title","title":"Title","text":"

A good title should be a short, one-sentence description of the issue, containing all relevant information and keywords to simplify the search in our issue tracker.

Example Clear Clarify resource templating setup Unclear Missing information in the docs Useless Help"},{"location":"community/reporting-a-docs-issue/#description","title":"Description","text":"

Provide a clear and concise summary of the inconsistency or issue you encountered in the documentation or the documentation section that needs improvement. Explain why you think the documentation should be adjusted and describe the severity of the issue:

  • Keep it short and concise \u2013 if the inconsistency or issue can be precisely explained in one or two sentences, perfect. Maintainers and future users will be grateful for having to read less.
  • One issue at a time \u2013 if you encounter several unrelated inconsistencies, please create separate issues for them.

Why we need this: describing the problem clearly and concisely is a prerequisite for improving our documentation \u2013 we need to understand what's wrong so we can fix it.

"},{"location":"community/reporting-a-docs-issue/#related-links","title":"Related Links","text":"

After you describe the documentation section that needs to be adjusted, share the link to this specific documentation section and other possibly related sections. Use anchor links (permanent links) where possible, as it simplifies discovery.

Why we need this: providing the links to the documentation helps us understand which sections of our documentation need to be adjusted, extended, or overhauled.

"},{"location":"community/reporting-a-docs-issue/#proposed-change","title":"Proposed Change optional","text":"

Now that you have provided us with the description and links to the documentation sections, you can help us, maintainers, and the community by proposing an improvement. You can sketch out rough ideas or write a concrete proposal. This field is optional but very helpful.

Why we need this: an improvement proposal can be beneficial for other users who encounter the same issue, as they offer solutions before we maintainers can update the documentation.

"},{"location":"community/reporting-a-docs-issue/#checklist","title":"Checklist","text":"

Thanks for following the guide and providing valuable feedback for our documentation \u2013 you are almost done. The checklist ensures that you have read this guide and have worked to your best knowledge to provide us with every piece of information we need to improve it.

We'll take it from here.

"},{"location":"community/requesting-a-change/","title":"Change Requests","text":"

We value every idea or contribution from our community. Please follow this guide before submitting your change request in our public issue tracker. This helps us better understand the proposed change and how it will benefit our community.

"},{"location":"community/requesting-a-change/#before-creating-an-issue","title":"Before Creating an Issue","text":"

Before you invest time in submitting a change request, answer these questions to determine if your idea is a good fit and matches the project's philosophy and tone.

"},{"location":"community/requesting-a-change/#its-not-a-bug-its-a-feature","title":"It's Not a Bug, It's a Feature","text":"

Change requests suggest minor adjustments, new features, or influence the project's direction. They are not intended for reporting bugs. Refer to our bug reporting guide for that.

"},{"location":"community/requesting-a-change/#look-for-sources-of-inspiration","title":"Look for Sources of Inspiration","text":"

If your idea is implemented in another tool or framework, collect information on its implementation. This helps us evaluate its fit more quickly.

"},{"location":"community/requesting-a-change/#connect-with-our-community","title":"Connect with Our Community","text":"

Our discussion board is the best place to connect with our community. Seeking input from other users helps implement features that benefit a larger number of users.

Start a discussion

"},{"location":"community/requesting-a-change/#issue-template","title":"Issue Template","text":"

After doing the preliminary work, create a change request. Follow these steps:

  • Title
  • Context optional
  • Description
  • Related Links
  • Use Cases
  • Visuals optional
  • Checklist
"},{"location":"community/requesting-a-change/#title","title":"Title","text":"

A good title is short and descriptive, summarizing the idea so the potential impact and benefit can be inferred.

Example Clear Support for resource templating Wordy Add support for templating resources for easier testing Unclear Improve templating Useless Help"},{"location":"community/requesting-a-change/#context","title":"Context optional","text":"

Provide additional context to help us understand what you are trying to achieve. Explain the circumstances and relevant settings without describing the change request itself.

"},{"location":"community/requesting-a-change/#description","title":"Description","text":"

Provide a detailed and clear description of your idea. Explain why your idea is relevant and should be implemented here, not in one of its dependencies.

  • Explain the what, not the why \u2013 focus on describing the change request precisely.
  • Keep it short and concise \u2013 be brief and to the point.
  • One idea at a time \u2013 if you have multiple ideas, open separate change requests for each.
"},{"location":"community/requesting-a-change/#related-links","title":"Related Links","text":"

Provide any relevant links to issues, discussions, or documentation sections related to your change request. This helps us gain additional context.

"},{"location":"community/requesting-a-change/#use-cases","title":"Use Cases","text":"

Explain how your change request would work from an author's and user's perspective. What is the expected impact, and why does it benefit other users? Would it potentially break existing functionality?

"},{"location":"community/requesting-a-change/#visuals","title":"Visuals optional","text":"

If you have any visuals, such as sketches, screenshots, mockups, or external assets, present them in this section. If you have seen this change used in other tools, showcase and describe its implementation.

"},{"location":"community/requesting-a-change/#checklist","title":"Checklist","text":"

Thanks for following the guide and creating a high-quality change request. The checklist ensures that you have read this guide and provided all necessary information for us to review your idea.

We'll take it from here.

"},{"location":"community/requesting-a-change/#rejected-requests","title":"Rejected Requests","text":"

Your change request got rejected? We're sorry for that. We understand it can be frustrating, but we always need to consider the needs of our entire community. If you're unsure why your change request was rejected, please ask for clarification.

We consider the following principles when evaluating change requests:

  • Alignment with the project's vision and tone
  • Compatibility with existing features and plugins
  • Compatibility with all screen sizes and browsers
  • Effort of implementation and maintenance
  • Usefulness to the majority of users
  • Simplicity and ease of use
  • Accessibility

If your idea was rejected, you can always implement it via [customization]. If you're unsure how or want to know if someone has already done it, get in touch with our community on the discussion board.

"},{"location":"jp/functions/","title":"Functions","text":""},{"location":"jp/functions/#built-in-functions","title":"built-in functions","text":"Name Signature abs abs(number) avg avg(array[number]) ceil ceil(number) contains contains(array\\|string, any) ends_with ends_with(string, string) find_first find_first(string, string, number, number) find_last find_last(string, string, number, number) floor floor(number) from_items from_items(array[array]) group_by group_by(array, expref) items items(object) join join(string, array[string]) keys keys(object) length length(string\\|array\\|object) lower lower(string) map map(expref, array) max max(array[number]\\|array[string]) max_by max_by(array, expref) merge merge(object) min min(array[number]\\|array[string]) min_by min_by(array, expref) not_null not_null(any) pad_left pad_left(string, number, string) pad_right pad_right(string, number, string) replace replace(string, string, string, number) reverse reverse(array\\|string) sort sort(array[string]\\|array[number]) sort_by sort_by(array, expref) split split(string, string, number) starts_with starts_with(string, string) sum sum(array[number]) to_array to_array(any) to_number to_number(any) to_string to_string(any) trim trim(string, string) trim_left trim_left(string, string) trim_right trim_right(string, string) type type(any) upper upper(string) values values(object) zip zip(array, array)"},{"location":"jp/functions/#custom-functions","title":"custom functions","text":"Name Signature at at(array, any) concat concat(string, string) json_parse json_parse(string) wildcard wildcard(string, string)"},{"location":"jp/functions/#kyverno-functions","title":"kyverno functions","text":"Name Signature compare compare(string, string) equal_fold equal_fold(string, string) replace replace(string, string, string, number) replace_all replace_all(string, string, string) to_upper to_upper(string) to_lower to_lower(string) trim trim(string, string) trim_prefix trim_prefix(string, string) split split(string, string) regex_replace_all regex_replace_all(string, string\\|number, string\\|number) regex_replace_all_literal regex_replace_all_literal(string, string\\|number, string\\|number) regex_match regex_match(string, string\\|number) pattern_match pattern_match(string, string\\|number) label_match label_match(object, object) to_boolean to_boolean(string) add add(any, any) sum sum(array) subtract subtract(any, any) multiply multiply(any, any) divide divide(any, any) modulo modulo(any, any) round round(number, number) base64_decode base64_decode(string) base64_encode base64_encode(string) time_since time_since(string, string, string) time_now time_now() time_now_utc time_now_utc() path_canonicalize path_canonicalize(string) truncate truncate(string, number) semver_compare semver_compare(string, string) parse_json parse_json(string) parse_yaml parse_yaml(string) lookup lookup(object\\|array, string\\|number) items items(object\\|array, string, string) object_from_lists object_from_lists(array, array) random random(string) x509_decode x509_decode(string) time_to_cron time_to_cron(string) time_add time_add(string, string) time_parse time_parse(string, string) time_utc time_utc(string) time_diff time_diff(string, string) time_before time_before(string, string) time_after time_after(string, string) time_between time_between(string, string, string) time_truncate time_truncate(string, string)"},{"location":"policies/asserts/","title":"Assertion trees","text":"

Assertion trees can be used to apply complex and dynamic conditional checks using JMESPath expressions.

"},{"location":"policies/asserts/#assert","title":"Assert","text":"

An assert declaration contains an any or all list in which each entry contains a:

  • check: the assertion check
  • message: an optional message

A check can contain one or more JMESPath expressions. Expressions represent projections of selected data in the JSON payload and the result of this projection is passed to descendants for further analysis.

All comparisons happen in the leaves of the assertion tree.

A simple example:

This policy checks that a pod does not use the default service account:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: assert-sample\nspec:\n  rules:\n    - name: foo-bar\n      match:\n        all:\n        - apiVersion: v1\n          kind: Pod\n      assert:\n        all:\n        - message: \"serviceAccountName 'default' is not allowed\"\n          check:\n            spec:\n              (serviceAccountName == 'default'): false\n

A detailed example:

Given the input payload below:

foo:\n  baz: true\n  bar: 4\n  bat: 6\n

It is possible to write a validation rule like this:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar-4\n      validate:\n        assert:\n          all:\n          - message: \"...\"\n            check:\n              # project field `foo` onto itself, the content of `foo` becomes the current object for descendants\n              foo:\n\n                # evaluate expression `(bar > `3`)`, the boolean result becomes the current object for descendants\n                # the `true` leaf is compared with the current value `true`\n                (bar > `3`): true\n\n                # evaluate expression `(!baz)`, the boolean result becomes the current object for descendants\n                # the leaf `false` is compared with the current value `false`\n                (!baz): false\n\n                # evaluate expression `(bar + bat)`, the numeric result becomes the current object for descendants\n                # the leaf `10` is compared with the current value `10`\n                (bar + bat): 10\n
"},{"location":"policies/asserts/#iterating-with-projection-modifiers","title":"Iterating with Projection Modifiers","text":"

Assertion tree expressions support modifiers to influence the way projected values are processed.

The ~ modifier applies to arrays and maps, it mean the input array or map elements will be processed individually by descendants.

When the ~ modifier is not used, descendants receive the whole array, not each individual element.

Consider the following input document:

foo:\n  bar:\n  - 1\n  - 2\n  - 3\n

The policy below does not use the ~ modifier and foo.bar array is compared against the expected array:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar\n      validate:\n        assert:\n          all:\n          - foo:\n              # the content of the `bar` field will be compared against `[1, 2, 3]`\n              bar:\n              - 1\n              - 2\n              - 3\n

With the ~ modifier, we can apply descendant assertions to all elements in the array individually. The policy below ensures that all elements in the input array are < 5:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar\n      validate:\n        assert:\n          all:\n          - foo:\n              # with the `~` modifier all elements in the `[1, 2, 3]` array are processed individually and passed to descendants\n              ~.bar:\n                # the expression `(@ < `5`)` is evaluated for every element and the result is expected to be `true`\n                (@ < `5`): true\n

The ~ modifier supports binding the index of the element being processed to a named binding with the following syntax ~index_name.bar. When this is used, we can access the element index in descendants with $index_name.

When used with a map, the named binding receives the key of the element being processed.

"},{"location":"policies/asserts/#explicit-bindings","title":"Explicit bindings","text":"

Sometimes it can be useful to refer to a parent node in the assertion tree.

This is possible to add an explicit binding at every node in the tree by appending the ->binding_name to the key.

Given the input document:

foo:\n  bar: 4\n  bat: 6\n

The following policy will compute a sum and bind the result to the sum binding. A descendant can then use $sum and use it:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar\n      validate:\n        assert:\n          all:\n          - foo:\n              # evaluate expression `(bar + bat)` and bind it to `sum`\n              (bar + bat)->sum:\n                # get the `$sum` binding and compare it against `10`\n                ($sum): 10\n

All binding are available to descendants, if a descendant creates a binding with a name that already exists the binding will be overridden for descendants only and it doesn't affect the bindings at upper levels in the tree.

In other words, a node in the tree always sees bindings that are defined in the parents and if a name is reused, the first binding with the given name wins when winding up the tree.

As a consequence, the policy below will evaluate to true:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar\n      validate:\n        assert:\n          all:\n          - foo:\n              (bar + bat)->sum:\n                ($sum + $sum)->sum:\n                  ($sum): 20\n                ($sum): 10\n

Finally, we can always access the current payload, policy and rule being evaluated using the built-in $payload, $policy and $rule bindings. No protection is made to prevent you from overriding those bindings though.

"},{"location":"policies/asserts/#escaping-projection","title":"Escaping projection","text":"

It can be necessary to prevent a projection under certain circumstances.

Consider the following document:

foo:\n  (bar): 4\n  (baz):\n  - 1\n  - 2\n  - 3\n

Here the (bar) key conflict with the projection syntax. To workaround this situation, you can escape a projection by surrounding it with \\ characters like this:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar\n      validate:\n        assert:\n          all:\n          - foo:\n              \\(bar)\\: 10\n

In this case, the leading and trailing \\ characters will be erased and the projection won't be applied.

Note that it's still possible to use the ~ modifier or to create a named binding with and escaped projection.

Keys like this are perfectly valid:

  • ~index.\\baz\\
  • \\baz\\@foo
  • ~index.\\baz\\@foo
"},{"location":"policies/authz-policy/","title":"Policy Reference","text":"

This page provides guidance on writing policies for request content processed by the kyverno-json validating policy, utilizing Envoy\u2019s External Authorization filter.

"},{"location":"policies/authz-policy/#writing-policies","title":"Writing Policies","text":"

Let start with an example policy that restricts access to an endpoint based on user's role and permissions.

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n    name: checkrequest\nspec:\n    rules:\n    - name: deny-guest-request-at-post\n        assert:\n        any:\n        - message: \"POST method calls at path /book are not allowed to guests users\"\n            check:\n            request:\n                http:\n                    method: POST\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book                             \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): guest\n                    path: /book  \n

The above policy uses the jwt_decode builtin function to parse and verify the JWT containing information about the user making the request. it uses other builtins like split, base64_decode, campare, contains etc kyverno has many different function which can be used in policy.

Sample input recevied by kyverno-json validating policy is shown below:

{\n  \"source\": {\n    \"address\": {\n      \"socketAddress\": {\n        \"address\": \"10.244.1.10\",\n        \"portValue\": 59252\n      }\n    }\n  },\n  \"destination\": {\n    \"address\": {\n      \"socketAddress\": {\n        \"address\": \"10.244.1.4\",\n        \"portValue\": 8080\n      }\n    }\n  },\n  \"request\": {\n    \"time\": \"2024-04-09T07:42:29.634453Z\",\n    \"http\": {\n      \"id\": \"14694995155993896575\",\n      \"method\": \"GET\",\n      \"headers\": {\n        \":authority\": \"testapp.demo.svc.cluster.local:8080\",\n        \":method\": \"GET\",\n        \":path\": \"/book\",\n        \":scheme\": \"http\",\n        \"authorization\": \"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk\",\n        \"user-agent\": \"Wget\",\n        \"x-forwarded-proto\": \"http\",\n        \"x-request-id\": \"27cd2724-e0f4-4a69-a1b1-9a94edfa31bb\"\n      },\n      \"path\": \"/book\",\n      \"host\": \"echo.demo.svc.cluster.local:8080\",\n      \"scheme\": \"http\",\n      \"protocol\": \"HTTP/1.1\"\n    }\n  },\n  \"metadataContext\": {},\n  \"routeMetadataContext\": {}\n}\n

With the help of assertion tree, we can write policies that can be used to validate the request content.

An assert declaration contains an any or all list in which each entry contains a check and a message. The check contains a JMESPath expression that is evaluated against the request content. The message is a string that is returned when the check fails. A check can contain one or more JMESPath expressions. Expressions represent projections of seleted data in the JSON payload and the result of this projection is passed to descendants for futher analysis. All comparisons happen in the leaves of the assertion tree.

For more detail checkout Policy Structure and Assertion trees.

  • HTTP method request.http.method
  • Request path request.http.path
  • Authorization header request.http.headers.authorization

when we decode this above mentioned JWT token in the request payload we get payload.role guest:

{\n  \"exp\": 2241081539,\n  \"nbf\": 1514851139,\n  \"role\": \"guest\",\n  \"sub\": \"YWxpY2U=\"\n}\n
With the input value above, the answer is:
true\n

"},{"location":"policies/policies/","title":"Policy Structure","text":"

Kyverno policies are Kubernetes resources and can be easily managed via Kubernetes APIs, GitOps workflows, and other existing tools.

Policies that apply to JSON payload have a few differences from Kyverno policies that are applied to Kubernetes resources at admission controls.

"},{"location":"policies/policies/#resource-scope","title":"Resource Scope","text":"

Policies that apply to JSON payloads are always cluster-wide resources.

"},{"location":"policies/policies/#api-group-and-kind","title":"API Group and Kind","text":"

kyverno-json policies belong to the json.kyverno.io group and can only be of kind ValidatingPolicy.

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test\nspec:\n  rules:\n    - name: foo-bar-4\n      validate:\n        assert:\n          all:\n          - foo:\n              bar: 4\n
"},{"location":"policies/policies/#policy-rules","title":"Policy Rules","text":"

A policy can have multiple rules, and rules are processed in order. Evaluation stops at the first rule that fails.

"},{"location":"policies/policies/#match-and-exclude","title":"Match and Exclude","text":"

Policies that apply to JSON payloads use assertion trees in both the match/exclude declarations as well as the validate rule declaration.

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: required-s3-tags\nspec:\n  rules:\n    - name: require-team-tag\n      identifier: address\n      match:\n        any:\n        - type: aws_s3_bucket\n      exclude:\n        any:\n        - name: bypass-me\n      validate:\n        assert:\n          all:\n          - values:\n              tags:\n                Team: ?*\n

In the example above, every resource having type: aws_s3_bucket will match, and payloads having name: bypass-me will be excluded.

"},{"location":"policies/policies/#identifying-payload-entries","title":"Identifying Payload Entries","text":"

A policy rule can contain an optional identifier which declares the path to the payload element that uniquely identifies each entry.

"},{"location":"policies/policies/#context-entries","title":"Context Entries","text":"

A policy rule can contain optional context entries that are made available to the rule via bindings:

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: required-s3-tags\nspec:\n  rules:\n    - name: require-team-tag\n      match:\n        any:\n        - type: aws_s3_bucket\n      context:\n      # creates a `expectedTeam` binding automatically\n      - name: expectedTeam\n        variable: Kyverno\n      validate:\n        message: Bucket `{{ name }}` does not have the required Team tag {{ $expectedTeam }}\n        assert:\n          all:\n          - values:\n              tags:\n                # use the `$expectedTeam` binding coming from the context\n                Team: ($expectedTeam)\n
"},{"location":"policies/policies/#no-foreach-pattern-operators-anchors-or-wildcards","title":"No forEach, pattern operators, anchors, or wildcards","text":"

The use of assertion trees addresses some features of Kyverno policies that apply to Kubernetes resources.

Specifically, forEach, pattern operators, anchors, or wildcards are not supported for policies that apply to JSON resources. Instead, assertion trees with JMESPath expressions are used to achieve the same powerful features.

"},{"location":"reference/","title":"Reference documentation","text":"

Info

Select an item in the navigation menu to browse a specific page.

"},{"location":"reference/json-schemas/","title":"JSON schemas","text":"

JSON schemas for the Kyverno Envoy Plugin are available:

  • AuthorizationPolicy (v1alpha1)

They can be used to enable validation and autocompletion in your IDE.

"},{"location":"reference/json-schemas/#vs-code","title":"VS code","text":"

In VS code, simply add a comment on top of your YAML resources.

"},{"location":"reference/json-schemas/#authorizationpolicy","title":"AuthorizationPolicy","text":"
# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/kyverno-envoy-plugin/main/.schemas/json/authorizationpolicy-envoy-v1alpha1.json\napiVersion: envoy.kyverno.io/v1alpha1\nkind: AuthorizationPolicy\nmetadata:\n  name: demo-policy.example.com\nspec:\n  variables:\n  - name: force_authorized\n    expression: object.attributes.request.http.headers[?\"x-force-authorized\"].orValue(\"\") in [\"enabled\", \"true\"]\n  - name: force_unauthenticated\n    expression: object.attributes.request.http.headers[?\"x-force-unauthenticated\"].orValue(\"\") in [\"enabled\", \"true\"]\n  authorizations:\n  - expression: >\n      variables.force_authorized && !variables.force_unauthenticated\n      ? envoy\n          .Allowed()\n          .WithHeader(\"x-validated-by\", \"my-security-checkpoint\")\n          .WithoutHeader(\"x-force-authorized\")\n          .WithResponseHeader(\"x-add-custom-response-header\", \"added\")\n          .Response()\n          .WithMetadata({\"my-new-metadata\": \"my-new-value\"})\n      : envoy\n          .Denied(variables.force_unauthenticated ? 401 : 403)\n          .WithBody(variables.force_unauthenticated ? \"Authentication Failed\" : \"Unauthorized Request\")\n          .Response()\n
"},{"location":"reference/apis/policy.v1alpha1/","title":"policy (v1alpha1)","text":""},{"location":"reference/apis/policy.v1alpha1/#resource-types","title":"Resource Types","text":"
  • AuthorizationPolicy
"},{"location":"reference/apis/policy.v1alpha1/#envoy-kyverno-io-v1alpha1-AuthorizationPolicy","title":"AuthorizationPolicy","text":"

AuthorizationPolicy defines an authorization policy resource

Field Type Required Inline Description apiVersion string envoy.kyverno.io/v1alpha1 kind string AuthorizationPolicy metadata meta/v1.ObjectMeta No description provided. spec AuthorizationPolicySpec No description provided."},{"location":"reference/apis/policy.v1alpha1/#envoy-kyverno-io-v1alpha1-Authorization","title":"Authorization","text":"

Appears in:

  • AuthorizationPolicySpec

Authorization defines an authorization policy rule

Field Type Required Inline Description expression string

Expression represents the expression which will be evaluated by CEL. ref: https://github.com/google/cel-spec CEL expressions have access to CEL variables as well as some other useful variables: - 'object' - The object from the incoming request. (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkrequest) CEL expressions are expected to return an envoy CheckResponse (https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkresponse).

"},{"location":"reference/apis/policy.v1alpha1/#envoy-kyverno-io-v1alpha1-AuthorizationPolicySpec","title":"AuthorizationPolicySpec","text":"

Appears in:

  • AuthorizationPolicy

AuthorizationPolicySpec defines the spec of an authorization policy

Field Type Required Inline Description failurePolicy admissionregistration/v1.FailurePolicyType

FailurePolicy defines how to handle failures for the policy. Failures can occur from CEL expression parse errors, type check errors, runtime errors and invalid or mis-configured policy definitions. FailurePolicy does not define how validations that evaluate to false are handled. Allowed values are Ignore or Fail. Defaults to Fail.

variables []admissionregistration/v1.Variable

Variables contain definitions of variables that can be used in composition of other expressions. Each variable is defined as a named CEL expression. The variables defined here will be available under variables in other expressions of the policy except MatchConditions because MatchConditions are evaluated before the rest of the policy. The expression of a variable can refer to other variables defined earlier in the list but not those after. Thus, Variables must be sorted by the order of first appearance and acyclic.

authorizations []Authorization

Authorizations contain CEL expressions which is used to apply the authorization.

"},{"location":"tutorials/istio/","title":"Istio","text":"

Istio is an open source service mesh for managing the different microservices that make up a cloud-native application. Istio provides a mechanism to use a service as an external authorizer with the AuthorizationPolicy API.

This tutorial shows how Istio\u2019s AuthorizationPolicy can be configured to delegate authorization decisions to Kyverno-envoy-plugin.

"},{"location":"tutorials/istio/#prerequisites","title":"Prerequisites","text":"

This tutorial requires Kubernetes 1.20 or later. To run the tutorial locally ensure you start a cluster with Kubernetes version 1.20+, we recommend using minikube or KIND.

The tutorial also requries istio v1.19.0 or later. To install istio, follow the instructions here or run the below script it will create a kind cluster and install istio

#!/bin/bash\n\nKIND_IMAGE=kindest/node:v1.29.2\nISTIO_REPO=https://istio-release.storage.googleapis.com/charts\nISTIO_NS=istio-system\n\n# Create Kind cluster\nkind create cluster --image $KIND_IMAGE --wait 1m --config - <<EOF\nkind: Cluster\napiVersion: kind.x-k8s.io/v1alpha4\nnodes:\n  - role: control-plane\n    kubeadmConfigPatches:\n      - |-\n        kind: InitConfiguration\n        nodeRegistration:\n          kubeletExtraArgs:\n            node-labels: \"ingress-ready=true\"\n    extraPortMappings:\n      - containerPort: 80\n        hostPort: 80\n        protocol: TCP\n      - containerPort: 443\n        hostPort: 443\n        protocol: TCP\n  - role: worker\nEOF\n\n# Install Istio components\nhelm upgrade --install istio-base       --namespace $ISTIO_NS           --create-namespace --wait --repo $ISTIO_REPO base\nhelm upgrade --install istiod           --namespace $ISTIO_NS           --create-namespace --wait --repo $ISTIO_REPO istiod\n
The tutorial requires admission controller in the kyverno-envoy-sidecar-injector namespace that automatically injects the kyverno-envoy-plugin sidecar into pods in namespaces labelled with kyverno-envoy-sidecar/injection=enabled. To install the sidecar-injector admission controller then checkout the installation guide.

"},{"location":"tutorials/istio/#creating-a-simple-authorization-policy","title":"Creating a simple authorization policy","text":"

This tutorial assumes you have some basic knowledge of validatingPolicy and assertion trees. In summary the policy below does the following:

  • Checks that the JWT token is valid
  • Checks that the action is allowed based on the token payload role and the request path
  • Guests have read-only access to the /book endpoint, admins can create users too as long as the name is not the same as the admin's name.
apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n    name: checkrequest\nspec:\n    rules:\n    - name: deny-guest-request-at-post\n        assert:\n        any:\n        - message: \"POST method calls at path /book are not allowed to guests users\"\n            check:\n            request:\n                http:\n                    method: POST\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book                             \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): guest\n                    path: /book \n
"},{"location":"tutorials/istio/#deploying-the-application","title":"Deploying the application","text":"

Create a namespace called demo and label it with istio-injection=enabled to enable sidecar injection:

$ kubectl apply -f - <<EOF\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: demo\n  labels:\n    istio-injection: enabled\nEOF\n

First we need to apply kyverno policy configmap this policy will be passed to kyverno-envoy-sidecar injector admission controller:

$ kubectl apply -f - <<EOF\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: policy-files\n  namespace: demo\ndata:\n  policy.yaml: |\n    apiVersion: json.kyverno.io/v1alpha1\n    kind: ValidatingPolicy\n    metadata:\n      name: checkrequest\n    spec:\n      rules:\n        - name: deny-guest-request-at-post\n          assert:\n            any:\n            - message: \"POST method calls at path /book are not allowed to guests users\"\n              check:\n                request:\n                    http:\n                        method: POST\n                        headers:\n                            authorization:\n                                (split(@, ' ')[1]):\n                                    (jwt_decode(@ , 'secret').payload.role): admin\n                        path: /book                             \n            - message: \"GET method call is allowed to both guest and admin users\"\n              check:\n                request:\n                    http:\n                        method: GET\n                        headers:\n                            authorization:\n                                (split(@, ' ')[1]):\n                                    (jwt_decode(@ , 'secret').payload.role): admin\n                        path: /book \n            - message: \"GET method call is allowed to both guest and admin users\"\n              check:\n                request:\n                    http:\n                        method: GET\n                        headers:\n                            authorization:\n                                (split(@, ' ')[1]):\n                                    (jwt_decode(@ , 'secret').payload.role): guest\n                        path: /book               \nEOF                   \n

Deploy the sample application which provides information about books in a collection and exposes APIs to get, create and delete Book resources at /book endpoint and make it accessible in the cluster, and enable sidecar injection of the kyverno-envoy-plugin sidecar by adding the kyverno-envoy-sidecar/injection: enabled label to the deployment:

$ kubectl apply -f - <<EOF\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: testapp\n  namespace: demo\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: testapp\n  template:\n    metadata:\n      labels:\n        kyverno-envoy-sidecar/injection: enabled\n        app: testapp\n    spec:\n      containers:\n      - name: testapp\n        image: sanskardevops/test-application:0.0.1\n        ports:\n        - containerPort: 8080\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: testapp\n  namespace: demo\nspec:\n  type: ClusterIP\n  selector:\n    app: testapp\n  ports:\n  - port: 8080\n    targetPort: 8080\nEOF\n
Check that their should be three containers should be running in the pod.

$ kubectl -n demo  get all\nNAME                        READY   STATUS    RESTARTS   AGE\npod/echo-55c77757f4-w6979   3/3     Running   0          3h59m\n\nNAME           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE\nservice/echo   ClusterIP   10.96.110.173   <none>        8080/TCP   4h5m\n\nNAME                   READY   UP-TO-DATE   AVAILABLE   AGE\ndeployment.apps/echo   1/1     1            1           3h59m\n\nNAME                              DESIRED   CURRENT   READY   AGE\nreplicaset.apps/echo-55c77757f4   1         1         1       3h59m\n
"},{"location":"tutorials/istio/#serviceentry","title":"ServiceEntry","text":"

ServiceEntry to registor the kyverno-envoy-plugin sidecar as external authorizer and ServiceEntry to allow Istio to find the Kyverno-Envoy-Plugin sidecar.

kubectl apply -f ./manifests/service-entry.yaml\n
apiVersion: networking.istio.io/v1beta1\nkind: ServiceEntry\nmetadata:\n  name: kyverno-ext-authz-grpc-local\nspec:\n  hosts:\n  - \"kyverno-ext-authz-grpc.local\"\n  # The service name to be used in the extension provider in the mesh config.\n  endpoints:\n  - address: \"127.0.0.1\"\n  ports:\n  - name: grpc\n    number: 9000\n    # The port number to be used in the extension provider in the mesh config.\n    protocol: GRPC\n  resolution: STATIC\n

"},{"location":"tutorials/istio/#register-authorization-provider","title":"Register authorization provider","text":"

Edit the mesh configmap to register authorization provider with the following command:

kubectl edit configmap istio -n istio-system \n

In the editor, add the extension provider definitions to the mesh configmap.

  data:\n    mesh: |-   \n      extensionProviders:\n      - name: \"kyverno-ext-authz-grpc\"\n        envoyExtAuthzGrpc:\n          service: \"kyverno-ext-authz-grpc.local\"\n          port: \"9000\"\n
"},{"location":"tutorials/istio/#authorization-policy","title":"Authorization policy","text":"

AuthorizationPolicy to direct authorization checks to the Kyverno-Envoy-Plugin sidecar.

$ kubectl apply -f - <<EOF\napiVersion: security.istio.io/v1\nkind: AuthorizationPolicy\nmetadata:\n  name: kyverno-ext-authz-grpc\n  namespace: demo\nspec:\n  action: CUSTOM\n  provider:\n    # The provider name must match the extension provider defined in the mesh config.\n    name: kyverno-ext-authz-grpc\n  rules:\n  # The rules specify when to trigger the external authorizer.\n  - to:\n    # Allowed all path except /healthz\n    - operation:\n        notPaths: [\"/healthz\"]\nEOF \n
This policy configures an external service for authorization. Note that the service is not specified directly in the policy but using a provider.name field.

"},{"location":"tutorials/istio/#verify-the-authorization","title":"Verify the authorization","text":"

For convenience, we\u2019ll want to store Alice\u2019s and Bob\u2019s tokens in environment variables. Here bob is assigned the admin role and alice is assigned the guest role.

export ALICE_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk\"\nexport BOB_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6ImFkbWluIiwic3ViIjoiWVd4cFkyVT0ifQ.veMeVDYlulTdieeX-jxFZ_tCmqQ_K8rwx2OktUHv5Z0\"\n

Check for Alice which can get book but cannot create book.

kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$ALICE_TOKEN\"\" --output-document - testapp.demo.svc.cluster.local:8080/book\n

kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$ALICE_TOKEN\"\" --post-data='{\"bookname\":\"Harry Potter\", \"author\":\"J.K. Rowling\"}' --output-document - testapp.demo.svc.cluster.local:8080/book\n
Check the Bob which can get book also create the book

kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$BOB_TOKEN\"\" --output-document - testapp.demo.svc.cluster.local:8080/book\n
kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$BOB_TOKEN\"\" --post-data='{\"bookname\":\"Harry Potter\", \"author\":\"J.K. Rowling\"}' --output-document - testapp.demo.svc.cluster.local:8080/book\n

Check on kyverno-envoy-plugin container logs

kubectl logs \"$(kubectl get pod -l app=testapp -n demo -o jsonpath={.items..metadata.name})\" -n demo -c ext-authz -f\n

"},{"location":"tutorials/istio/#wrap-up","title":"Wrap Up","text":"

Congratulations on completing the tutorial!

This tutorial demonstrated how to configure Istio\u2019s EnvoyFilter to utilize the kyverno-envoy-plugin as an external authorization service.

Additionally, the tutorial provided an example policy using the kyverno-envoy-plugin that returns a boolean decision to determine whether a request should be permitted.

Further details about the tutorial can be found here.

"},{"location":"tutorials/mtls-istio/","title":"Istio mTLS","text":"

Istio is an open source service mesh for managing the different microservices that make up a cloud-native application. Istio provides a mechanism to use a service as an external authorizer with the AuthorizationPolicy API.

The kyverno-envoy-plugin is a custom Envoy filter that is used to intercept the incoming request to the service and validate the request using the kyverno engine.

In this tutorial we will create a two simple microservices which are going to make external authorization to a single kyverno-envoy-plugin service as a separate pod in the mesh. With this tutorial we are going to understand how to use multiple microservices to make authorization decisions to a single ext-authz server.

To handle multiple different requests effectively, we leverage the match/exclude declarations to route the specific authz-request to the appropriate validating policy within the Kyverno engine. This approach allows us to execute the right validating policy for each request, enabling efficient and targeted request processing.

"},{"location":"tutorials/mtls-istio/#example-policy","title":"Example Policy","text":"

The following policies will be executed by the kyverno-envoy-plugin to validate incoming requests made specifically to the testapp-1 service. By leveraging the match declarations, we ensure that these policies are executed only when the incoming request is destined for the testapp-1 service. This targeted approach allows us to apply the appropriate validation rules and policies based on the specific service being accessed.

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test-policy\nspec:\n  rules:\n    - name: deny-external-calls-testapp-1\n      match:\n        any:\n        - request:\n            http:\n                host: 'testapp-1.demo.svc.cluster.local:8080'\n      assert:\n        all:\n        - message: \"The GET method is restricted to the /book path.\"\n          check:\n            request:\n                http:\n                    method: 'GET'\n                    path: '/book'\n
To execute the policy when the incoming request is made to testapp-2 service we need to use the match declarations.

apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n  name: test-policy\nspec:\n  rules:\n    - name: deny-external-calls-testapp-2\n      match:\n        any:\n        - request:\n            http:\n                host: 'testapp-2.demo.svc.cluster.local:8080'\n      assert:\n        all:\n        - message: \"The GET method is restricted to the /movies path.\"\n          check:\n            request:\n                http:\n                    method: 'GET'\n                    path: '/movie'   \n
The example json request for above payload will be like below.

{\n  \"source\": {\n    \"address\": {\n      \"socketAddress\": {\n        \"address\": \"10.244.0.71\",\n        \"portValue\": 33880\n      }\n    }\n  },\n  \"destination\": {\n    \"address\": {\n      \"socketAddress\": {\n        \"address\": \"10.244.0.65\",\n        \"portValue\": 8080\n      }\n    }\n  },\n  \"request\": {\n    \"time\": \"2024-05-20T07:52:01.566887Z\",\n    \"http\": {\n      \"id\": \"5415544797791892902\",\n      \"method\": \"GET\",\n      \"headers\": {\n        \":authority\": \"testapp-2.demo.svc.cluster.local:8080\",\n        \":method\": \"GET\",\n        \":path\": \"/movie\",\n        \":scheme\": \"http\",\n        \"user-agent\": \"Wget\",\n        \"x-forwarded-proto\": \"http\",\n        \"x-request-id\": \"a3ad9f03-c9cd-4eab-97d1-83e90e0cee1b\"\n      },\n      \"path\": \"/movie\",\n      \"host\": \"testapp-2.demo.svc.cluster.local:8080\",\n      \"scheme\": \"http\",\n      \"protocol\": \"HTTP/1.1\"\n    }\n  },\n  \"metadataContext\": {},\n  \"routeMetadataContext\": {}\n}\n

To enhance security, we can implement Mutual TLS (mTLS) for peer authentication between test services and kyverno-envoy-plugin. Since we are currently using JSON request data to validate incoming requests, there is a potential risk of this data being tampered with during transit. Implementing mTLS would ensure that communication between services is encrypted and authenticated, mitigating the risk of unauthorized data modification.

apiVersion: security.istio.io/v1beta1\nkind: PeerAuthentication\nmetadata:\n  name: mtls-demo\n  namespace: demo\nspec:\n  mtls:\n    mode: STRICT\n---\napiVersion: security.istio.io/v1beta1\nkind: PeerAuthentication\nmetadata:\n  name: mtls-testapp-1\n  namespace: demo\nspec:\n  selector:\n    matchLabels:\n      app: testapp-1\n  mtls:\n    mode: STRICT\n  portLevelMtls:\n    8080:\n      mode: PERMISSIVE\n---\napiVersion: security.istio.io/v1beta1\nkind: PeerAuthentication\nmetadata:\n  name: mtls-testapp-2\n  namespace: demo\nspec:\n  selector:\n    matchLabels:\n      app: testapp-2\n  mtls:\n    mode: STRICT\n  portLevelMtls:\n    8080:\n      mode: PERMISSIVE\n
"},{"location":"tutorials/mtls-istio/#demo-instructions","title":"Demo instructions","text":""},{"location":"tutorials/mtls-istio/#required-tools","title":"Required tools","text":"
  1. kind
  2. kubectl
  3. helm
"},{"location":"tutorials/mtls-istio/#create-a-local-cluster-and-install-istio","title":"Create a local cluster and install Istio","text":"

The tutorial also requries istio v1.19.0 or later. To install istio, follow the instructions here or run the below script it will create a kind cluster and install istio

#!/bin/bash\n\nKIND_IMAGE=kindest/node:v1.29.2\nISTIO_REPO=https://istio-release.storage.googleapis.com/charts\nISTIO_NS=istio-system\n\n# Create Kind cluster\nkind create cluster --image $KIND_IMAGE --wait 1m --config - <<EOF\nkind: Cluster\napiVersion: kind.x-k8s.io/v1alpha4\nnodes:\n  - role: control-plane\n    kubeadmConfigPatches:\n      - |-\n        kind: InitConfiguration\n        nodeRegistration:\n          kubeletExtraArgs:\n            node-labels: \"ingress-ready=true\"\n    extraPortMappings:\n      - containerPort: 80\n        hostPort: 80\n        protocol: TCP\n      - containerPort: 443\n        hostPort: 443\n        protocol: TCP\n  - role: worker\nEOF\n\n# Install Istio components\nhelm upgrade --install istio-base       --namespace $ISTIO_NS           --create-namespace --wait --repo $ISTIO_REPO base\nhelm upgrade --install istiod           --namespace $ISTIO_NS           --create-namespace --wait --repo $ISTIO_REPO istiod\n
"},{"location":"tutorials/mtls-istio/#sample-applications","title":"Sample applications","text":"

Manifests for the sample applications are available in test-application-1.yaml and test-application-2.yaml. The sample app testapp-1 provides information about books in a collection and exposes APIs to get, create and delete Book resources. The sample app testapp-2 provides information about movies in a collection and exposes APIs to get, create and delete Movie resources.

$ kubectl apply -f - <<EOF\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: demo\n  labels:\n    istio-injection: enabled\nEOF\n
# test-application-1.yaml\n# Deploy sample application testapp-1 \n$ kubectl apply -f - <<EOF\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: testapp-1\n  namespace: demo\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: testapp-1\n  template:\n    metadata:\n      labels:\n        app: testapp-1\n    spec:\n      containers:\n      - name: testapp-1\n        image: sanskardevops/test-application:0.0.1\n        ports:\n        - containerPort: 8080\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: testapp-1\n  namespace: demo\nspec:\n  type: NodePort\n  selector:\n    app: testapp-1\n  ports:\n  - port: 8080\n    targetPort: 8080\nEOF\n
# test-application-2.yaml\n# Deploy sample application testapp-2\n$ kubectl apply -f - <<EOF\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: testapp-2\n  namespace: demo\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: testapp-2\n  template:\n    metadata:\n      labels:\n        app: testapp-2\n    spec:\n      containers:\n      - name: testapp-2\n        image: sanskardevops/test-application-1:0.0.3\n        ports:\n        - containerPort: 8080\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: testapp-2\n  namespace: demo\nspec:\n  type: ClusterIP\n  selector:\n    app: testapp-2\n  ports:\n  - port: 8080\n    targetPort: 8080\nEOF\n
"},{"location":"tutorials/mtls-istio/#calling-the-sample-applications","title":"Calling the sample applications","text":"

We are going to call the sample applications using a pod in the cluster.

$ kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --output-document - testapp-1.demo.svc.cluster.local:8080/book\n
[{\"id\":\"1\",\"bookname\":\"Harry Potter\",\"author\":\"J.K. Rowling\"},{\"id\":\"2\",\"bookname\":\"Animal Farm\",\"author\":\"George Orwell\"}]\npod \"test\" deleted\n
$ kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --output-document - testapp-2.demo.svc.cluster.local:8080/movie\n
[{\"id\":\"1\",\"Moviename\":\"Inception\",\"Actor\":\"Leonardo DiCaprio\"},{\"id\":\"2\",\"Moviename\":\"Batman\",\"Actor\":\"Jack Nicholson\"}]\npod \"test\" deleted\n
"},{"location":"tutorials/mtls-istio/#register-authorization-provider","title":"Register authorization provider","text":"

Edit the mesh configmap to register authorization provider with the following command:

$ kubectl edit configmap istio -n istio-system\n
In the editor, add the extension provider definitions to the mesh configmap.

  data:\n    mesh: |-   \n      extensionProviders:\n      - name: \"kyverno-ext-authz-grpc\"\n        envoyExtAuthzGrpc:\n          service: \"ext-authz.demo.svc.cluster.local\"\n          port: \"9000\"\n
"},{"location":"tutorials/mtls-istio/#authorization-policy","title":"Authorization policy","text":"

Now we can deploy an istio AuthorizationPolicy: AuthorizationPolicy to tell Istio to use kyverno-envoy-plugin as the Authz Server

$ kubectl apply -f - <<EOF\napiVersion: security.istio.io/v1\nkind: AuthorizationPolicy\nmetadata:\n  name: kyverno-ext-authz-grpc\n  namespace: demo\nspec:\n  action: CUSTOM\n  provider:\n    # The provider name must match the extension provider defined in the mesh config.\n    name: kyverno-ext-authz-grpc\n  rules:\n  # The rules specify when to trigger the external authorizer.\n  - to:\n    - operation:\n        paths: [\"/book\",\"/movie\"]\nEOF        \n

This policy configures an external service for authorization. Note that the service is not specified directly in the policy but using a provider.name field. The rules specify that requests to paths /book and /movies.

"},{"location":"tutorials/mtls-istio/#authorization-service-deployment","title":"Authorization service deployment","text":"

The deployment manifest of the authorization service is available in ext-auth-server.yaml. This deployment require policy through configmap .

Apply the policy configmap with the following command.

$ kubectl apply -f - <<EOF\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: policy-files\n  namespace: demo\ndata:\n  policy1.yaml: |\n    apiVersion: json.kyverno.io/v1alpha1\n    kind: ValidatingPolicy\n    metadata:\n      name: test-policy\n    spec:\n      rules:\n        - name: deny-external-calls-testapp-1\n          match:\n            any:\n            - request:\n                http:\n                    host: 'testapp-1.demo.svc.cluster.local:8080'\n          assert:\n            all:\n            - message: \"The GET method is restricted to the /book path.\"\n              check:\n                request:\n                    http:\n                        method: 'GET'\n                        path: '/book'\n  policy2.yaml: |\n    apiVersion: json.kyverno.io/v1alpha1\n    kind: ValidatingPolicy\n    metadata:\n      name: test-policy\n    spec:\n      rules:\n        - name: deny-external-calls-testapp-2\n          match:\n            any:\n            - request:\n                http:\n                    host: 'testapp-2.demo.svc.cluster.local:8080'\n          assert:\n            all:\n            - message: \"The GET method is restricted to the /movies path.\"\n              check:\n                request:\n                    http:\n                        method: 'GET'\n                        path: '/movie'                \nEOF                        \n
# ext-auth-server.yaml\n# Deploy the kyverno external authorizer server\n$ kubectl apply -f - <<EOF\napiVersion: v1\nkind: Service\nmetadata:\n  name: ext-authz\n  labels:\n    app: ext-authz\n  namespace: demo  \nspec:\n  ports:\n  - name: http\n    port: 8000\n    targetPort: 8000\n  - name: grpc\n    port: 9000\n    targetPort: 9000\n  selector:\n    app: ext-authz\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: ext-authz\n  namespace: demo \nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: ext-authz \n  template:\n    metadata:\n      labels:\n        app: ext-authz\n    spec:\n      containers:\n      - image: sanskardevops/plugin:0.0.29\n        imagePullPolicy: IfNotPresent\n        name: ext-authz\n        ports:\n        - containerPort: 8000\n        - containerPort: 9000\n        args:\n        - \"serve\"\n        - \"--policy=/policies/policy1.yaml\"\n        - \"--policy=/policies/policy2.yaml\"\n        volumeMounts:\n        - name: policy-files\n          mountPath: /policies\n      volumes:\n      - name: policy-files\n        configMap:\n          name: policy-files\nEOF\n

Verify the sample external authorizer is up and running:

$ kubectl logs \"$(kubectl get pod -l app=ext-authz -n demo -o jsonpath={.items..metadata.name})\" -n demo -c ext-authz -f\n

Starting GRPC server on Port 9000\nStarting HTTP server on Port 8000\n
"},{"location":"tutorials/mtls-istio/#apply-peerauthentication-policy","title":"Apply PeerAuthentication Policy","text":"

Apply the PeerAuthentication policy to enable mTLS for the sample applications and external authorizer.

$ kubectl apply -f - <<EOF\napiVersion: security.istio.io/v1beta1\nkind: PeerAuthentication\nmetadata:\n  name: mtls-demo\n  namespace: demo\nspec:\n  mtls:\n    mode: STRICT\n---\napiVersion: security.istio.io/v1beta1\nkind: PeerAuthentication\nmetadata:\n  name: mtls-testapp-1\n  namespace: demo\nspec:\n  selector:\n    matchLabels:\n      app: testapp-1\n  mtls:\n    mode: STRICT\n  portLevelMtls:\n    8080:\n      mode: PERMISSIVE\n---\napiVersion: security.istio.io/v1beta1\nkind: PeerAuthentication\nmetadata:\n  name: mtls-testapp-2\n  namespace: demo\nspec:\n  selector:\n    matchLabels:\n      app: testapp-2\n  mtls:\n    mode: STRICT\n  portLevelMtls:\n    8080:\n      mode: PERMISSIVE\nEOF\n
"},{"location":"tutorials/mtls-istio/#test-the-sample-applications","title":"Test the sample applications","text":"

Check on the logs of the sample applications to see that the requests are accepted and rejected

Check on GET request on testapp-1 which is allowed according to policy deny-external-calls-testapp-1

$ kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --output-document - testapp-1.demo.svc.cluster.local:8080/book\n
[{\"id\":\"1\",\"bookname\":\"Harry Potter\",\"author\":\"J.K. Rowling\"},{\"id\":\"2\",\"bookname\":\"Animal Farm\",\"author\":\"George Orwell\"}]\npod \"test\" deleted\n

Check on GET request on testapp-2 which is allowed according to policy deny-external-calls-testapp-2

$ kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --output-document - testapp-2.demo.svc.cluster.local:8080/movie\n
[{\"id\":\"1\",\"Moviename\":\"Inception\",\"Actor\":\"Leonardo DiCaprio\"},{\"id\":\"2\",\"Moviename\":\"Batman\",\"Actor\":\"Jack Nicholson\"}]\npod \"test\" deleted\n

Check logs of external authorizer to see that the requests are which policy was executed for a perticular request .

$ kubectl logs \"$(kubectl get pod -l app=ext-authz -n demo -o jsonpath={.items..metadata.name})\" -n demo -c ext-authz -f\n

Starting GRPC server on Port 9000\nStarting HTTP server on Port 8000\n2024/05/21 07:41:33 Request is initialized in kyvernojson engine .\n2024/05/21 07:41:33 Request passed the deny-external-calls-testapp-1 policy rule.\n2024/05/21 07:42:22 Request is initialized in kyvernojson engine .\n2024/05/21 07:42:22 Request passed the deny-external-calls-testapp-2 policy rule.\n
First request was directed to testapp-1 which was allowed by the policy deny-external-calls-testapp-1 and the second request was directed to testapp-2 which was allowed by the policy deny-external-calls-testapp-2.

"},{"location":"tutorials/mtls-istio/#wrap-up","title":"Wrap Up","text":"

Congratulations on completing the tutorial!

This tutorial demonstrated how to configure Istio's AuthorizationPolicy to utilize the kyverno-envoy-plugin as an separate pod external authorization service. By leveraging the power of Kyverno's policy engine, you can enforce fine-grained authorization rules across your microservices within the Istio service mesh.

Additionally, the tutorial showcased the use of mTLS (Mutual TLS) to secure communication between services and the kyverno-envoy-plugin, ensuring end-to-end encryption and authentication.

The combination of Istio's AuthorizationPolicy and the kyverno-envoy-plugin provides a flexible and powerful solution for implementing custom authorization logic in your cloud-native applications. By following this tutorial, you've gained hands-on experience in configuring and deploying this solution, setting the stage for further exploration and customization to meet your specific requirements.

We hope this tutorial has been informative and has provided you with a solid foundation for integrating the kyverno-envoy-plugin into your Istio service mesh environment. Feel free to explore the project's documentation and community resources for further assistance and to stay updated with the latest developments.

"},{"location":"tutorials/standalone-envoy/","title":"Standalone Envoy","text":"

The tutorial shows how Envoy's External Authorization filter can be used with Kyverno as an authorization service to enforce security policies over API requests received by Envoy.

"},{"location":"tutorials/standalone-envoy/#overview","title":"Overview","text":"

In this tutorial we'll see how to use Kyverno-envoy-plugin as an External Authorization service for the Envoy proxy. The goal of the demo to show user how kyverno-envoy-plugin will work with standalone envoy and how it can be used to enforce policies to the traffic between services. The Kyverno-envoy-plugin allows configuring these Envoy proxies to query Kyverno-json for policy decisions on incoming requests. The kyverno-envoy-plugin is cofigured as a static binary and can be run as a sidecar container in the same pod as the application.

We'll do this by:

  • Running a local Kubernetes cluster
  • Creating a simple authorization policy in ValidatingPolicy
  • Deploying a sample application with Envoy and kyverno-envoy-plugin sidecars
  • Run some sample requests to see the policy in action

Note that other than the HTTP client and bundle server, all components are co-located in the same pod.

"},{"location":"tutorials/standalone-envoy/#demo-instructions","title":"Demo instructions","text":""},{"location":"tutorials/standalone-envoy/#required-tools","title":"Required tools","text":"
  1. kind
  2. kubectl

{{< info >}} If you haven't used kind before, you can find installation instructions in the project documentation. {{</ info >}}

"},{"location":"tutorials/standalone-envoy/#running-a-local-kubernetes-cluster","title":"Running a local Kubernetes cluster","text":"

To start a local kubernetes cluster to run our demo, we'll be using kind. In order to use the kind command, you\u2019ll need to have Docker installed on your machine.

Create a cluster with the following command:

$ kind create cluster --name kyverno-tutorial --image kindest/node:v1.29.2\nCreating cluster \"kyverno-tutorial\" ...\n \u2713 Ensuring node image (kindest/node:v1.29.2) \ud83d\uddbc\n \u2713 Preparing nodes \ud83d\udce6  \n \u2713 Writing configuration \ud83d\udcdc \n \u2713 Starting control-plane \ud83d\udd79\ufe0f \n \u2713 Installing CNI \ud83d\udd0c \n \u2713 Installing StorageClass \ud83d\udcbe \nSet kubectl context to \"kind-kyverno-tutorial\"\nYou can now use your cluster with:\n\nkubectl cluster-info --context kind-kyverno-tutorial\n\nThanks for using kind! \ud83d\ude0a\n

Listing the cluster nodes, should show something like this:

$ kubectl get nodes\nNAME                             STATUS   ROLES           AGE   VERSION\nkyverno-tutorial-control-plane   Ready    control-plane   79s   v1.29.2\n
"},{"location":"tutorials/standalone-envoy/#creating-a-simple-authorization-policy","title":"Creating a simple authorization policy","text":"

This tutorial assumes you have some basic knowledge of validatingPolicy and assertion trees. In summary the policy below does the following:

  • Checks that the JWT token is valid
  • Checks that the action is allowed based on the token payload role and the request path
  • Guests have read-only access to the /book endpoint, admins can create users too as long as the name is not the same as the admin's name.
apiVersion: json.kyverno.io/v1alpha1\nkind: ValidatingPolicy\nmetadata:\n    name: checkrequest\nspec:\n    rules:\n    - name: deny-guest-request-at-post\n        assert:\n        any:\n        - message: \"POST method calls at path /book are not allowed to guests users\"\n            check:\n            request:\n                http:\n                    method: POST\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book                             \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): admin\n                    path: /book \n        - message: \"GET method call is allowed to both guest and admin users\"\n            check:\n            request:\n                http:\n                    method: GET\n                    headers:\n                        authorization:\n                            (split(@, ' ')[1]):\n                                (jwt_decode(@ , 'secret').payload.role): guest\n                    path: /book \n

Create a file called policy.yaml with the above content and store it in a configMap:

$ kubectl create configmap policy --from-file=policy.yaml\n
"},{"location":"tutorials/standalone-envoy/#deploying-an-application-with-envoy-and-kyverno-envoy-plugin-sidecars","title":"Deploying an application with Envoy and Kyverno-Envoy-Plugin sidecars","text":"

In this tutorial, we are manually configuring the Envoy proxy sidecar to intermediate HTTP traffic from clients and our application. Envoy will consult Kyverno-Envoy-Plugin to make authorization decisions for each request by sending CheckRequest gRPC messages over a gRPC connection.

We will use the following Envoy configuration to achieve this. In summary, this configures Envoy to:

  • Listen on Port 7000 for HTTP traffic
  • Consult Kyverno-Envoy-Plugin at 127.0.0.1:9000 for authorization decisions and deny failing requests
  • Forward request to the application at 127.0.0.1:8080 if ok.
    static_resources:\n      listeners:\n      - address:\n          socket_address:\n            address: 0.0.0.0\n            port_value: 7000\n        filter_chains:\n        - filters:\n          - name: envoy.filters.network.http_connection_manager\n            typed_config:\n              \"@type\": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n              codec_type: auto\n              stat_prefix: ingress_http\n              route_config:\n                name: local_route\n                virtual_hosts:\n                - name: backend\n                  domains:\n                  - \"*\"\n                  routes:\n                  - match:\n                      prefix: \"/\"\n                    route:\n                      cluster: service\n              http_filters:\n              - name: envoy.ext_authz\n                typed_config:\n                  \"@type\": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz\n                  transport_api_version: V3\n                  with_request_body:\n                    max_request_bytes: 8192\n                    allow_partial_message: true\n                  failure_mode_allow: false\n                  grpc_service:\n                    google_grpc:\n                      target_uri: 127.0.0.1:9000\n                      stat_prefix: ext_authz\n                    timeout: 0.5s\n              - name: envoy.filters.http.router\n                typed_config:\n                  \"@type\": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n      clusters:\n      - name: service\n        connect_timeout: 0.25s\n        type: strict_dns\n        lb_policy: round_robin\n        load_assignment:\n          cluster_name: service\n          endpoints:\n          - lb_endpoints:\n            - endpoint:\n                address:\n                  socket_address:\n                    address: 127.0.0.1\n                    port_value: 8080\n    admin:\n      access_log_path: \"/dev/null\"\n      address:\n        socket_address:\n          address: 0.0.0.0\n          port_value: 8001\n    layered_runtime:\n      layers:\n        - name: static_layer_0\n          static_layer:\n            envoy:\n              resource_limits:\n                listener:\n                  example_listener_name:\n                    connection_limit: 10000\n            overload:\n              global_downstream_max_connections: 50000\n

Create a ConfigMap containing the above configuration by running:

$ kubectl create configmap proxy-config --from-file envoy.yaml \n
Our application will be configured using a Deployment and Service. There are few things to note:

  • The pods have an initContainer that configures the iptables rules to redirect traffic to the Envoy Proxy sidecar.
  • The test-application container is simple go application stores book information in-memory state.
  • The envoy container is configured to use proxy-config ConfigMap as the Envoy configuration we created earlier
  • The kyverno-envoy-plugin container is configured to use policy ConfigMap as the Kyverno policy we created earlier
# test-application.yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: testapp\n  namespace: demo\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: testapp\n  template:\n    metadata:\n      labels:\n        app: testapp\n    spec:\n      initContainers:\n        - name: proxy-init\n          image: sanskardevops/proxyinit:latest\n          # Configure the iptables bootstrap script to redirect traffic to the\n          # Envoy proxy on port 8000, specify that Envoy will be running as user\n          # 1111, and that we want to exclude port 8181 from the proxy for the Kyverno health checks.\n          # These values must match up with the configuration\n          # defined below for the \"envoy\" and \"kyverno-envoy-plugin\" containers.\n          args: [\"-p\", \"7000\", \"-u\", \"1111\", -w, \"8181\"]\n          securityContext:\n            capabilities:\n              add:\n                - NET_ADMIN\n            runAsNonRoot: false\n            runAsUser: 0\n      containers:\n        - name: test-application\n          image: sanskardevops/test-application:0.0.1\n          ports:\n            - containerPort: 8080\n        - name: envoy\n          image: envoyproxy/envoy:v1.30-latest\n          securityContext:\n            runAsUser: 1111\n          imagePullPolicy: IfNotPresent\n          volumeMounts:\n            - readOnly: true\n              mountPath: /config\n              name: proxy-config\n          args:\n            - \"envoy\"\n            - \"--config-path\"\n            - \"/config/envoy.yaml\"\n        - name: kyverno-envoy-plugin\n          image: sanskardevops/plugin:0.0.34\n          imagePullPolicy: IfNotPresent\n          ports:\n            - containerPort: 8181\n            - containerPort: 9000\n          volumeMounts:\n            - readOnly: true\n              mountPath: /policies\n              name: policy-files\n          args:\n            - \"serve\"\n            - \"--policy=/policies/policy.yaml\"\n            - \"--address=:9000\"\n            - \"--healthaddress=:8181\"\n          livenessProbe:\n            httpGet:\n              path: /health\n              scheme: HTTP\n              port: 8181\n            initialDelaySeconds: 5\n            periodSeconds: 5\n          readinessProbe:\n            httpGet:\n              path: /health\n              scheme: HTTP\n              port: 8181\n            initialDelaySeconds: 5\n            periodSeconds: 5  \n      volumes:\n        - name: proxy-config\n          configMap:\n            name: proxy-config\n        - name: policy-files\n          configMap:\n            name: policy-files\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: testapp\n  namespace: demo\nspec:\n  type: ClusterIP\n  selector:\n    app: testapp\n  ports:\n  - port: 8080\n    targetPort: 8080      \n

Deploy the application and Kubernetes Service to the cluster with:

$ kubectl apply -f test-application.yaml\n
Check that everything is working by listing the pod and make sure all three pods are running ok.

$ kubectl get pods\nNAME                         READY   STATUS    RESTARTS   AGE\ntestapp-74b4bc88-5d4wh       3/3     Running   0          1m\n
"},{"location":"tutorials/standalone-envoy/#policy-in-action","title":"Policy in action","text":"

For convenience, we\u2019ll want to store Alice\u2019s and Bob\u2019s tokens in environment variables. Here bob is assigned the admin role and alice is assigned the guest role.

export ALICE_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk\"\nexport BOB_TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6ImFkbWluIiwic3ViIjoiWVd4cFkyVT0ifQ.veMeVDYlulTdieeX-jxFZ_tCmqQ_K8rwx2OktUHv5Z0\"\n

Check for Alice which can get book but cannot create book.

kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$ALICE_TOKEN\"\" --output-document - testapp.demo.svc.cluster.local:8080/book\n
kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$ALICE_TOKEN\"\" --post-data='{\"bookname\":\"Harry Potter\", \"author\":\"J.K. Rowling\"}' --output-document - testapp.demo.svc.cluster.local:8080/book\n
Check the Bob which can get book also create the book

kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$BOB_TOKEN\"\" --output-document - testapp.demo.svc.cluster.local:8080/book\n
kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header=\"authorization: Bearer \"$BOB_TOKEN\"\" --post-data='{\"bookname\":\"Harry Potter\", \"author\":\"J.K. Rowling\"}' --output-document - testapp.demo.svc.cluster.local:8080/book\n

Check on logs

kubectl logs \"$(kubectl get pod -l app=testapp -n demo -o jsonpath={.items..metadata.name})\" -n demo -c kyverno-envoy-plugin -f\n
First , third and last request is passed but second request is failed.

sanskar@sanskar-HP-Laptop-15s-du1xxx:~$ kubectl logs \"$(kubectl get pod -l app=testapp -n demo -o jsonpath={.items..metadata.name})\" -n demo -c kyverno-envoy-plugin -f\nStarting HTTP server on Port 8000\nStarting GRPC server on Port 9000\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:11:42 Request passed the deny-guest-request-at-post policy rule.\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:22:11 Request violation: -> POST method calls at path /book are not allowed to guests users\n -> any[0].check.request.http.headers.authorization.(split(@, ' ')[1]).(jwt_decode(@ , 'secret').payload.role): Invalid value: \"guest\": Expected value: \"admin\"\n-> GET method call is allowed to both guest and admin users\n -> any[1].check.request.http.headers.authorization.(split(@, ' ')[1]).(jwt_decode(@ , 'secret').payload.role): Invalid value: \"guest\": Expected value: \"admin\"\n -> any[1].check.request.http.method: Invalid value: \"POST\": Expected value: \"GET\"\n-> GET method call is allowed to both guest and admin users\n -> any[2].check.request.http.method: Invalid value: \"POST\": Expected value: \"GET\"\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:23:13 Request passed the deny-guest-request-at-post policy rule.\nRequest is initialized in kyvernojson engine .\n2024/04/26 17:23:55 Request passed the deny-guest-request-at-post policy rule.\n
"},{"location":"tutorials/standalone-envoy/#cleanup","title":"Cleanup","text":"

Delete the cluster by running:

$ kind delete cluster --name kyverno-tutorial\n

"},{"location":"tutorials/standalone-envoy/#wrap-up","title":"Wrap Up","text":"

Congratulations on completing the tutorial!

In this tutorial, you learned how to utilize the kyverno-envoy-plugin as an external authorization service to enforce custom policies through Envoy\u2019s external authorization filter.

The tutorial also included an example policy using kyverno-envoy-plugin that returns a boolean decision indicating whether a request should be permitted.

Moreover, Envoy\u2019s external authorization filter supports the inclusion of optional response headers and body content that can be sent to either the downstream client or upstream server. An example of a rule that not only determines request authorization but also provides optional response headers, body content, and HTTP status is available here.

"}]} \ No newline at end of file diff --git a/main/sitemap.xml b/main/sitemap.xml index 4c78dad..940a834 100644 --- a/main/sitemap.xml +++ b/main/sitemap.xml @@ -60,6 +60,18 @@ https://github.io/kyverno/kyverno-envoy-plugin/main/policies/policies/ 2024-11-06 + + https://github.io/kyverno/kyverno-envoy-plugin/main/reference/ + 2024-11-06 + + + https://github.io/kyverno/kyverno-envoy-plugin/main/reference/json-schemas/ + 2024-11-06 + + + https://github.io/kyverno/kyverno-envoy-plugin/main/reference/apis/policy.v1alpha1/ + 2024-11-06 + https://github.io/kyverno/kyverno-envoy-plugin/main/tutorials/istio/ 2024-11-06 diff --git a/main/sitemap.xml.gz b/main/sitemap.xml.gz index 2e29598df77f7b8215e82dabd9600f515bfbdfb4..fd74503df11868aab3cb85758ecf78b4900ac248 100644 GIT binary patch delta 384 zcmV-`0e}AP0*(Wa7k}5!Qu81X0Pwp{(aD|AIF6%E8*kqed_DlB+XM?NvP)}xdTCRu zXUD^G2!DnwzZo(MSv|i8zc@f9hjHC>?Xp=ww77L~ST`^K|EY)OxxZPJ>ym5H4~?8I;!~ z!;v8GFb5MY$N;?+XZMQOWR?N47($7TCutmu^>cQc!XQl+%)X`}WmGEOs|;@iNZf8_ zNSJwcZpt8;w|PZN$`KWir#$cN%(SD41Bl75yAmze(~*Q^TJ+93AZ>^svp7}Z@k5=v zlQ7z~A%c}*2N}eim*1RM!l=G(a>8sTxywFI?V&S1ji!@f6r eKv}(_#jWueVV?GT-?^%PDg7rB2GFt93;+ODW4Sp1 delta 352 zcmV-m0iXVk1MUKl7k}1EZo?oD0MLC;VR?^nqN-{u$JzA+?Ewg$*fhX6%#hSQeX$)y zT~}Se!hD2>CxHf<{g<=F1p*oLyK+<2WdUAeXMDdaKi=QObGdIHniwnr7iqGmT{$q1 zuToyG*NO~-QzKOvdP#an9g?>}SywM*`%t7Z2UuH6X)Nk?j(D5%!Bed=HVD2 z95FcMHRJ(0N25=ISOv}lq|v$P4NtjoG^XQh4v`@@nNz2fhJ+Ghv{nQ-N1*I>Gr)*} zSLY%2IrBQyBv}kmg#47t-mXkNDBnXi`SlpR=5#w!CYctk(FRB=A_$zFD)R6xmhO}p y?cT_O#gQ=>Wl5TJ@G&=(V>{=H3}xv~_nornVV(AO-%_P#ZTkab{Lupj3jhFlNu)^t diff --git a/main/tutorials/istio/index.html b/main/tutorials/istio/index.html index 36c55d9..dbe5895 100644 --- a/main/tutorials/istio/index.html +++ b/main/tutorials/istio/index.html @@ -1,4 +1,4 @@ - Istio - Kyverno Envoy Plugin

Istio

Istio is an open source service mesh for managing the different microservices that make up a cloud-native application. Istio provides a mechanism to use a service as an external authorizer with the AuthorizationPolicy API.

This tutorial shows how Istio’s AuthorizationPolicy can be configured to delegate authorization decisions to Kyverno-envoy-plugin.

Prerequisites

This tutorial requires Kubernetes 1.20 or later. To run the tutorial locally ensure you start a cluster with Kubernetes version 1.20+, we recommend using minikube or KIND.

The tutorial also requries istio v1.19.0 or later. To install istio, follow the instructions here or run the below script it will create a kind cluster and install istio

#!/bin/bash
+ Istio - Kyverno Envoy Plugin      

Istio

Istio is an open source service mesh for managing the different microservices that make up a cloud-native application. Istio provides a mechanism to use a service as an external authorizer with the AuthorizationPolicy API.

This tutorial shows how Istio’s AuthorizationPolicy can be configured to delegate authorization decisions to Kyverno-envoy-plugin.

Prerequisites

This tutorial requires Kubernetes 1.20 or later. To run the tutorial locally ensure you start a cluster with Kubernetes version 1.20+, we recommend using minikube or KIND.

The tutorial also requries istio v1.19.0 or later. To install istio, follow the instructions here or run the below script it will create a kind cluster and install istio

#!/bin/bash
 
 KIND_IMAGE=kindest/node:v1.29.2
 ISTIO_REPO=https://istio-release.storage.googleapis.com/charts
diff --git a/main/tutorials/mtls-istio/index.html b/main/tutorials/mtls-istio/index.html
index 9edc8b2..6a6961b 100644
--- a/main/tutorials/mtls-istio/index.html
+++ b/main/tutorials/mtls-istio/index.html
@@ -1,4 +1,4 @@
- Istio mTLS - Kyverno Envoy Plugin      

Istio mTLS

Istio is an open source service mesh for managing the different microservices that make up a cloud-native application. Istio provides a mechanism to use a service as an external authorizer with the AuthorizationPolicy API.

The kyverno-envoy-plugin is a custom Envoy filter that is used to intercept the incoming request to the service and validate the request using the kyverno engine.

In this tutorial we will create a two simple microservices which are going to make external authorization to a single kyverno-envoy-plugin service as a separate pod in the mesh. With this tutorial we are going to understand how to use multiple microservices to make authorization decisions to a single ext-authz server.

arch-istio-mtls

To handle multiple different requests effectively, we leverage the match/exclude declarations to route the specific authz-request to the appropriate validating policy within the Kyverno engine. This approach allows us to execute the right validating policy for each request, enabling efficient and targeted request processing.

Example Policy

The following policies will be executed by the kyverno-envoy-plugin to validate incoming requests made specifically to the testapp-1 service. By leveraging the match declarations, we ensure that these policies are executed only when the incoming request is destined for the testapp-1 service. This targeted approach allows us to apply the appropriate validation rules and policies based on the specific service being accessed.

apiVersion: json.kyverno.io/v1alpha1
+ Istio mTLS - Kyverno Envoy Plugin      

Istio mTLS

Istio is an open source service mesh for managing the different microservices that make up a cloud-native application. Istio provides a mechanism to use a service as an external authorizer with the AuthorizationPolicy API.

The kyverno-envoy-plugin is a custom Envoy filter that is used to intercept the incoming request to the service and validate the request using the kyverno engine.

In this tutorial we will create a two simple microservices which are going to make external authorization to a single kyverno-envoy-plugin service as a separate pod in the mesh. With this tutorial we are going to understand how to use multiple microservices to make authorization decisions to a single ext-authz server.

arch-istio-mtls

To handle multiple different requests effectively, we leverage the match/exclude declarations to route the specific authz-request to the appropriate validating policy within the Kyverno engine. This approach allows us to execute the right validating policy for each request, enabling efficient and targeted request processing.

Example Policy

The following policies will be executed by the kyverno-envoy-plugin to validate incoming requests made specifically to the testapp-1 service. By leveraging the match declarations, we ensure that these policies are executed only when the incoming request is destined for the testapp-1 service. This targeted approach allows us to apply the appropriate validation rules and policies based on the specific service being accessed.

apiVersion: json.kyverno.io/v1alpha1
 kind: ValidatingPolicy
 metadata:
   name: test-policy
@@ -421,4 +421,4 @@
 2024/05/21 07:41:33 Request passed the deny-external-calls-testapp-1 policy rule.
 2024/05/21 07:42:22 Request is initialized in kyvernojson engine .
 2024/05/21 07:42:22 Request passed the deny-external-calls-testapp-2 policy rule.
-
First request was directed to testapp-1 which was allowed by the policy deny-external-calls-testapp-1 and the second request was directed to testapp-2 which was allowed by the policy deny-external-calls-testapp-2.

Wrap Up

Congratulations on completing the tutorial!

This tutorial demonstrated how to configure Istio's AuthorizationPolicy to utilize the kyverno-envoy-plugin as an separate pod external authorization service. By leveraging the power of Kyverno's policy engine, you can enforce fine-grained authorization rules across your microservices within the Istio service mesh.

Additionally, the tutorial showcased the use of mTLS (Mutual TLS) to secure communication between services and the kyverno-envoy-plugin, ensuring end-to-end encryption and authentication.

The combination of Istio's AuthorizationPolicy and the kyverno-envoy-plugin provides a flexible and powerful solution for implementing custom authorization logic in your cloud-native applications. By following this tutorial, you've gained hands-on experience in configuring and deploying this solution, setting the stage for further exploration and customization to meet your specific requirements.

We hope this tutorial has been informative and has provided you with a solid foundation for integrating the kyverno-envoy-plugin into your Istio service mesh environment. Feel free to explore the project's documentation and community resources for further assistance and to stay updated with the latest developments.

\ No newline at end of file +
First request was directed to testapp-1 which was allowed by the policy deny-external-calls-testapp-1 and the second request was directed to testapp-2 which was allowed by the policy deny-external-calls-testapp-2.

Wrap Up

Congratulations on completing the tutorial!

This tutorial demonstrated how to configure Istio's AuthorizationPolicy to utilize the kyverno-envoy-plugin as an separate pod external authorization service. By leveraging the power of Kyverno's policy engine, you can enforce fine-grained authorization rules across your microservices within the Istio service mesh.

Additionally, the tutorial showcased the use of mTLS (Mutual TLS) to secure communication between services and the kyverno-envoy-plugin, ensuring end-to-end encryption and authentication.

The combination of Istio's AuthorizationPolicy and the kyverno-envoy-plugin provides a flexible and powerful solution for implementing custom authorization logic in your cloud-native applications. By following this tutorial, you've gained hands-on experience in configuring and deploying this solution, setting the stage for further exploration and customization to meet your specific requirements.

We hope this tutorial has been informative and has provided you with a solid foundation for integrating the kyverno-envoy-plugin into your Istio service mesh environment. Feel free to explore the project's documentation and community resources for further assistance and to stay updated with the latest developments.

\ No newline at end of file diff --git a/main/tutorials/standalone-envoy/index.html b/main/tutorials/standalone-envoy/index.html index fca89df..af8b15f 100644 --- a/main/tutorials/standalone-envoy/index.html +++ b/main/tutorials/standalone-envoy/index.html @@ -1,4 +1,4 @@ - Standalone Envoy - Kyverno Envoy Plugin

Standalone Envoy

The tutorial shows how Envoy's External Authorization filter can be used with Kyverno as an authorization service to enforce security policies over API requests received by Envoy.

Overview

In this tutorial we'll see how to use Kyverno-envoy-plugin as an External Authorization service for the Envoy proxy. The goal of the demo to show user how kyverno-envoy-plugin will work with standalone envoy and how it can be used to enforce policies to the traffic between services. The Kyverno-envoy-plugin allows configuring these Envoy proxies to query Kyverno-json for policy decisions on incoming requests. The kyverno-envoy-plugin is cofigured as a static binary and can be run as a sidecar container in the same pod as the application.

We'll do this by:

  • Running a local Kubernetes cluster
  • Creating a simple authorization policy in ValidatingPolicy
  • Deploying a sample application with Envoy and kyverno-envoy-plugin sidecars
  • Run some sample requests to see the policy in action

Note that other than the HTTP client and bundle server, all components are co-located in the same pod.

Demo instructions

Required tools

  1. kind
  2. kubectl

{{< info >}} If you haven't used kind before, you can find installation instructions in the project documentation. {{</ info >}}

Running a local Kubernetes cluster

To start a local kubernetes cluster to run our demo, we'll be using kind. In order to use the kind command, you’ll need to have Docker installed on your machine.

Create a cluster with the following command:

$ kind create cluster --name kyverno-tutorial --image kindest/node:v1.29.2
+ Standalone Envoy - Kyverno Envoy Plugin      

Standalone Envoy

The tutorial shows how Envoy's External Authorization filter can be used with Kyverno as an authorization service to enforce security policies over API requests received by Envoy.

Overview

In this tutorial we'll see how to use Kyverno-envoy-plugin as an External Authorization service for the Envoy proxy. The goal of the demo to show user how kyverno-envoy-plugin will work with standalone envoy and how it can be used to enforce policies to the traffic between services. The Kyverno-envoy-plugin allows configuring these Envoy proxies to query Kyverno-json for policy decisions on incoming requests. The kyverno-envoy-plugin is cofigured as a static binary and can be run as a sidecar container in the same pod as the application.

We'll do this by:

  • Running a local Kubernetes cluster
  • Creating a simple authorization policy in ValidatingPolicy
  • Deploying a sample application with Envoy and kyverno-envoy-plugin sidecars
  • Run some sample requests to see the policy in action

Note that other than the HTTP client and bundle server, all components are co-located in the same pod.

Demo instructions

Required tools

  1. kind
  2. kubectl

{{< info >}} If you haven't used kind before, you can find installation instructions in the project documentation. {{</ info >}}

Running a local Kubernetes cluster

To start a local kubernetes cluster to run our demo, we'll be using kind. In order to use the kind command, you’ll need to have Docker installed on your machine.

Create a cluster with the following command:

$ kind create cluster --name kyverno-tutorial --image kindest/node:v1.29.2
 Creating cluster "kyverno-tutorial" ...
   Ensuring node image (kindest/node:v1.29.2) 🖼
   Preparing nodes 📦