Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: least priv rbac creation #324

Merged
merged 28 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c8b910d
feat: least priv RBAC creation
cmwylie19 Oct 17, 2023
5851a97
chore: refactoring for efficiency
cmwylie19 Oct 18, 2023
d51579f
docs: add updates to docs
cmwylie19 Oct 19, 2023
2886a45
chore: improve unit test & docs
cmwylie19 Oct 20, 2023
99714fd
chore: preparing e2e
cmwylie19 Oct 20, 2023
2826a12
chore: scoped clusterrole journey and remove duplicate entry in eslintrc
cmwylie19 Oct 21, 2023
eea8c0a
Merge branch 'main' into 31
cmwylie19 Oct 21, 2023
917e997
chore: journey build test shouldnt change
cmwylie19 Oct 21, 2023
7698d23
chore: fix e2e
cmwylie19 Oct 21, 2023
fd4b234
chore: expected e2e journey
cmwylie19 Oct 21, 2023
976720d
chore: fix test
cmwylie19 Oct 21, 2023
94568ff
Merge branch 'main' into 31
cmwylie19 Oct 21, 2023
283307a
chore: update examples
cmwylie19 Oct 22, 2023
1a64eb1
chore: update rbac docs
cmwylie19 Oct 22, 2023
9dc8336
chore: links to docs in README.md
cmwylie19 Oct 22, 2023
1c87d43
Merge branch 'main' into 31
cmwylie19 Oct 23, 2023
5a96e45
Merge branch 'main' into 31
cmwylie19 Oct 23, 2023
cfe85d5
feat: finish feature
cmwylie19 Oct 23, 2023
d1bcde3
chore: update website docs
cmwylie19 Oct 23, 2023
509fa9a
chore: fix journey test for clusterrole
cmwylie19 Oct 23, 2023
0bdb804
chore: forgot to fix pepr-build-wasm
cmwylie19 Oct 23, 2023
a534481
chore: rbacMode defaults to admin, remoce check for undefined
cmwylie19 Oct 23, 2023
5124d46
chore: format fix
cmwylie19 Oct 23, 2023
1680070
Merge branch 'main' into 31
cmwylie19 Oct 24, 2023
4609222
Merge branch 'main' into 31
cmwylie19 Oct 25, 2023
d750628
docs: update docs for theme
cmwylie19 Oct 25, 2023
fb6ae87
chore: addressed unnecessary default
cmwylie19 Oct 26, 2023
38f1569
Merge branch 'main' into 31
cmwylie19 Oct 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"plugins": ["@typescript-eslint"],
"ignorePatterns": [
"src/templates",
"journey",
"node_modules",
"dist",
"hack",
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@
## Additional Docs
### [Pepr Cli](cli.md)
### [Metrics](metrics.md)
### [RBAC](rbac.md)
### [WebAssembly](webassembly.md)
2 changes: 2 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,6 @@ Create a [zarf.yaml](https://zarf.dev) and K8s manifest for the current module.

**Options:**

- `-r, --registry-info [<registry>/<username>]` - 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")
148 changes: 148 additions & 0 deletions docs/rbac.md
Original file line number Diff line number Diff line change
@@ -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: <none>
Annotations: <none>
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
2 changes: 1 addition & 1 deletion journey/entrypoint-wasm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
16 changes: 14 additions & 2 deletions journey/pepr-build-wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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() {
Expand Down
26 changes: 26 additions & 0 deletions journey/resources/clusterrole.yaml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 8 additions & 2 deletions src/cli/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -26,7 +27,12 @@ export default function (program: RootCmd) {
)
.option(
"-r, --registry-info [<registry>/<username>]",
"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
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 4 additions & 3 deletions src/lib/assets/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions src/lib/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
}
38 changes: 27 additions & 11 deletions src/lib/assets/rbac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
},
],
};
}

Expand Down Expand Up @@ -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"],
Expand Down
4 changes: 2 additions & 2 deletions src/lib/assets/yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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),
Expand Down
Loading