diff --git a/files/policy/filter b/files/policy/filter new file mode 100644 index 0000000..fbd387b --- /dev/null +++ b/files/policy/filter @@ -0,0 +1,211 @@ +# +# Example of forbidding all attempts to login via +# realms. +# +deny_realms { + if (&User-Name && (&User-Name =~ /@|\\/)) { + reject + } +} + +# +# Filter the username +# +# Force some sanity on User-Name. This helps to avoid issues +# issues where the back-end database is "forgiving" about +# what constitutes a user name. +# +filter_username { + if (&User-Name) { + # + # reject mixed case e.g. "UseRNaMe" + # + #if (&User-Name != "%{tolower:%{User-Name}}") { + # reject + #} + + # + # reject all whitespace + # e.g. "user@ site.com", or "us er", or " user", or "user " + # + if (&User-Name =~ / /) { + update request { + &Module-Failure-Message += 'Rejected: User-Name contains whitespace' + } + reject + } + + # + # reject Multiple @'s + # e.g. "user@site.com@site.com" + # + if (&User-Name =~ /@[^@]*@/ ) { + update request { + &Module-Failure-Message += 'Rejected: Multiple @ in User-Name' + } + reject + } + + # + # reject double dots + # e.g. "user@site..com" + # + if (&User-Name =~ /\.\./ ) { + update request { + &Module-Failure-Message += 'Rejected: User-Name contains multiple ..s' + } + reject + } + + # + # must have at least 1 string-dot-string after @ + # e.g. "user@site.com" + # + # if ((&User-Name =~ /@/) && (&User-Name !~ /@(.+)\.(.+)$/)) { + # update request { + # &Module-Failure-Message += 'Rejected: Realm does not have at least one dot separator' + # } + # reject + # } + + # + # Realm ends with a dot + # e.g. "user@site.com." + # + if (&User-Name =~ /\.$/) { + update request { + &Module-Failure-Message += 'Rejected: Realm ends with a dot' + } + reject + } + + # + # Realm begins with a dot + # e.g. "user@.site.com" + # + if (&User-Name =~ /@\./) { + update request { + &Module-Failure-Message += 'Rejected: Realm begins with a dot' + } + reject + } + } +} + +# +# Filter the User-Password +# +# Some equipment sends passwords with embedded zeros. +# This policy filters them out. +# +filter_password { + if (&User-Password && \ + (&User-Password != "%{string:User-Password}")) { + update request { + &Tmp-String-0 := "%{string:User-Password}" + &User-Password := "%{string:Tmp-String-0}" + &Tmp-String-0 !* "" + } + } +} + +filter_inner_identity { + # + # No names, reject. + # + if (!&outer.request:User-Name || !&User-Name) { + update request { + Module-Failure-Message = "User-Name is required for tunneled authentication" + } + reject + } + + # + # Do detailed checks only if the inner and outer + # NAIs are different. + # + # If the NAIs are the same, it violates user privacy, + # but is allowed. + # + if (&outer.request:User-Name != &User-Name) { + # + # Get the outer realm. + # + if (&outer.request:User-Name =~ /@([^@]+)$/) { + update request { + Outer-Realm-Name = "%{1}" + } + + # + # When we have an outer realm name, the user portion + # MUST either be empty, or begin with "anon". + # + # We don't check for the full "anonymous", because + # some vendors don't follow the standards. + # + if (&outer.request:User-Name !~ /^(anon|@)/) { + update request { + Module-Failure-Message = "User-Name is not anonymized" + } + reject + } + } + + # + # There's no outer realm. The outer NAI is different from the + # inner NAI. The User-Name MUST be anonymized. + # + # Otherwise, you could log in as outer "bob", and inner "doug", + # and we'd have no idea which one was correct. + # + elsif (&outer.request:User-Name !~ /^anon/) { + update request { + Module-Failure-Message = "User-Name is not anonymized" + } + reject + } + + # + # Get the inner realm. + # + if (&User-Name =~ /@([^@]+)$/) { + update request { + Inner-Realm-Name = "%{1}" + } + + # + # Note that we do EQUALITY checks for realm names. + # There is no simple way to do case insensitive checks + # on internationalized domain names. There is no reason + # to allow outer "anonymous@EXAMPLE.COM" and inner + # "user@example.com". The user should enter the same + # realm for both identities. + # + # If the inner realm isn't the same as the outer realm, + # the inner realm MUST be a subdomain of the outer realm. + # + if (&Outer-Realm-Name && \ + (&Inner-Realm-Name != &Outer-Realm-Name) && \ + (&Inner-Realm-Name !~ /\.%{Outer-Realm-Name}$/)) { + update request { + Module-Failure-Message = "Inner realm '%{Inner-Realm-Name}' and outer realm '%{Outer-Realm-Name}' are not from the same domain." + } + reject + } + + # + # It's OK to have an inner realm and no outer realm. + # + # That won't work for roaming, but the local RADIUS server + # can still authenticate the user. + # + } + + # + # It's OK to have an outer realm and no inner realm. + # + # It will work for roaming, and the local RADIUS server + # can authenticate the user without the realm. + # + } +} \ No newline at end of file diff --git a/templates/ConfigMap/filter.yaml b/templates/ConfigMap/filter.yaml new file mode 100644 index 0000000..e1f8591 --- /dev/null +++ b/templates/ConfigMap/filter.yaml @@ -0,0 +1,19 @@ +{{- /* +Copyright (c) 2025 Firmansyah Nainggolan. All Rights Reserved. +SPDX-License-Identifier: APACHE-2.0 +*/}} + +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ printf "%s-filter" (include "st-common.names.fullname" .) }} + namespace: {{ include "st-common.names.namespace" . | quote }} + labels: {{- include "st-common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "st-common.tplvalues.render" (dict "value" .Values.commonLabels "context" $) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "st-common.tplvalues.render" (dict "value" .Values.commonAnnotations "context" $) | nindent 4 }} + {{- end }} +data: +{{ (.Files.Glob "files/policy/filter").AsConfig | indent 2 }} diff --git a/templates/Deployment.yaml b/templates/Deployment.yaml index 4af19a5..9afa3e2 100644 --- a/templates/Deployment.yaml +++ b/templates/Deployment.yaml @@ -288,7 +288,10 @@ spec: subPath: queries.conf - name: freeradius-radius-conf mountPath: /etc/freeradius/radius.conf - subPath: radius.conf + subPath: radius.conf + - name: freeradius-filter + mountPath: /etc/freeradius/policy.d/filter + subPath: filter - name: freeradius-sites mountPath: /etc/freeradius/sites-enabled/default subPath: default @@ -339,7 +342,10 @@ spec: name: {{ printf "%s-mods-config" (include "st-common.names.fullname" .) }} - name: freeradius-radius-conf configMap: - name: {{ printf "%s-radius-conf" (include "st-common.names.fullname" .) }} + name: {{ printf "%s-radius-conf" (include "st-common.names.fullname" .) }} + - name: freeradius-filter + configMap: + name: {{ printf "%s-filter" (include "st-common.names.fullname" .) }} - name: temp emptyDir: {} - name: shared-certs