Те, кто использует Istio для управления микросервисами, не должны быть слишком удивлены его суперспособностями. Работая с различными командами и организациями в рамках предприятия, я должен признать, что Istio иногда может быть очень сложным. К счастью, Google, Solo и многие другие компании продолжают внедрять инновации в этой области, чтобы вы, как конечный пользователь, могли извлечь из этого пользу.
Согласно ZTA, мы никогда не должны доверять, всегда проверяйте.
В этой статье я хочу затронуть вопрос о том, как использовать PaC (он же Policy-As-Code) для обеспечения правильной реализации Istio (чтобы было понятно, что нет абсолютной правильности или неправильности, но следуя лучшим практикам, вы достигаете правильности на данный момент), например, Protocol Selection. По умолчанию Istio может автоматически определять HTTP(/2) трафик, иначе он будет рассматриваться как обычный TCP трафик. Как учит нас Zen of Python, явное лучше неявного. Мы всегда должны стремиться к удобочитаемости и сопровождаемости кода. Поэтому давайте обеспечим соблюдение этого правила во время проектирования и во время выполнения.
Эта статья не предназначена для глубокого погружения в Rego, и я оставлю это на усмотрение читателей.
Согласно Istio Explicit Protocol Selection:
Это может быть настроено двумя способами:
По имени порта:
name: <protocol>[-<suffix>]
.
В Kubernetes 1.18+, по полю appProtocol:appProtocol: <protocol>
.
Давайте разберем это.
- порт: имя
Можно, конечно, использовать встроенную функцию re_match, но это может усложнить шаблон ограничений Gatekeeper’s ConstraintTemplate, о котором мы поговорим чуть позже. Вместо этого мы можем просто split
имя_порта на -
, и проверить его принадлежность.
protocol := split(port_name, "-")[0]
protocol in protocols
- порт: appProtocol
Есть несколько способов проверить принадлежность порта:
#1 Unification, which is different from `:=` and `==` in Rego.
protocol = protocols[_]
#2 `in`, which requires to `import future.keywords`
protocol in protocols
#3 Set
protocol_set := { p | p := input.parameters.protocols[_] }
protocol_set[protocol]
Если следовать руководству Styra’s Rego Style Guide, то вариант №2 будет предпочтительным.
Есть одна маленькая тонкость, о которой мы еще не говорили. Когда у сервиса есть и имя порта, и порт appProtocol, последний имеет приоритет в Istio. Итак, как мы выражаем это в Rego?
В Rego внутри правила подразумевается условие AND
, что означает, что все тело правила должно быть True, чтобы это правило стало True. Условие OR
достигается несколькими Правилами с одним и тем же именем Правила.
# when port.appProtocl exists just use it and ignore port name altogether.
_is_valid(port, protocols) {
port.appProtocol
_match_app_protocol(port.appProtocol, protocols)
}
# when port.appProtocol doesn't exit port name has to exist and match the protocols we specified.
_is_valid(port, protocols) {
not port.appProtocol
port.name
_match_port_name(port.name, protocols)
}
Давайте соберем все вместе:
package istio.security.protocolselection
import future.keywords
violation[{"msg": msg}] {
protocols := input.parameters.protocols
some port in input.review.object.spec.ports
not _is_valid(port, protocols)
msg := sprintf("port: %v name or appProtocol is invalid", [port])
}
_is_valid(port, protocols) {
port.appProtocol
_match_app_protocol(port.appProtocol, protocols)
}
_is_valid(port, protocols) {
not port.appProtocol
port.name
_match_port_name(port.name, protocols)
}
_match_app_protocol(protocol, protocols) {
protocol in protocols
}
_match_port_name(port_name, protocols) {
protocol := split(port_name, "-")[0]
protocol in protocols
}
Теперь самое сложное решено, и давайте обратимся к OPA Gatekeeper. Gatekeeper использует OPA Constraint Framework для описания и применения политики. На данный момент есть в основном 3 части, на которые мы должны обратить внимание:
- ContraintTemplate: описывает как Rego, который обеспечивает выполнение ограничения, так и схему ограничения.
- Constraint: описывает, что и как должно быть реализовано в ContraintTemplate.
- Config: описывает поведение для определенных процессов.
1: ContraintTemplate
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
annotations:
description: Explicit protocol selection either by name or appProtocol
name: istioexplicitprotocolselection
spec:
crd:
spec:
names:
kind: IstioExplicitProtocolSelection
validation:
openAPIV3Schema:
type: object
properties:
prefixes:
type: string
protocols:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |-
package istio.security.protocolselection
import future.keywords
violation[{"msg": msg}] {
protocols := input.parameters.protocols
some port in input.review.object.spec.ports
not _is_valid(port, protocols)
msg := sprintf("port: %v name or appProtocol is invalid", [port])
}
_is_valid(port, protocols) {
port.appProtocol
_match_app_protocol(port.appProtocol, protocols)
}
_is_valid(port, protocols) {
not port.appProtocol
port.name
_match_port_name(port.name, protocols)
}
_match_app_protocol(protocol, protocols) {
protocol in protocols
}
_match_port_name(port_name, protocols) {
protocol := split(port_name, "-")[0]
protocol in protocols
}
2: Ограничение
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: IstioExplicitProtocolSelection
metadata:
name: explicitprotocolselection
spec:
enforcementAction: deny
match:
kinds:
- apiGroups:
- ""
kinds:
- Service
parameters:
protocols:
- http
- https
- http2
- grpc
- grpc-web
- tcp
- tls
3: Конфигурация
Мы будем использовать его для игнорирования всех пространств имен, к которым мы не хотим применять политику.
apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata:
name: config
namespace: "gatekeeper-system"
spec:
match:
- excludedNamespaces: ["kube-*", "istio-*"]
processes: ["*"]
Вуаля!
OPA Gatekeeper имеет больше, чем я только что показал вам здесь. Я оставлю это на ваше усмотрение: Gator, Mutation, Audit, Replication и т.д.
Ссылка: https://github.com/feiyao/gatekeeper-istio