|
| 1 | +# 121 - Enforce minimum access rights on confidential files |
| 2 | + |
| 3 | +Kroxylicious should validate that confidential files (TLS private keys, keystores, truststores, |
| 4 | +password files, and KMS credential files) have suitably restrictive filesystem permissions before |
| 5 | +reading them, similar to how the `ssh` command refuses to use a world-readable private key. |
| 6 | + |
| 7 | +## Current situation |
| 8 | + |
| 9 | +Kroxylicious reads confidential material from the filesystem - TLS private keys, keystores, |
| 10 | +truststores, and passwords - without checking whether those files are accessible by users other |
| 11 | +than the owner. A world-readable private key file (`0644`) is silently accepted and used. This |
| 12 | +violates the principle of least privilege and increases the risk of credential exposure through |
| 13 | +over-permissive filesystem configurations. |
| 14 | + |
| 15 | +## Motivation |
| 16 | + |
| 17 | +Security best practices require that private key material is accessible only to the process that |
| 18 | +owns it. Tools such as `ssh`, `gpg`, and many TLS libraries enforce this by refusing to operate |
| 19 | +on files with group or other read bits set. Kroxylicious should provide equivalent protection. |
| 20 | + |
| 21 | +The threat model includes: |
| 22 | +- Accidental over-permissive file creation (e.g. default `umask` producing `0644`). |
| 23 | +- Multi-tenant environments where other users on the same host could read Kroxylicious credentials. |
| 24 | +- Kubernetes deployments where secret volumes default to world-readable `0644` unless explicitly |
| 25 | + configured otherwise. |
| 26 | + |
| 27 | +## Proposal |
| 28 | + |
| 29 | +### File permission policy |
| 30 | + |
| 31 | +Introduce a configurable `security.filePermissions.policy` setting with three modes: |
| 32 | + |
| 33 | +- `STRICT` - files must be owner-only (equivalent to `chmod 400` or `chmod 600`). Any group or |
| 34 | + other read/write/execute bits cause an `IllegalStateException` at startup. Mirrors SSH behaviour. |
| 35 | +- `RELAXED` - other-user bits are forbidden, but group bits are permitted. This supports |
| 36 | + Kubernetes deployments where `fsGroup` is used to grant a specific GID read access to mounted |
| 37 | + secrets (e.g. `defaultMode: 0440`). |
| 38 | +- `DISABLED` - no enforcement. A warning is logged for files that would be rejected by other |
| 39 | + policies, but startup is never rejected. This is the default for backward compatibility; see |
| 40 | + the [Delivery](#delivery) section for how the default will eventually change to `STRICT`. |
| 41 | + |
| 42 | +### Scope of enforcement |
| 43 | + |
| 44 | +The policy applies before reading any of the following: |
| 45 | + |
| 46 | +- TLS private key files (`key.privateKeyFile`) |
| 47 | +- TLS keystore and truststore files (`key.storeFile`, `trust.storeFile`) |
| 48 | +- Password files referenced by `FilePassword` providers (including KMS credentials) |
| 49 | +- AWS IRSA web identity token files |
| 50 | +- AWS EKS Pod Identity authorization token files |
| 51 | + |
| 52 | +### Configuration |
| 53 | + |
| 54 | +```yaml |
| 55 | +--- |
| 56 | +management: |
| 57 | + # ... |
| 58 | +virtualClusters: |
| 59 | + - name: "one" |
| 60 | + targetCluster: |
| 61 | + # ... |
| 62 | + gateways: |
| 63 | + # ... |
| 64 | +security: |
| 65 | + filePermissions: |
| 66 | + policy: "STRICT" |
| 67 | +``` |
| 68 | +
|
| 69 | +### Global policy propagation |
| 70 | +
|
| 71 | +`FilePermissionValidator` holds a static `AtomicReference<Policy>` that is set once when |
| 72 | +`Configuration` is constructed. This allows `FilePassword` and KMS credential providers - which |
| 73 | +live in modules that do not depend on `kroxylicious-runtime` - to enforce the configured policy |
| 74 | +without requiring changes to their public interfaces. |
| 75 | + |
| 76 | +### Kubernetes operator |
| 77 | + |
| 78 | +The operator mounts all secret volumes with `defaultMode: 0440` (group-readable, no world |
| 79 | +access). |
| 80 | + |
| 81 | +On plain Kubernetes, it sets `fsGroup` and `runAsGroup` to the |
| 82 | +[Kroxylicious Dockerfile](kroxylicious-app/src/main/docker/proxy.dockerfile) GID (185) |
| 83 | +so the kubelet chowns volume files to that GID and the container process can read them via group |
| 84 | +membership. |
| 85 | + |
| 86 | +On OpenShift, |
| 87 | +[`fsGroup` and `runAsGroup` are omitted](https://www.redhat.com/en/blog/a-guide-to-openshift-and-uids); |
| 88 | +[the `restricted-v2` SCC's `MustRunAs` strategy injects the namespace-allocated GID automatically](https://docs.redhat.com/en/documentation/openshift_container_platform/4.22/html/authentication_and_authorization/managing-pod-security-policies#security-context-constraints-example_configuring-internal-oauth). |
| 89 | + |
| 90 | +The `KafkaProxy` CRD exposes `spec.security.filePermissions.policy` (default `RELAXED`) to allow |
| 91 | +users to override the policy. |
| 92 | + |
| 93 | +## Affected/not affected projects |
| 94 | + |
| 95 | +**Affected:** |
| 96 | +- `kroxylicious-security` - new module; contains `FilePermissionValidator` and `FilePermissionConfig` |
| 97 | +- `kroxylicious-api` - `FilePassword.getProvidedPassword()` now validates permissions |
| 98 | +- `kroxylicious-runtime` - `Configuration`, `NettyKeyProvider`, `NettyTrustProvider`, `VirtualClusterModel`, `ServerConnectionStateMachine` |
| 99 | +- `kroxylicious-kms-providers` / `kroxylicious-kms-provider-aws-kms` - IRSA and Pod Identity providers validate their token files |
| 100 | +- `kroxylicious-kubernetes` / `kroxylicious-operator` - secret volume `defaultMode`, conditional `fsGroup`, `KafkaProxy` CRD field |
| 101 | + |
| 102 | +**Not affected:** |
| 103 | +- `kroxylicious-filters` - no file reading |
| 104 | +- `kroxylicious-authorizer-api`, `kroxylicious-authorizer-providers` - no file reading |
| 105 | +- KMS providers other than AWS (vault token, Azure, Fortanix) - covered transitively via `FilePassword` |
| 106 | + |
| 107 | +## Delivery |
| 108 | + |
| 109 | +In order to comply with the project's [deprecation policy](https://github.com/kroxylicious/kroxylicious/blob/main/DEV_GUIDE.md#deprecation-policy), |
| 110 | +the change in default policy should be staged across two releases. |
| 111 | + |
| 112 | +**Stage 1 (this proposal):** Introduce the feature with `DISABLED` as the default for backward |
| 113 | +compatibility. When `DISABLED` is the effective policy, Kroxylicious logs a `WARN` for every |
| 114 | +confidential file whose permissions would be rejected by `STRICT`. This gives users a |
| 115 | +deprecation period to identify and harden their file permissions. The deprecation of `DISABLED` |
| 116 | +as the default should be announced in the `CHANGELOG` under "Changes, deprecations and removals". |
| 117 | + |
| 118 | +**Stage 2 (subsequent release, following the deprecation policy):** Change the default to `STRICT`. |
| 119 | +This will be a breaking change for deployments that have confidential files with overly permissive |
| 120 | +permissions and have not explicitly configured a policy. Deployments that set |
| 121 | +`security.filePermissions.policy: DISABLED` explicitly will be unaffected. |
| 122 | +The change in default should be documented in the `CHANGELOG` as a breaking change. |
| 123 | + |
| 124 | +## Compatibility |
| 125 | + |
| 126 | +### Backward compatibility |
| 127 | + |
| 128 | +The default policy in Stage 1 is `DISABLED`, so existing deployments are unaffected. Warnings are |
| 129 | +emitted for insecure files to assist users in identifying files to harden before the default |
| 130 | +changes to `STRICT` in Stage 2. |
| 131 | + |
| 132 | +### `FilePassword.getProvidedPassword()` behaviour change |
| 133 | + |
| 134 | +With a non-`DISABLED` policy, `FilePassword.getProvidedPassword()` can now throw |
| 135 | +`IllegalStateException` if the password file has group or other read bits set. This is an |
| 136 | +unchecked exception that did not previously occur. Filter authors using `FilePassword` directly |
| 137 | +should be aware of this. |
| 138 | + |
| 139 | +### API additions |
| 140 | + |
| 141 | +`FilePermissionValidator` and `FilePermissionConfig` in the new `kroxylicious-security` module |
| 142 | +become accessible to consumers of `kroxylicious-api` (which depends on `kroxylicious-security`). |
| 143 | +`FilePermissionValidator.setGlobalPolicy()` is necessarily public because `Configuration` (in |
| 144 | +`kroxylicious-runtime`) and `FilePermissionValidator` (in `kroxylicious-security`) are in |
| 145 | +different modules and Java's access control cannot express "accessible to exactly one other module" |
| 146 | +without JPMS. This is a known design limitation: third-party code could in principle call |
| 147 | +`setGlobalPolicy()` and alter the global policy for all validations. A future improvement could |
| 148 | +adopt JPMS module encapsulation to restrict the method to the `kroxylicious.runtime` module only. |
| 149 | + |
| 150 | +## Rejected alternatives |
| 151 | + |
| 152 | +### Move `FilePermissionValidator` to `kroxylicious-runtime` |
| 153 | + |
| 154 | +`FilePassword` (in `kroxylicious-api`) needs to call the validator to enforce permissions before |
| 155 | +reading a password file. `kroxylicious-runtime` already depends on `kroxylicious-api`, so |
| 156 | +`kroxylicious-api` depending back on `kroxylicious-runtime` would create a circular dependency. |
| 157 | +The validator therefore cannot live in `kroxylicious-runtime` if `FilePassword` is to use it |
| 158 | +directly. |
| 159 | + |
| 160 | +### Move `FilePermissionValidator` to `kroxylicious-api` |
| 161 | + |
| 162 | +Moving the validator directly to `kroxylicious-api` (rather than creating `kroxylicious-security`) |
| 163 | +was considered. Rejected because `kroxylicious-api` is conceptually a contract/interface layer; |
| 164 | +adding a logging-heavy implementation utility (with SLF4J, `AtomicBoolean`, `ConcurrentHashMap`) |
| 165 | +would pollute the API module with infrastructure concerns. A dedicated `kroxylicious-security` |
| 166 | +module is the right architectural home. |
| 167 | + |
| 168 | +### System property override |
| 169 | + |
| 170 | +A JVM system property (`-Dkroxylicious.security.filePermissionPolicy=STRICT`) was considered as |
| 171 | +a secondary configuration mechanism alongside the YAML field. Rejected because system properties |
| 172 | +are undiscoverable, untestable, and bypass the normal configuration validation path. The YAML |
| 173 | +field is the right single mechanism. |
0 commit comments