From ee9d557c9ce99459ccd1eb03b062fe469e456158 Mon Sep 17 00:00:00 2001 From: zheng861 Date: Sat, 17 Jan 2026 14:54:14 -0500 Subject: [PATCH 1/8] Include file path in entrypoint Otherwise, no file is run --- tilt/Tiltfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tilt/Tiltfile b/tilt/Tiltfile index acce5b3..27d0d61 100644 --- a/tilt/Tiltfile +++ b/tilt/Tiltfile @@ -21,7 +21,7 @@ if separate_frontend_and_backend: docker_build_with_restart( "anvilops/anvilops", "../", dockerfile="./anvilops-backend.Dockerfile", - entrypoint=["/usr/local/bin/node", "--experimental-strip-types"], + entrypoint=["/usr/local/bin/node", "--experimental-strip-types", "./src/index.ts"], live_update=[ sync("../backend/src", "/app/src") ], From f7ec3bd92cb5116aefc1cd19fa8bfc789c6a2c1b Mon Sep 17 00:00:00 2001 From: zheng861 Date: Sun, 18 Jan 2026 01:12:02 -0500 Subject: [PATCH 2/8] Optionally create ingress network policies --- backend/src/lib/cluster/resources.ts | 156 ++++++++++++++---- backend/src/lib/env.ts | 8 + backend/src/service/deleteApp.ts | 6 +- backend/src/service/helper/deployment.ts | 4 +- backend/src/service/updateDeployment.ts | 4 +- .../anvilops/anvilops-deployment.yaml | 8 + .../templates/anvilops/anvilops-netpol.yaml | 2 +- charts/anvilops/values.yaml | 5 + tilt/local-values.yaml | 6 + 9 files changed, 159 insertions(+), 40 deletions(-) diff --git a/backend/src/lib/cluster/resources.ts b/backend/src/lib/cluster/resources.ts index 4d06130..1706642 100644 --- a/backend/src/lib/cluster/resources.ts +++ b/backend/src/lib/cluster/resources.ts @@ -3,6 +3,8 @@ import type { V1EnvVar, V1Ingress, V1Namespace, + V1NetworkPolicy, + V1NetworkPolicyPeer, V1Secret, } from "@kubernetes/client-node"; import { randomBytes } from "node:crypto"; @@ -13,6 +15,7 @@ import type { Organization, WorkloadConfig, } from "../../db/models.ts"; +import { env } from "../env.ts"; import { getOctokit } from "../octokit.ts"; import { createIngressConfig } from "./resources/ingress.ts"; import { createServiceConfig } from "./resources/service.ts"; @@ -42,6 +45,27 @@ export const RANDOM_TAG_LEN = 8; export const getNamespace = (subdomain: string) => NAMESPACE_PREFIX + subdomain; +let allowedIngressPeers: V1NetworkPolicyPeer[] | null; +const getAllowedIngressPeers = (): V1NetworkPolicyPeer[] | null => { + if (!env.CREATE_INGRESS_NETPOL || !env.ALLOW_INGRESS_FROM) { + return null; + } + + if (!allowedIngressPeers) { + const allowedLabels = JSON.parse(env.ALLOW_INGRESS_FROM) as { + [key: string]: string; + }[]; + allowedIngressPeers = allowedLabels.map((labels) => ({ + namespaceSelector: { + matchLabels: labels, + }, + podSelector: {}, + })); + } + + return allowedIngressPeers; +}; + export interface K8sObject { apiVersion: string; kind: string; @@ -132,6 +156,48 @@ const createSecretConfig = ( }; }; +const createIngressNetPol = ({ + name, + namespace, + groupLabels, +}: { + name: string; + namespace: string; + groupLabels: { [key: string]: string }; +}): V1NetworkPolicy & K8sObject => { + if (!env.CREATE_INGRESS_NETPOL || !env.ALLOW_INGRESS_FROM) { + return null; + } + + return { + apiVersion: "networking.k8s.io/v1", + kind: "NetworkPolicy", + metadata: { + name, + namespace, + }, + spec: { + podSelector: { + matchLabels: groupLabels, + }, + policyTypes: ["Ingress"], + ingress: [ + { + _from: [ + ...getAllowedIngressPeers(), + { + namespaceSelector: { + matchLabels: groupLabels, // Allow ingress from pods in namespaces of this group + }, + podSelector: {}, + }, + ], + }, + ], + }, + } satisfies V1NetworkPolicy; +}; + const applyLabels = (config: K8sObject, labels: { [key: string]: string }) => { config.metadata.labels = { ...config.metadata.labels, ...labels }; if (config.spec?.template) { @@ -146,31 +212,39 @@ const applyLabels = (config: K8sObject, labels: { [key: string]: string }) => { } }; -export const createAppConfigsFromDeployment = async ( - org: Organization, - app: App, - appGroup: AppGroup, - deployment: Deployment, - conf: WorkloadConfig, -) => { +export const createAppConfigsFromDeployment = async ({ + org, + app, + appGroup, + deployment, + config, + withLabels = true, +}: { + org: Organization; + app: App; + appGroup: AppGroup; + deployment: Deployment; + config: WorkloadConfig; + withLabels?: boolean; +}) => { const namespaceName = getNamespace(app.namespace); const namespace = createNamespaceConfig(namespaceName, app.projectId); const configs: K8sObject[] = []; const octokit = - conf.source === "GIT" ? await getOctokit(org.githubInstallationId) : null; + config.source === "GIT" ? await getOctokit(org.githubInstallationId) : null; const secretName = `${app.name}-secrets-${deployment.id}`; const envVars = await getEnvVars( - conf.getEnv(), + config.getEnv(), secretName, octokit, deployment, - conf, + config, app, ); - const secretData = getEnvRecord(conf.getEnv()); + const secretData = getEnvRecord(config.getEnv()); if (secretData !== null) { const secretConfig = createSecretConfig( secretData, @@ -184,20 +258,20 @@ export const createAppConfigsFromDeployment = async ( const params = { deploymentId: deployment.id, - collectLogs: conf.collectLogs, + collectLogs: config.collectLogs, name: app.name, namespace: namespaceName, serviceName: namespaceName, - image: conf.imageTag, + image: config.imageTag, env: envVars, logIngestSecret: app.logIngestSecret, - subdomain: conf.subdomain, - createIngress: conf.createIngress, - port: conf.port, - replicas: conf.replicas, - mounts: conf.mounts, - requests: conf.requests, - limits: conf.limits, + subdomain: config.subdomain, + createIngress: config.createIngress, + port: config.port, + replicas: config.replicas, + mounts: config.mounts, + requests: config.requests, + limits: config.limits, }; const svc = createServiceConfig(params); @@ -210,19 +284,37 @@ export const createAppConfigsFromDeployment = async ( configs.push(ingress); } - const appGroupLabel = `${appGroup.name.replaceAll(" ", "_")}-${appGroup.id}-${org.id}`; - const labels = { - "anvilops.rcac.purdue.edu/app-group-id": appGroup.id.toString(), - "anvilops.rcac.purdue.edu/app-id": app.id.toString(), - "anvilops.rcac.purdue.edu/deployment-id": deployment.id.toString(), - "app.kubernetes.io/name": app.name, - "app.kubernetes.io/part-of": appGroupLabel, - "app.kubernetes.io/managed-by": "anvilops", - }; - applyLabels(namespace, labels); - for (let config of configs) { - applyLabels(config, labels); + if (withLabels) { + const appGroupLabel = `${appGroup.name.replaceAll(" ", "_")}-${appGroup.id}-${org.id}`; + const groupLabels = { + "anvilops.rcac.purdue.edu/app-group-id": appGroup.id.toString(), + "app.kubernetes.io/part-of": appGroupLabel, + }; + + const netpol = createIngressNetPol({ + name: params.name, + namespace: params.namespace, + groupLabels, + }); + + if (netpol) { + configs.push(netpol); + } + + const labels = { + ...groupLabels, + "anvilops.rcac.purdue.edu/app-id": app.id.toString(), + "anvilops.rcac.purdue.edu/deployment-id": deployment.id.toString(), + "app.kubernetes.io/name": app.name, + "app.kubernetes.io/managed-by": "anvilops", + }; + + applyLabels(namespace, labels); + for (let config of configs) { + applyLabels(config, labels); + } } + const postCreate = async (api: KubernetesObjectApi) => { // Clean up secrets and ingresses from previous deployments of the app const secrets = (await api diff --git a/backend/src/lib/env.ts b/backend/src/lib/env.ts index 15e20d7..d3696ec 100644 --- a/backend/src/lib/env.ts +++ b/backend/src/lib/env.ts @@ -211,6 +211,14 @@ const variables = { * Annotations to add to end users' Ingress configurations */ INGRESS_ANNOTATIONS: { required: false }, + /** + * Whether to create network policies in tenant namespaces to restrict ingress. Ingress will be restricted to namespaces with allowed labels and apps in the same group. + */ + CREATE_INGRESS_NETPOL: { required: false }, + /** + * Labels identifying namespaces to allow ingress to tenant pods. AnvilOps will not create network policies unless CREATE_INGRESS_NETPOL is true and ALLOW_INGRESS_FROM is set. + */ + ALLOW_INGRESS_FROM: { required: false }, /** * The storageClassName to use when provisioning tenant apps. If you omit this value, storage-related options will be hidden. */ diff --git a/backend/src/service/deleteApp.ts b/backend/src/service/deleteApp.ts index ec60765..d3ee5a6 100644 --- a/backend/src/service/deleteApp.ts +++ b/backend/src/service/deleteApp.ts @@ -48,13 +48,13 @@ export async function deleteApp( ]); const { namespace, configs, postCreate } = - await createAppConfigsFromDeployment( + await createAppConfigsFromDeployment({ org, app, appGroup, - lastDeployment, + deployment: lastDeployment, config, - ); + }); const { KubernetesObjectApi: api } = await getClientsForRequest( userId, diff --git a/backend/src/service/helper/deployment.ts b/backend/src/service/helper/deployment.ts index ddc5b56..3d7f322 100644 --- a/backend/src/service/helper/deployment.ts +++ b/backend/src/service/helper/deployment.ts @@ -387,13 +387,13 @@ export class DeploymentService { // If we're creating a deployment directly from an existing image tag, just deploy it now try { const { namespace, configs, postCreate } = - await createAppConfigsFromDeployment( + await createAppConfigsFromDeployment({ org, app, appGroup, deployment, config, - ); + }); const api = getClientForClusterUsername( app.clusterUsername, "KubernetesObjectApi", diff --git a/backend/src/service/updateDeployment.ts b/backend/src/service/updateDeployment.ts index 069e40a..b88e927 100644 --- a/backend/src/service/updateDeployment.ts +++ b/backend/src/service/updateDeployment.ts @@ -95,13 +95,13 @@ export async function updateDeployment(secret: string, newStatus: string) { if (newStatus === "DEPLOYING") { const { namespace, configs, postCreate } = - await createAppConfigsFromDeployment( + await createAppConfigsFromDeployment({ org, app, appGroup, deployment, config, - ); + }); try { const api = getClientForClusterUsername( diff --git a/charts/anvilops/templates/anvilops/anvilops-deployment.yaml b/charts/anvilops/templates/anvilops/anvilops-deployment.yaml index 5b19b05..d0fe6f9 100644 --- a/charts/anvilops/templates/anvilops/anvilops-deployment.yaml +++ b/charts/anvilops/templates/anvilops/anvilops-deployment.yaml @@ -210,6 +210,14 @@ spec: - name: INGRESS_ANNOTATIONS value: {{ . | toJson | quote }} {{- end }} + {{- if .Values.anvilops.apps.netpol.createIngressNetworkPolicy }} + - name: CREATE_INGRESS_NETPOL + value: "true" + {{- with .Values.anvilops.apps.netpol.allowedIngressMatchLabels }} + - name: ALLOW_INGRESS_FROM + value: {{ toJson . | quote }} + {{- end }} + {{- end }} - name: STORAGE_CLASS_NAME value: {{ .Values.tenants.storageClassName }} - name: STORAGE_ACCESS_MODES diff --git a/charts/anvilops/templates/anvilops/anvilops-netpol.yaml b/charts/anvilops/templates/anvilops/anvilops-netpol.yaml index d5c91cc..f4a8a6b 100644 --- a/charts/anvilops/templates/anvilops/anvilops-netpol.yaml +++ b/charts/anvilops/templates/anvilops/anvilops-netpol.yaml @@ -7,7 +7,7 @@ metadata: spec: podSelector: matchLabels: - app: anvilops + app.kubernetes.io/name: anvilops ingress: - {} policyTypes: diff --git a/charts/anvilops/values.yaml b/charts/anvilops/values.yaml index fb34946..21cf9e9 100644 --- a/charts/anvilops/values.yaml +++ b/charts/anvilops/values.yaml @@ -91,6 +91,11 @@ anvilops: annotations: # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" + netpol: + createIngressNetworkPolicy: false + # allowedIngressMatchLabels: + # - kubernetes.io/metadata.name: kube-system + # - kubernetes.io/metadata.name: ingress-nginx postgres: generateCredentials: true diff --git a/tilt/local-values.yaml b/tilt/local-values.yaml index 63bd7e6..7122b60 100644 --- a/tilt/local-values.yaml +++ b/tilt/local-values.yaml @@ -45,6 +45,12 @@ anvilops: ingress: className: nginx annotations: + netpol: + createIngressNetworkPolicy: true + allowedIngressMatchLabels: + - kubernetes.io/metadata.name: kube-system + - kubernetes.io/metadata.name: ingress-nginx + postgres: generateCredentials: false image: postgres:17 From d7541f657e4409ecda3c8f21ef49f981e004e2b9 Mon Sep 17 00:00:00 2001 From: zheng861 Date: Sun, 18 Jan 2026 14:00:31 -0500 Subject: [PATCH 3/8] Delete network policies when migrating the app --- backend/src/lib/cluster/resources.ts | 126 ++++++++++++++++++--------- backend/src/service/deleteApp.ts | 3 +- 2 files changed, 89 insertions(+), 40 deletions(-) diff --git a/backend/src/lib/cluster/resources.ts b/backend/src/lib/cluster/resources.ts index 1706642..10d688b 100644 --- a/backend/src/lib/cluster/resources.ts +++ b/backend/src/lib/cluster/resources.ts @@ -1,7 +1,6 @@ import type { KubernetesObjectApi, V1EnvVar, - V1Ingress, V1Namespace, V1NetworkPolicy, V1NetworkPolicyPeer, @@ -218,14 +217,14 @@ export const createAppConfigsFromDeployment = async ({ appGroup, deployment, config, - withLabels = true, + migrating = false, }: { org: Organization; app: App; appGroup: AppGroup; deployment: Deployment; config: WorkloadConfig; - withLabels?: boolean; + migrating?: boolean; }) => { const namespaceName = getNamespace(app.namespace); @@ -284,13 +283,30 @@ export const createAppConfigsFromDeployment = async ({ configs.push(ingress); } - if (withLabels) { - const appGroupLabel = `${appGroup.name.replaceAll(" ", "_")}-${appGroup.id}-${org.id}`; - const groupLabels = { - "anvilops.rcac.purdue.edu/app-group-id": appGroup.id.toString(), - "app.kubernetes.io/part-of": appGroupLabel, - }; + const appGroupLabel = `${appGroup.name.replaceAll(" ", "_")}-${appGroup.id}-${org.id}`; + const groupLabels = { + "anvilops.rcac.purdue.edu/app-group-id": appGroup.id.toString(), + "app.kubernetes.io/part-of": appGroupLabel, + }; + const labels = { + ...groupLabels, + "anvilops.rcac.purdue.edu/app-id": app.id.toString(), + "anvilops.rcac.purdue.edu/deployment-id": deployment.id.toString(), + "app.kubernetes.io/name": app.name, + "app.kubernetes.io/managed-by": "anvilops", + }; + if (migrating) { + // When migrating off AnvilOps, remove the labels by setting their values to null + const deletedLabels = Object.keys(labels).reduce( + (deleted, key) => ({ ...deleted, [key]: null }), + {}, + ); + applyLabels(namespace, deletedLabels); + for (let config of configs) { + applyLabels(config, deletedLabels); + } + } else { const netpol = createIngressNetPol({ name: params.name, namespace: params.namespace, @@ -301,14 +317,6 @@ export const createAppConfigsFromDeployment = async ({ configs.push(netpol); } - const labels = { - ...groupLabels, - "anvilops.rcac.purdue.edu/app-id": app.id.toString(), - "anvilops.rcac.purdue.edu/deployment-id": deployment.id.toString(), - "app.kubernetes.io/name": app.name, - "app.kubernetes.io/managed-by": "anvilops", - }; - applyLabels(namespace, labels); for (let config of configs) { applyLabels(config, labels); @@ -317,32 +325,72 @@ export const createAppConfigsFromDeployment = async ({ const postCreate = async (api: KubernetesObjectApi) => { // Clean up secrets and ingresses from previous deployments of the app - const secrets = (await api - .list("v1", "Secret", namespaceName) - .then((data) => data.items) - .then((data) => - data.map((d) => ({ ...d, apiVersion: "v1", kind: "Secret" })), - )) as (V1Secret & K8sObject)[]; - const ingresses = (await api - .list("networking.k8s.io/v1", "Ingress", namespaceName) - .then((data) => data.items) - .then((data) => - data.map((d) => ({ - ...d, + const outdatedResources = []; + + if (migrating) { + if (env.CREATE_INGRESS_NETPOL) { + // When migrating, AnvilOps-specific labels are removed, so grouping network policies will not work. + // Delete all network policies that are managed by AnvilOps. + const netpols = await api + .list("networking.k8s.io/v1", "NetworkPolicy", namespaceName) + .then((data) => + data.items.map((item) => ({ + ...item, + apiVersion: "networking.k8s.io/v1", + kind: "NetworkPolicy", + })), + ); + + outdatedResources.push( + ...netpols.filter( + (netpol) => + netpol.metadata.labels?.["app.kubernetes.io/managed-by"] === + "anvilops", + ), + ); + } + } else { + const resourceTypes = [ + { + apiVersion: "v1", + kind: "Secret", + }, + { apiVersion: "networking.k8s.io/v1", kind: "Ingress", - })), - )) as (V1Ingress & K8sObject)[]; + }, + ]; + + const resourceLists = await Promise.all( + resourceTypes.map((type) => + api.list(type.apiVersion, type.kind, namespaceName).then((data) => + data.items.map((item) => ({ + ...item, + apiVersion: type.apiVersion, + kind: type.kind, + })), + ), + ), + ); + + outdatedResources.concat( + resourceLists + .flat() + .filter( + (resource) => + parseInt( + resource.metadata.labels?.[ + "anvilops.rcac.purdue.edu/deployment-id" + ], + ) !== deployment.id, + ), + ); + } await Promise.all( - [...secrets, ...ingresses] - .filter( - (secret) => - parseInt( - secret.metadata.labels["anvilops.rcac.purdue.edu/deployment-id"], - ) !== deployment.id, - ) - .map((secret) => api.delete(secret).catch((err) => console.error(err))), + outdatedResources.map((resource) => + api.delete(resource).catch((err) => console.error(err)), + ), ); }; return { namespace, configs, postCreate }; diff --git a/backend/src/service/deleteApp.ts b/backend/src/service/deleteApp.ts index d3ee5a6..e7101b4 100644 --- a/backend/src/service/deleteApp.ts +++ b/backend/src/service/deleteApp.ts @@ -37,7 +37,7 @@ export async function deleteApp( ["KubernetesObjectApi"], ); await deleteNamespace(api, getNamespace(namespace)); - } else if (config.appType === "workload" && config.collectLogs) { + } else if (config.appType === "workload") { // If the log shipper was enabled, redeploy without it config.collectLogs = false; // <-- Disable log shipping @@ -54,6 +54,7 @@ export async function deleteApp( appGroup, deployment: lastDeployment, config, + migrating: true, // Deploy without any anvilops-related labels }); const { KubernetesObjectApi: api } = await getClientsForRequest( From 11a9d7f3df5573ef1e328cce4701379878fe8f18 Mon Sep 17 00:00:00 2001 From: zheng861 Date: Wed, 21 Jan 2026 13:26:04 -0500 Subject: [PATCH 4/8] Document network pollicy options --- charts/anvilops/README.md | 32 ++++++++++++++++++++++++++++++++ charts/anvilops/values.yaml | 6 +++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/charts/anvilops/README.md b/charts/anvilops/README.md index 9efe579..6860b56 100644 --- a/charts/anvilops/README.md +++ b/charts/anvilops/README.md @@ -50,6 +50,9 @@ If an option has a ⭐ beside it, you will likely have to change it to fit your | | **App ingress configuration** | _These values influence the generated `Ingress` configuration created for every app._ | | | ⭐ | `anvilops.apps.ingress.className` | Ingress class name | nginx | | | `anvilops.apps.ingress.annotations` | Annotations added to end users' `Ingress` resources. | | +| | **App network policy configuration** | _These values control NetworkPolicy resources created for tenant apps to restrict ingress traffic._ | | +| ⭐ | `anvilops.apps.netpol.createIngressNetworkPolicy` | If `true`, creates NetworkPolicy resources in tenant namespaces that restrict ingress traffic. Requires `allowedIngressMatchLabels` to be configured. | false | +| ⭐ | `anvilops.apps.netpol.allowedIngressMatchLabels` | A list of label selectors that specify which namespaces are allowed to send ingress traffic to tenant apps. Apps in the same app group are always allowed. See notes below for important considerations. | `[{kubernetes.io/metadata.name: kube-system}, {kubernetes.io/metadata.name: ingress-nginx}]` | | | **Postgres configuration** | _AnvilOps uses Postgres to store all user data except container images._ | | | | `postgres.generateCredentials` | Automatically generate a password and field encryption secret and store them in a secret called `postgres-credentials`. If you set this to `false`, populate that secret with random values in the `password` and `field-encryption-key` keys. | true | | | `postgres.image` | The image to use for the Postgres deployment. | postgres:17 | @@ -108,6 +111,35 @@ For example, if your `appDomain` was `https://example.com` (users' apps would be - Domain: `*.example.com` - IP: the public IP address of your ingress controller +## Network Policy Configuration + +When `anvilops.apps.netpol.createIngressNetworkPolicy` is `true` and `anvilops.apps.netpol.allowedIngressMatchLabels` is configured, AnvilOps creates NetworkPolicy resources in tenant namespaces to restrict ingress. These policies allowlist ingress from: + +1. Namespaces matching any of the label selectors specified in `allowedIngressMatchLabels` +2. Apps within the same app group + +All other ingress traffic is blocked by default. + +### Configuring `allowedIngressMatchLabels` + +The `allowedIngressMatchLabels` field accepts a list of label selectors identifying namespaces to allow traffic from. For example, this default configuration allowlists traffic from the namespaces `kube-system` and `ingress-nginx`. + +```yaml +allowedIngressMatchLabels: + - kubernetes.io/metadata.name: kube-system + - kubernetes.io/metadata.name: ingress-nginx +``` + +- **The network policy will block ingress from all namespaces that do not have an allowlisted label and are not part of the app group.** This could include many system services. + +- If `createIngressNetworkPolicy` is set to `true`, make sure to include all namespaces that need to communicate with tenant apps in `allowedIngressMatchLabels`. + +### Rancher Project Isolation + +When project isolation is enabled, it may be sufficient to set `allowedIngressMatchLabels` to an empty list(`[]`). This is because Rancher will create network policies in each namespace to allow traffic from the System project(as well as the current project). + +Setting `createIngressNetworkPolicy: true` will still ensure that apps that are in the same group can communicate across projects. + ## Required Secrets Every secret in this section must exist for AnvilOps to run. diff --git a/charts/anvilops/values.yaml b/charts/anvilops/values.yaml index 21cf9e9..cb505ba 100644 --- a/charts/anvilops/values.yaml +++ b/charts/anvilops/values.yaml @@ -93,9 +93,9 @@ anvilops: # kubernetes.io/tls-acme: "true" netpol: createIngressNetworkPolicy: false - # allowedIngressMatchLabels: - # - kubernetes.io/metadata.name: kube-system - # - kubernetes.io/metadata.name: ingress-nginx + allowedIngressMatchLabels: + - kubernetes.io/metadata.name: kube-system + - kubernetes.io/metadata.name: ingress-nginx postgres: generateCredentials: true From 06d5b40a7a728c2cc5374fac9afbae528720a6e9 Mon Sep 17 00:00:00 2001 From: zheng861 Date: Wed, 21 Jan 2026 14:00:46 -0500 Subject: [PATCH 5/8] Fix ttl --- charts/anvilops/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/anvilops/values.yaml b/charts/anvilops/values.yaml index 5d1ffb2..89fd78e 100644 --- a/charts/anvilops/values.yaml +++ b/charts/anvilops/values.yaml @@ -99,7 +99,7 @@ rancher: apiBase: "" refreshTokens: true refreshSchedule: "0 0 25 * *" - tokenTtl: 2592000000 # 30 days + tokenTtl: "2592000000" # 30 days postgres: generateCredentials: true From eb2d952ae40b22e037049b766fea11e419b23214 Mon Sep 17 00:00:00 2001 From: zheng861 Date: Wed, 21 Jan 2026 14:11:13 -0500 Subject: [PATCH 6/8] Quote ttl --- charts/anvilops/templates/jobs/rotate-rancher-credentials.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/anvilops/templates/jobs/rotate-rancher-credentials.yaml b/charts/anvilops/templates/jobs/rotate-rancher-credentials.yaml index 3269f09..cf8501c 100644 --- a/charts/anvilops/templates/jobs/rotate-rancher-credentials.yaml +++ b/charts/anvilops/templates/jobs/rotate-rancher-credentials.yaml @@ -29,7 +29,7 @@ spec: - name: RANCHER_SECRET_NAME value: rancher-config - name: RANCHER_TOKEN_TTL - value: {{ .Values.rancher.tokenTtl }} + value: {{ .Values.rancher.tokenTtl | quote }} {{- if .Values.anvilops.serviceAccount.useKubeconfig }} - name: KUBECONFIG value: /opt/creds/kubeconfig From 0fc5356d6e88028fae6ce35adc30a93e0a153fab Mon Sep 17 00:00:00 2001 From: zheng861 Date: Wed, 21 Jan 2026 14:41:34 -0500 Subject: [PATCH 7/8] Remove default ingress match labels and log missing allowlist --- backend/src/lib/cluster/resources.ts | 10 +++++++++- charts/anvilops/README.md | 4 ++-- charts/anvilops/values.yaml | 6 +++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/src/lib/cluster/resources.ts b/backend/src/lib/cluster/resources.ts index 79ed96f..c7bac1d 100644 --- a/backend/src/lib/cluster/resources.ts +++ b/backend/src/lib/cluster/resources.ts @@ -14,6 +14,7 @@ import type { Organization, WorkloadConfig, } from "../../db/models.ts"; +import { logger } from "../../index.ts"; import { env } from "../env.ts"; import { getOctokit } from "../octokit.ts"; import { createIngressConfig } from "./resources/ingress.ts"; @@ -160,7 +161,14 @@ const createIngressNetPol = ({ namespace: string; groupLabels: { [key: string]: string }; }): V1NetworkPolicy & K8sObject => { - if (!env.CREATE_INGRESS_NETPOL || !env.ALLOW_INGRESS_FROM) { + if (!env.CREATE_INGRESS_NETPOL) { + return null; + } + + if (!env.ALLOW_INGRESS_FROM) { + logger.warn( + "ALLOW_INGRESS_FROM is not set, skipping network policy creation", + ); return null; } diff --git a/charts/anvilops/README.md b/charts/anvilops/README.md index 73601a5..291e1fd 100644 --- a/charts/anvilops/README.md +++ b/charts/anvilops/README.md @@ -52,7 +52,7 @@ If an option has a ⭐ beside it, you will likely have to change it to fit your | | `anvilops.apps.ingress.annotations` | Annotations added to end users' `Ingress` resources. | | | | **App network policy configuration** | _These values control NetworkPolicy resources created for tenant apps to restrict ingress traffic._ | | | ⭐ | `anvilops.apps.netpol.createIngressNetworkPolicy` | If `true`, creates NetworkPolicy resources in tenant namespaces that restrict ingress traffic. Requires `allowedIngressMatchLabels` to be configured. | false | -| ⭐ | `anvilops.apps.netpol.allowedIngressMatchLabels` | A list of label selectors that specify which namespaces are allowed to send ingress traffic to tenant apps. Apps in the same app group are always allowed. See notes below for important considerations. | `[{kubernetes.io/metadata.name: kube-system}, {kubernetes.io/metadata.name: ingress-nginx}]` | +| ⭐ | `anvilops.apps.netpol.allowedIngressMatchLabels` | A list of label selectors that specify which namespaces are allowed to send ingress traffic to tenant apps. Apps in the same app group are always allowed. See notes below for important considerations. | | | | **Rancher configuration** | _AnvilOps can integrate with Rancher to deploy applications inside Projects._ | | | ⭐ | `rancher.enabled` | Enable Rancher integrations. | false | | ⭐ | `rancher.apiBase` | Base URL of the Rancher v3 API. | | @@ -130,7 +130,7 @@ All other ingress traffic is blocked by default. ### Configuring `allowedIngressMatchLabels` -The `allowedIngressMatchLabels` field accepts a list of label selectors identifying namespaces to allow traffic from. For example, this default configuration allowlists traffic from the namespaces `kube-system` and `ingress-nginx`. +The `allowedIngressMatchLabels` field accepts a list of label selectors identifying namespaces to allow traffic from. For example, this configuration allowlists traffic from `kube-system` and `ingress-nginx`. ```yaml allowedIngressMatchLabels: diff --git a/charts/anvilops/values.yaml b/charts/anvilops/values.yaml index 89fd78e..296fe97 100644 --- a/charts/anvilops/values.yaml +++ b/charts/anvilops/values.yaml @@ -90,9 +90,9 @@ anvilops: # kubernetes.io/tls-acme: "true" netpol: createIngressNetworkPolicy: false - allowedIngressMatchLabels: - - kubernetes.io/metadata.name: kube-system - - kubernetes.io/metadata.name: ingress-nginx + # allowedIngressMatchLabels: + # - kubernetes.io/metadata.name: kube-system + # - kubernetes.io/metadata.name: ingress-nginx rancher: enabled: false From 372d106d6caa081caa975ed42dc3cffd451daa28 Mon Sep 17 00:00:00 2001 From: FluxCapacitor2 <31071265+FluxCapacitor2@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:49:41 -0500 Subject: [PATCH 8/8] Make templating fail if createIngressNetworkPolicy is enabled but allowedIngressMatchLabels has no value --- .../templates/anvilops/anvilops-deployment.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/charts/anvilops/templates/anvilops/anvilops-deployment.yaml b/charts/anvilops/templates/anvilops/anvilops-deployment.yaml index caaf11e..25badbc 100644 --- a/charts/anvilops/templates/anvilops/anvilops-deployment.yaml +++ b/charts/anvilops/templates/anvilops/anvilops-deployment.yaml @@ -76,7 +76,7 @@ spec: value: /etc/ssl/certs/anvilops-tls.crt {{- end }} - name: ANVILOPS_VERSION - value: {{ .Chart.Version }} + value: {{ .Chart.Version | quote }} - name: CLUSTER_CONFIG_PATH value: /opt/config/cluster.json - name: POSTGRES_USER @@ -118,7 +118,7 @@ spec: name: session-secret key: secret - name: BASE_URL - value: {{ .Values.anvilops.env.baseURL }} + value: {{ .Values.anvilops.env.baseURL | quote }} - name: GITHUB_WEBHOOK_SECRET valueFrom: secretKeyRef: @@ -162,7 +162,7 @@ spec: {{- if .Values.rancher.enabled }} {{- with .Values.rancher.apiBase }} - name: RANCHER_API_BASE - value: {{ . }} + value: {{ . | quote }} {{- end }} - name: RANCHER_TOKEN valueFrom: @@ -224,6 +224,8 @@ spec: {{- with .Values.anvilops.apps.netpol.allowedIngressMatchLabels }} - name: ALLOW_INGRESS_FROM value: {{ toJson . | quote }} + {{- else -}} + {{- fail "anvilops.apps.netpol.createIngressNetworkPolicy is enabled, but anvilops.apps.netpol.allowedIngressMatchLabels does not have a value." -}} {{- end }} {{- end }} - name: STORAGE_CLASS_NAME @@ -237,7 +239,7 @@ spec: - name: CHART_PROJECT_NAME value: {{ .Values.anvilops.env.harborChartRepoName }} - name: ALLOW_HELM_DEPLOYMENTS - value: "{{ .Values.anvilops.env.allowHelmDeployments }}" + value: {{ .Values.anvilops.env.allowHelmDeployments | quote }} - name: BUILDKITD_ADDRESS value: {{ .Values.buildkitd.address }} - name: FILE_BROWSER_IMAGE