diff --git a/.eslintrc.json b/.eslintrc.json index 02abc4080..d633899fa 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -18,7 +18,6 @@ "plugins": ["@typescript-eslint"], "ignorePatterns": [ "src/templates", - "journey", "node_modules", "dist", "hack", diff --git a/docs/README.md b/docs/README.md index 78312dd23..3106f2cbc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,3 +7,5 @@ ## Additional Docs ### [Pepr Cli](cli.md) ### [Metrics](metrics.md) +### [RBAC](rbac.md) +### [WebAssembly](webassembly.md) diff --git a/docs/cli.md b/docs/cli.md index 439792e0a..de7017018 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -55,4 +55,6 @@ Create a [zarf.yaml](https://zarf.dev) and K8s manifest for the current module. **Options:** +- `-r, --registry-info [/]` - Registry Info: Image registry and username. Note: You must be signed into the registry +- `--rbac-mode [admin|scoped]` - Rbac Mode: admin, scoped (default: admin) - `-l, --log-level [level]` - Log level: debug, info, warn, error (default: "info") diff --git a/docs/rbac.md b/docs/rbac.md new file mode 100644 index 000000000..4b7c9447c --- /dev/null +++ b/docs/rbac.md @@ -0,0 +1,148 @@ +# RBAC Modes + +During the build phase of Pepr (`npx pepr build --rbac-mode [admin|scoped]`), you have the option to specify the desired RBAC mode through specific flags. This allows fine-tuning the level of access granted based on requirements and preferences. + +## Modes + +**admin** + +```bash +npx pepr build --rbac-mode admin +``` + +**Description:** The service account is given cluster-admin permissions, granting it full, unrestricted access across the entire cluster. This can be useful for administrative tasks where broad permissions are necessary. However, use this mode with caution, as it can pose security risks if misused. This is the default mode. + +**scoped** + +```bash +npx pepr build --rbac-mode scoped +``` + +**Description:** The service account is provided just enough permissions to perform its required tasks, and no more. This mode is recommended for most use cases as it limits potential attack vectors and aligns with best practices in security. _The admission controller's primary mutating or validating action doesn't require a ClusterRole (as the request is not persisted or executed while passing through admission control), if you have a use case where the admission controller's logic involves reading other Kubernetes resources or taking additional actions beyond just validating, mutating, or watching the incoming request, appropriate RBAC settings should be reflected in the ClusterRole. See how in [Updating the ClusterRole](#updating-the-clusterrole)._ + +## Debugging RBAC Issues + +If encountering unexpected behaviors in Pepr while running in scoped mode, check to see if they are related to RBAC. + +1. Check Deployment logs for RBAC errors: + +```bash +kubectl logs -n pepr-system -l app | jq + +# example output +{ + "level": 50, + "time": 1697983053758, + "pid": 16, + "hostname": "pepr-static-test-watcher-745d65857d-pndg7", + "data": { + "kind": "Status", + "apiVersion": "v1", + "metadata": {}, + "status": "Failure", + "message": "configmaps \"pepr-ssa-demo\" is forbidden: User \"system:serviceaccount:pepr-system:pepr-static-test\" cannot patch resource \"configmaps\" in API group \"\" in the namespace \"pepr-demo-2\"", + "reason": "Forbidden", + "details": { + "name": "pepr-ssa-demo", + "kind": "configmaps" + }, + "code": 403 + }, + "ok": false, + "status": 403, + "statusText": "Forbidden", + "msg": "Dooes the ServiceAccount permissions to CREATE and PATCH this ConfigMap?" +} +``` + +2. Verify ServiceAccount Permissions with `kubectl auth can-i` + +```bash +SA=$(kubectl get deploy -n pepr-system -o=jsonpath='{range .items[0]}{.spec.template.spec.serviceAccountName}{"\n"}{end}') + +# Can i create configmaps as the service account in pepr-demo-2? +kubectl auth can-i create cm --as=system:serviceaccount:pepr-system:$SA -n pepr-demo-2 + +# example output: no +``` + +3. Describe the ClusterRole + +```bash +SA=$(kubectl get deploy -n pepr-system -o=jsonpath='{range .items[0]}{.spec.template.spec.serviceAccountName}{"\n"}{end}') + +kubectl describe clusterrole $SA + +# example output: +Name: pepr-static-test +Labels: +Annotations: +PolicyRule: + Resources Non-Resource URLs Resource Names Verbs + --------- ----------------- -------------- ----- + peprstores.pepr.dev [] [] [create delete get list patch update watch] + configmaps [] [] [watch] + namespaces [] [] [watch] +``` + +## Updating the ClusterRole + +As discussed in the [Modes](#modes) section, the admission controller's primary mutating or validating action doesn't require a ClusterRole (as the request is not persisted or executed while passing through admission control), if you have a use case where the admission controller's logic involves reading other Kubernetes resources or taking additional actions beyond just validating, mutating, or watching the incoming request, appropriate RBAC settings should be reflected in the ClusterRole. + +Step 1: Figure out the desired permissions. (`kubectl create clusterrole --help` is a good place to start figuring out the syntax) + +```bash + kubectl create clusterrole configMapApplier --verb=create,patch --resource=configmap --dry-run=client -oyaml + + # example output +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: configMapApplier +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - patch +``` + +Step 2: Update the ClusterRole in the `dist` folder. + +```yaml +... +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: pepr-static-test +rules: + - apiGroups: + - pepr.dev + resources: + - peprstores + verbs: + - create + - get + - patch + - watch + - apiGroups: + - '' + resources: + - namespaces + verbs: + - watch + - apiGroups: + - '' + resources: + - configmaps + verbs: + - watch + - create # New + - patch # New +... +``` + +Step 3: Apply the updated configuration diff --git a/journey/entrypoint-wasm.test.ts b/journey/entrypoint-wasm.test.ts index cc20e7e32..844bdc8a0 100644 --- a/journey/entrypoint-wasm.test.ts +++ b/journey/entrypoint-wasm.test.ts @@ -15,4 +15,4 @@ export const cwd = "pepr-test-module"; // Allow 5 minutes for the tests to run jest.setTimeout(1000 * 60 * 5); -describe("Journey: `npx pepr build -r gchr.io/defenseunicorns`", peprBuild); +describe("Journey: `npx pepr build -r gchr.io/defenseunicorns --rbac-mode scoped`", peprBuild); diff --git a/journey/pepr-build-wasm.ts b/journey/pepr-build-wasm.ts index 666d6d61b..2127f429e 100644 --- a/journey/pepr-build-wasm.ts +++ b/journey/pepr-build-wasm.ts @@ -10,8 +10,8 @@ import { resolve } from "path"; import { cwd } from "./entrypoint.test"; export function peprBuild() { - it("should successfully build the Pepr project", async () => { - execSync("npx pepr build -r gchr.io/defenseunicorns", { cwd: cwd, stdio: "inherit" }); + it("should successfully build the Pepr project with arguments", async () => { + execSync("npx pepr build -r gchr.io/defenseunicorns --rbac-mode scoped", { cwd: cwd, stdio: "inherit" }); }); it("should generate produce the K8s yaml file", async () => { @@ -22,6 +22,18 @@ export function peprBuild() { await fs.access(resolve(cwd, "dist", "zarf.yaml")); await validateZarfYaml(); }); + + it("should generate a scoped ClusterRole", async () => { + await validateClusterRoleYaml(); + }); +} + +async function validateClusterRoleYaml() { + // Read the generated yaml files + const k8sYaml = await fs.readFile(resolve(cwd, "dist", "pepr-module-static-test.yaml"), "utf8"); + const cr = await fs.readFile(resolve("journey", "resources", "clusterrole.yaml"), "utf8"); + + expect(k8sYaml.includes(cr)).toEqual(true) } async function validateZarfYaml() { diff --git a/journey/resources/clusterrole.yaml b/journey/resources/clusterrole.yaml new file mode 100644 index 000000000..cfb9a3686 --- /dev/null +++ b/journey/resources/clusterrole.yaml @@ -0,0 +1,26 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: pepr-static-test +rules: + - apiGroups: + - pepr.dev + resources: + - peprstores + verbs: + - create + - get + - patch + - watch + - apiGroups: + - '' + resources: + - namespaces + verbs: + - watch + - apiGroups: + - '' + resources: + - configmaps + verbs: + - watch diff --git a/src/cli/build.ts b/src/cli/build.ts index d396077a6..496b50ceb 100644 --- a/src/cli/build.ts +++ b/src/cli/build.ts @@ -10,6 +10,7 @@ import { Assets } from "../lib/assets"; import { dependencies, version } from "./init/templates"; import { RootCmd } from "./root"; import { peprFormat } from "./format"; +import { Option } from "commander"; const peprTS = "pepr.ts"; @@ -26,7 +27,12 @@ export default function (program: RootCmd) { ) .option( "-r, --registry-info [/]", - "Where to upload the image. Note: You must be signed into the registry", + "Registry Info: Image registry and username. Note: You must be signed into the registry", + ) + .addOption( + new Option("--rbac-mode [admin|scoped]", "Rbac Mode: admin, scoped (default: admin)") + .choices(["admin", "scoped"]) + .default("admin"), ) .action(async opts => { // Build the module @@ -74,7 +80,7 @@ export default function (program: RootCmd) { const yamlFile = `pepr-module-${uuid}.yaml`; const yamlPath = resolve("dist", yamlFile); - const yaml = await assets.allYaml(); + const yaml = await assets.allYaml(opts.rbacMode); const zarfPath = resolve("dist", "zarf.yaml"); const zarf = assets.zarfYaml(yamlFile); diff --git a/src/lib/assets/deploy.ts b/src/lib/assets/deploy.ts index 208d07c78..23c16bac5 100644 --- a/src/lib/assets/deploy.ts +++ b/src/lib/assets/deploy.ts @@ -12,6 +12,7 @@ import { deployment, moduleSecret, namespace, watcher } from "./pods"; import { clusterRole, clusterRoleBinding, serviceAccount, storeRole, storeRoleBinding } from "./rbac"; import { peprStoreCRD } from "./store"; import { webhookConfig } from "./webhooks"; +import { CapabilityExport } from "../types"; export async function deploy(assets: Assets, webhookTimeout?: number) { Log.info("Establishing connection to Kubernetes"); @@ -56,18 +57,18 @@ export async function deploy(assets: Assets, webhookTimeout?: number) { throw new Error("No code provided"); } - await setupRBAC(name); + await setupRBAC(name, assets.capabilities); await setupController(assets, code, hash); await setupWatcher(assets, hash); } -async function setupRBAC(name: string) { +async function setupRBAC(name: string, capabilities: CapabilityExport[]) { Log.info("Applying cluster role binding"); const crb = clusterRoleBinding(name); await K8s(kind.ClusterRoleBinding).Apply(crb); Log.info("Applying cluster role"); - const cr = clusterRole(name); + const cr = clusterRole(name, capabilities); await K8s(kind.ClusterRole).Apply(cr); Log.info("Applying service account"); diff --git a/src/lib/assets/index.ts b/src/lib/assets/index.ts index d94bbdddd..52d260e41 100644 --- a/src/lib/assets/index.ts +++ b/src/lib/assets/index.ts @@ -40,8 +40,8 @@ export class Assets { zarfYaml = (path: string) => zarfYaml(this, path); - allYaml = async () => { + allYaml = async (rbacMode: string) => { this.capabilities = await loadCapabilities(this.path); - return allYaml(this); + return allYaml(this, rbacMode); }; } diff --git a/src/lib/assets/rbac.ts b/src/lib/assets/rbac.ts index 1c67dab52..03b67f3f4 100644 --- a/src/lib/assets/rbac.ts +++ b/src/lib/assets/rbac.ts @@ -2,26 +2,42 @@ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors import { kind } from "kubernetes-fluent-client"; - +import { CapabilityExport } from "../types"; +import { createRBACMap } from "../helpers"; /** * Grants the controller access to cluster resources beyond the mutating webhook. * * @todo: should dynamically generate this based on resources used by the module. will also need to explore how this should work for multiple modules. * @returns */ -export function clusterRole(name: string): kind.ClusterRole { +export function clusterRole(name: string, capabilities: CapabilityExport[], rbacMode: string = ""): kind.ClusterRole { + const rbacMap = createRBACMap(capabilities); return { apiVersion: "rbac.authorization.k8s.io/v1", kind: "ClusterRole", metadata: { name }, - rules: [ - { - // @todo: make this configurable - apiGroups: ["*"], - resources: ["*"], - verbs: ["create", "delete", "get", "list", "patch", "update", "watch"], - }, - ], + rules: + rbacMode === "scoped" + ? [ + ...Object.keys(rbacMap).map(key => { + // let group:string, version:string, kind:string; + let group: string; + key.split("/").length < 3 ? (group = "") : (group = key.split("/")[0]); + + return { + apiGroups: [group], + resources: [rbacMap[key].plural], + verbs: rbacMap[key].verbs, + }; + }), + ] + : [ + { + apiGroups: ["*"], + resources: ["*"], + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"], + }, + ], }; } @@ -64,7 +80,7 @@ export function storeRole(name: string): kind.Role { metadata: { name, namespace: "pepr-system" }, rules: [ { - apiGroups: ["pepr.dev/*"], + apiGroups: ["pepr.dev"], resources: ["peprstores"], resourceNames: [""], verbs: ["create", "get", "patch", "watch"], diff --git a/src/lib/assets/yaml.ts b/src/lib/assets/yaml.ts index 029671d2e..b03de6503 100644 --- a/src/lib/assets/yaml.ts +++ b/src/lib/assets/yaml.ts @@ -40,7 +40,7 @@ export function zarfYaml({ name, image, config }: Assets, path: string) { return dumpYaml(zarfCfg, { noRefs: true }); } -export async function allYaml(assets: Assets) { +export async function allYaml(assets: Assets, rbacMode: string) { const { name, tls, apiToken, path } = assets; const code = await fs.readFile(path); @@ -54,7 +54,7 @@ export async function allYaml(assets: Assets) { const resources = [ namespace, - clusterRole(name), + clusterRole(name, assets.capabilities, rbacMode), clusterRoleBinding(name), serviceAccount(name), apiTokenSecret(name, apiToken), diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts new file mode 100644 index 000000000..90025a6d5 --- /dev/null +++ b/src/lib/helpers.test.ts @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The Pepr Authors + +import { CapabilityExport } from "./types"; +import { createRBACMap, addVerbIfNotExists } from "./helpers"; +import { expect, describe, test } from "@jest/globals"; + +const capabilities: CapabilityExport[] = JSON.parse(`[ + { + "name": "hello-pepr", + "description": "A simple example capability to show how things work.", + "namespaces": [ + "pepr-demo", + "pepr-demo-2" + ], + "bindings": [ + { + "kind": { + "kind": "Namespace", + "version": "v1", + "group": "" + }, + "event": "CREATE", + "filters": { + "name": "", + "namespaces": [], + "labels": {}, + "annotations": {} + }, + "isMutate": true + }, + { + "kind": { + "kind": "Namespace", + "version": "v1", + "group": "" + }, + "event": "DELETE", + "filters": { + "name": "pepr-demo-2", + "namespaces": [], + "labels": {}, + "annotations": {} + }, + "isWatch": true + }, + { + "kind": { + "kind": "ConfigMap", + "version": "v1", + "group": "" + }, + "event": "CREATE", + "filters": { + "name": "example-1", + "namespaces": [], + "labels": {}, + "annotations": {} + }, + "isMutate": true + }, + { + "kind": { + "kind": "ConfigMap", + "version": "v1", + "group": "" + }, + "event": "UPDATE", + "filters": { + "name": "example-2", + "namespaces": [], + "labels": {}, + "annotations": {} + }, + "isMutate": true + }, + { + "kind": { + "kind": "ConfigMap", + "version": "v1", + "group": "" + }, + "event": "CREATE", + "filters": { + "name": "example-2", + "namespaces": [], + "labels": {}, + "annotations": {} + }, + "isValidate": true + }, + { + "kind": { + "kind": "ConfigMap", + "version": "v1", + "group": "" + }, + "event": "CREATE", + "filters": { + "name": "example-2", + "namespaces": [], + "labels": {}, + "annotations": {} + }, + "isWatch": true + }, + { + "kind": { + "kind": "ConfigMap", + "version": "v1", + "group": "" + }, + "event": "CREATE", + "filters": { + "name": "", + "namespaces": [], + "labels": {}, + "annotations": {} + }, + "isValidate": true + }, + { + "kind": { + "kind": "ConfigMap", + "version": "v1", + "group": "" + }, + "event": "CREATEORUPDATE", + "filters": { + "name": "", + "namespaces": [], + "labels": { + "change": "by-label" + }, + "annotations": {} + }, + "isMutate": true + }, + { + "kind": { + "kind": "ConfigMap", + "version": "v1", + "group": "" + }, + "event": "DELETE", + "filters": { + "name": "", + "namespaces": [], + "labels": { + "change": "by-label" + }, + "annotations": {} + }, + "isValidate": true + }, + { + "kind": { + "kind": "ConfigMap", + "version": "v1", + "group": "" + }, + "event": "CREATE", + "filters": { + "name": "example-4", + "namespaces": [], + "labels": {}, + "annotations": {} + }, + "isMutate": true + }, + { + "kind": { + "kind": "ConfigMap", + "version": "v1", + "group": "" + }, + "event": "CREATE", + "filters": { + "name": "example-4a", + "namespaces": [ + "pepr-demo-2" + ], + "labels": {}, + "annotations": {} + }, + "isMutate": true + }, + { + "kind": { + "kind": "ConfigMap", + "version": "v1", + "group": "" + }, + "event": "CREATE", + "filters": { + "name": "", + "namespaces": [], + "labels": { + "chuck-norris": "" + }, + "annotations": {} + }, + "isMutate": true + }, + { + "kind": { + "kind": "Secret", + "version": "v1", + "group": "" + }, + "event": "CREATE", + "filters": { + "name": "secret-1", + "namespaces": [], + "labels": {}, + "annotations": {} + }, + "isMutate": true + }, + { + "kind": { + "group": "pepr.dev", + "version": "v1", + "kind": "Unicorn" + }, + "event": "CREATE", + "filters": { + "name": "example-1", + "namespaces": [], + "labels": {}, + "annotations": {} + }, + "isMutate": true + }, + { + "kind": { + "group": "pepr.dev", + "version": "v1", + "kind": "Unicorn" + }, + "event": "CREATE", + "filters": { + "name": "example-2", + "namespaces": [], + "labels": {}, + "annotations": {} + }, + "isMutate": true + } + ] + } +]`); + +describe("createRBACMap", () => { + test("should return the correct RBACMap for given capabilities", () => { + const result = createRBACMap(capabilities); + + const expected = { + "pepr.dev/v1/peprstore": { + verbs: ["create", "get", "patch", "watch"], + plural: "peprstores", + }, + "/v1/Namespace": { verbs: ["watch"], plural: "namespaces" }, + "/v1/ConfigMap": { verbs: ["watch"], plural: "configmaps" }, + }; + + expect(result).toEqual(expected); + }); +}); + +describe("addVerbIfNotExists", () => { + test("should add a verb if it does not exist in the array", () => { + const verbs = ["get", "list"]; + addVerbIfNotExists(verbs, "watch"); + expect(verbs).toEqual(["get", "list", "watch"]); + }); + + test("should not add a verb if it already exists in the array", () => { + const verbs = ["get", "list", "watch"]; + addVerbIfNotExists(verbs, "get"); + expect(verbs).toEqual(["get", "list", "watch"]); // The array remains unchanged + }); +}); diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts new file mode 100644 index 000000000..067291a45 --- /dev/null +++ b/src/lib/helpers.ts @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The Pepr Authors + +import { CapabilityExport } from "./types"; + +type RBACMap = { + [key: string]: { + verbs: string[]; + plural: string; + }; +}; + +export const addVerbIfNotExists = (verbs: string[], verb: string) => { + if (!verbs.includes(verb)) { + verbs.push(verb); + } +}; + +export const createRBACMap = (capabilities: CapabilityExport[]): RBACMap => { + return capabilities.reduce((acc: RBACMap, capability: CapabilityExport) => { + capability.bindings.forEach(binding => { + const key = `${binding.kind.group}/${binding.kind.version}/${binding.kind.kind}`; + + acc["pepr.dev/v1/peprstore"] = { + verbs: ["create", "get", "patch", "watch"], + plural: "peprstores", + }; + + if (!acc[key] && binding.isWatch) { + acc[key] = { + verbs: ["watch"], + plural: binding.kind.plural || `${binding.kind.kind.toLowerCase()}s`, + }; + } + }); + + return acc; + }, {}); +}; diff --git a/src/templates/capabilities/hello-pepr.ts b/src/templates/capabilities/hello-pepr.ts index dc11710f1..daed2bab9 100644 --- a/src/templates/capabilities/hello-pepr.ts +++ b/src/templates/capabilities/hello-pepr.ts @@ -52,19 +52,24 @@ When(a.Namespace) .Watch(async ns => { Log.info("Namespace pepr-demo-2 was created."); + try { + // Apply the ConfigMap using K8s server-side apply + await K8s(kind.ConfigMap).Apply({ + metadata: { + name: "pepr-ssa-demo", + namespace: "pepr-demo-2", + }, + data: { + "ns-uid": ns.metadata.uid, + }, + }); + } catch (error) { + // You can use the Log object to log messages to the Pepr controller pod + Log.error(error, "Failed to apply ConfigMap using server-side apply."); + } + // You can share data between actions using the Store, including between different types of actions Store.setItem("watch-data", "This data was stored by a Watch Action."); - - // Apply the ConfigMap using K8s server-side apply - await K8s(kind.ConfigMap).Apply({ - metadata: { - name: "pepr-ssa-demo", - namespace: "pepr-demo-2", - }, - data: { - "ns-uid": ns.metadata.uid, - }, - }); }); /** diff --git a/website/content/en/docs/cli.md b/website/content/en/docs/cli.md index 9b01c5bfe..d2d6b18fd 100644 --- a/website/content/en/docs/cli.md +++ b/website/content/en/docs/cli.md @@ -2,10 +2,9 @@ title: CLI linkTitle: CLI --- - # Pepr CLI -## `pepr init` +## pepr init Initialize a new Pepr Module. @@ -16,7 +15,7 @@ Initialize a new Pepr Module. --- -## `pepr update` +## pepr update Update the current Pepr Module to the latest SDK version and update the global Pepr CLI to the same version. @@ -27,7 +26,7 @@ Update the current Pepr Module to the latest SDK version and update the global P --- -## `pepr dev` +## pepr dev Connect a local cluster to a local version of the Pepr Controller to do real-time debugging of your module. Note the `pepr dev` assumes a K3d cluster is running by default. If you are working with Kind or another docker-based @@ -42,7 +41,7 @@ cluster you will have to give Pepr a host path to your machine that is reachable --- -## `pepr deploy` +## pepr deploy Deploy the current module into a Kubernetes cluster, useful for CI systems. Not recommended for production use. @@ -54,12 +53,12 @@ Deploy the current module into a Kubernetes cluster, useful for CI systems. Not --- -## `pepr build` +## pepr build Create a [zarf.yaml](https://zarf.dev) and K8s manifest for the current module. This includes everything needed to deploy Pepr and the current module into production environments. **Options:** - `-r, --registry-info [/]` - Registry Info: Image registry and username. Note: You must be signed into the registry -- `-rm, --rbac-mode [admin|scoped]` - Rbac Mode: admin, scoped (default: admin) +- `--rbac-mode [admin|scoped]` - Rbac Mode: admin, scoped (default: admin) - `-l, --log-level [level]` - Log level: debug, info, warn, error (default: "info") diff --git a/website/content/en/docs/rbac.md b/website/content/en/docs/rbac.md index 87b6c2fd4..31aeacd84 100644 --- a/website/content/en/docs/rbac.md +++ b/website/content/en/docs/rbac.md @@ -3,14 +3,13 @@ title: RBAC linkTitle: RBAC --- - # RBAC Modes During the build phase of Pepr (`npx pepr build --rbac-mode [admin|scoped]`), you have the option to specify the desired RBAC mode through specific flags. This allows fine-tuning the level of access granted based on requirements and preferences. ## Modes -### `admin` +**admin** ```bash npx pepr build --rbac-mode admin @@ -18,13 +17,13 @@ npx pepr build --rbac-mode admin **Description:** The service account is given cluster-admin permissions, granting it full, unrestricted access across the entire cluster. This can be useful for administrative tasks where broad permissions are necessary. However, use this mode with caution, as it can pose security risks if misused. This is the default mode. -### `scoped` +**scoped** ```bash npx pepr build --rbac-mode scoped ``` -**Description:** The service account is provided just enough permissions to perform its required tasks, and no more. This mode is recommended for most use cases as it limits potential attack vectors and aligns with best practices in security. _The admission controller's primary mutating or validating action doesn't require a ClusterRole (as the request is not persisted or executed while passing through admission control), if you have a use case where the admission controller's logic involves reading other Kubernetes resources or taking additional actions beyond just validating, mutating, or watching the incoming request, appropriate RBAC settings should be reflected in the ClusterRole._ +**Description:** The service account is provided just enough permissions to perform its required tasks, and no more. This mode is recommended for most use cases as it limits potential attack vectors and aligns with best practices in security. _The admission controller's primary mutating or validating action doesn't require a ClusterRole (as the request is not persisted or executed while passing through admission control), if you have a use case where the admission controller's logic involves reading other Kubernetes resources or taking additional actions beyond just validating, mutating, or watching the incoming request, appropriate RBAC settings should be reflected in the ClusterRole. See how in [Updating the ClusterRole](#updating-the-clusterrole)._ ## Debugging RBAC Issues @@ -72,7 +71,7 @@ kubectl auth can-i create cm --as=system:serviceaccount:pepr-system:$SA -n pepr- # example output: no ``` -3. Describe the ServiceAccount +3. Describe the ClusterRole ```bash SA=$(kubectl get deploy -n pepr-system -o=jsonpath='{range .items[0]}{.spec.template.spec.serviceAccountName}{"\n"}{end}') @@ -90,3 +89,65 @@ PolicyRule: configmaps [] [] [watch] namespaces [] [] [watch] ``` + +## Updating the ClusterRole + +As discussed in the [Modes](#modes) section, the admission controller's primary mutating or validating action doesn't require a ClusterRole (as the request is not persisted or executed while passing through admission control), if you have a use case where the admission controller's logic involves reading other Kubernetes resources or taking additional actions beyond just validating, mutating, or watching the incoming request, appropriate RBAC settings should be reflected in the ClusterRole. + +Step 1: Figure out the desired permissions. (`kubectl create clusterrole --help` is a good place to start figuring out the syntax) + +```bash + kubectl create clusterrole configMapApplier --verb=create,patch --resource=configmap --dry-run=client -oyaml + + # example output +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: configMapApplier +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - patch +``` + +Step 2: Update the ClusterRole in the `dist` folder. + +```yaml +... +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: pepr-static-test +rules: + - apiGroups: + - pepr.dev + resources: + - peprstores + verbs: + - create + - get + - patch + - watch + - apiGroups: + - '' + resources: + - namespaces + verbs: + - watch + - apiGroups: + - '' + resources: + - configmaps + verbs: + - watch + - create # New + - patch # New +... +``` + +Step 3: Apply the updated configuration