Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 6 additions & 2 deletions build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@
# 101 is nginx
USER 101

LABEL org.opencontainers.image.version="${IC_VERSION}" \

Check warning on line 611 in build/Dockerfile

View workflow job for this annotation

GitHub Actions / Build Artifacts / Build Docker OSS (ubi, linux/arm64, linux/amd64) / OSS ubi linux/arm64, linux/amd64

Variables should be defined before their use

UndefinedVar: Usage of undefined variable '$NGINX_VERSION' More info: https://docs.docker.com/go/dockerfile/rule/undefined-var/
org.opencontainers.image.documentation=https://docs.nginx.com/nginx-ingress-controller \
org.opencontainers.image.vendor="NGINX Inc <[email protected]>" \
org.nginx.kic.image.build.target="${TARGETPLATFORM}" \
Expand Down Expand Up @@ -662,8 +662,12 @@
COPY --link --chown=101:0 nginx-ingress /
# root is required for `setcap` invocation
USER 0
RUN --mount=type=bind,target=/tmp [ -z "${BUILD_OS##*plus*}" ] && PLUS=-plus; cp -a /tmp/internal/configs/version1/nginx$PLUS.ingress.tmpl /tmp/internal/configs/version1/nginx$PLUS.tmpl \
/tmp/internal/configs/version2/nginx$PLUS.virtualserver.tmpl /tmp/internal/configs/version2/nginx$PLUS.transportserver.tmpl / \
RUN --mount=type=bind,target=/tmp if [ -z "${BUILD_OS##*plus*}" ]; then PLUS=-plus; fi \
&& cp -a /tmp/internal/configs/version1/nginx$PLUS.ingress.tmpl \
/tmp/internal/configs/version1/nginx$PLUS.tmpl \
/tmp/internal/configs/version2/nginx$PLUS.virtualserver.tmpl \
/tmp/internal/configs/version2/nginx$PLUS.transportserver.tmpl / \
&& if [ -z "${BUILD_OS##*plus*}" ]; then cp -a /tmp/internal/configs/version2/oidc.tmpl /; fi \
&& chown -R 101:0 /*.tmpl \
&& chmod -R g=u /*.tmpl \
&& setcap 'cap_net_bind_service=+ep' /nginx-ingress && setcap -v 'cap_net_bind_service=+ep' /nginx-ingress
Expand Down
3 changes: 2 additions & 1 deletion build/scripts/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ set -e

PLUS=""
if [ -z "${BUILD_OS##*plus*}" ]; then
mkdir -p /etc/nginx/oidc/
mkdir -p /etc/nginx/oidc/ /etc/nginx/oidc-conf.d/
cp -a /code/internal/configs/oidc/* /etc/nginx/oidc/
mkdir -p /etc/nginx/state_files/
mkdir -p /etc/nginx/reporting/
mkdir -p /etc/nginx/secrets/mgmt/
PLUS=-plus
cp -a /code/internal/configs/version2/oidc.tmpl /
fi

mkdir -p /etc/nginx/njs/ && cp -a /code/internal/configs/njs/* /etc/nginx/njs/
Expand Down
12 changes: 11 additions & 1 deletion cmd/nginx-ingress/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const (
socketPath = "/var/lib/nginx"
fatalEventFlushTime = 200 * time.Millisecond
secretErrorReason = "SecretError"
fileErrorReason = "FileError"
configMapErrorReason = "ConfigMapError"
)

Expand Down Expand Up @@ -191,6 +192,12 @@ func main() {
if err != nil {
logEventAndExit(ctx, eventRecorder, pod, secretErrorReason, err)
}

caBundlePath, err := nginxManager.GetOSCABundlePath()
if err != nil {
logEventAndExit(ctx, eventRecorder, pod, fileErrorReason, err)
}

globalConfigurationValidator := createGlobalConfigurationValidator()

mustProcessGlobalConfiguration(ctx)
Expand Down Expand Up @@ -226,6 +233,7 @@ func main() {
StaticSSLPath: staticSSLPath,
NginxVersion: nginxVersion,
AppProtectBundlePath: appProtectBundlePath,
DefaultCABundle: caBundlePath,
}

if *nginxPlus {
Expand Down Expand Up @@ -541,11 +549,13 @@ func createTemplateExecutors(ctx context.Context) (*version1.TemplateExecutor, *
nginxIngressTemplatePath := "nginx.ingress.tmpl"
nginxVirtualServerTemplatePath := "nginx.virtualserver.tmpl"
nginxTransportServerTemplatePath := "nginx.transportserver.tmpl"
nginxOIDCConfTemplatePath := ""
if *nginxPlus {
nginxConfTemplatePath = "nginx-plus.tmpl"
nginxIngressTemplatePath = "nginx-plus.ingress.tmpl"
nginxVirtualServerTemplatePath = "nginx-plus.virtualserver.tmpl"
nginxTransportServerTemplatePath = "nginx-plus.transportserver.tmpl"
nginxOIDCConfTemplatePath = "oidc.tmpl"
}

if *mainTemplatePath != "" {
Expand All @@ -566,7 +576,7 @@ func createTemplateExecutors(ctx context.Context) (*version1.TemplateExecutor, *
nl.Fatalf(l, "Error creating TemplateExecutor: %v", err)
}

templateExecutorV2, err := version2.NewTemplateExecutor(nginxVirtualServerTemplatePath, nginxTransportServerTemplatePath)
templateExecutorV2, err := version2.NewTemplateExecutor(nginxVirtualServerTemplatePath, nginxTransportServerTemplatePath, nginxOIDCConfTemplatePath)
if err != nil {
nl.Fatalf(l, "Error creating TemplateExecutorV2: %v", err)
}
Expand Down
19 changes: 19 additions & 0 deletions config/crd/bases/k8s.nginx.org_policies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -390,10 +390,29 @@ spec:
with a + sign, for example openid+profile+email, openid+email+userDefinedScope.
The default is openid.
type: string
sslVerify:
default: false
description: Enables verification of the IDP server SSL certificate.
Default is false.
type: boolean
sslVerifyDepth:
default: 1
description: Sets the verification depth in the IDP server certificates
chain. The default is 1.
minimum: 0
type: integer
tokenEndpoint:
description: URL for the token endpoint provided by your OpenID
Connect provider.
type: string
trustedCertSecret:
description: The name of the Kubernetes secret that stores the
CA certificate for IDP server verification. It must be in the
same namespace as the Policy resource. The secret must be of
the type nginx.org/ca, and the certificate must be stored in
the secret under the key ca.crt.
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
zoneSyncLeeway:
description: Specifies the maximum timeout in milliseconds for
synchronizing ID/access tokens and shared values between Ingress
Expand Down
19 changes: 19 additions & 0 deletions deploy/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -561,10 +561,29 @@ spec:
with a + sign, for example openid+profile+email, openid+email+userDefinedScope.
The default is openid.
type: string
sslVerify:
default: false
description: Enables verification of the IDP server SSL certificate.
Default is false.
type: boolean
sslVerifyDepth:
default: 1
description: Sets the verification depth in the IDP server certificates
chain. The default is 1.
minimum: 0
type: integer
tokenEndpoint:
description: URL for the token endpoint provided by your OpenID
Connect provider.
type: string
trustedCertSecret:
description: The name of the Kubernetes secret that stores the
CA certificate for IDP server verification. It must be in the
same namespace as the Policy resource. The secret must be of
the type nginx.org/ca, and the certificate must be stored in
the secret under the key ca.crt.
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
zoneSyncLeeway:
description: Specifies the maximum timeout in milliseconds for
synchronizing ID/access tokens and shared values between Ingress
Expand Down
3 changes: 3 additions & 0 deletions docs/crd/k8s.nginx.org_policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ The `.spec` object supports the following fields:
| `oidc.postLogoutRedirectURI` | `string` | URI to redirect to after the logout has been performed. Requires endSessionEndpoint. The default is /_logout. |
| `oidc.redirectURI` | `string` | Allows overriding the default redirect URI. The default is /_codexch. |
| `oidc.scope` | `string` | List of OpenID Connect scopes. The scope openid always needs to be present and others can be added concatenating them with a + sign, for example openid+profile+email, openid+email+userDefinedScope. The default is openid. |
| `oidc.sslVerify` | `boolean` | Enables verification of the IDP server SSL certificate. Default is false. |
| `oidc.sslVerifyDepth` | `integer` | Sets the verification depth in the IDP server certificates chain. The default is 1. |
| `oidc.tokenEndpoint` | `string` | URL for the token endpoint provided by your OpenID Connect provider. |
| `oidc.trustedCertSecret` | `string` | The name of the Kubernetes secret that stores the CA certificate for IDP server verification. It must be in the same namespace as the Policy resource. The secret must be of the type nginx.org/ca, and the certificate must be stored in the secret under the key ca.crt. |
| `oidc.zoneSyncLeeway` | `integer` | Specifies the maximum timeout in milliseconds for synchronizing ID/access tokens and shared values between Ingress Controller pods. The default is 200. |
| `rateLimit` | `object` | The rate limit policy controls the rate of processing requests per a defined key. |
| `rateLimit.burst` | `integer` | Excessive requests are delayed until their number exceeds the burst size, in which case the request is terminated with an error. |
Expand Down
7 changes: 7 additions & 0 deletions examples/common-secrets/keycloak-ca-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: keycloak-ca
type: nginx.org/ca
data:
ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZ4ekNDQTYrZ0F3SUJBZ0lVSnIxb2VDQTcxTmhjQ3VIVmh1NHVQcXNEVDhjd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2N6RUxNQWtHQTFVRUJoTUNTVVV4RFRBTEJnTlZCQWdNQkVOdmNtc3hEVEFMQmdOVkJBY01CRU52Y21zeApDekFKQmdOVkJBb01Ba1kxTVF3d0NnWURWUVFMREFOT1NVTXhLekFwQmdOVkJBTU1JbXRsZVdOc2IyRnJMbVJsClptRjFiSFF1YzNaakxtTnNkWE4wWlhJdWJHOWpZV3d3SGhjTk1qVXhNVEUzTVRJeU9EVTVXaGNOTXpVeE1URTEKTVRJeU9EVTVXakJ6TVFzd0NRWURWUVFHRXdKSlJURU5NQXNHQTFVRUNBd0VRMjl5YXpFTk1Bc0dBMVVFQnd3RQpRMjl5YXpFTE1Ba0dBMVVFQ2d3Q1JqVXhEREFLQmdOVkJBc01BMDVKUXpFck1Da0dBMVVFQXd3aWEyVjVZMnh2CllXc3VaR1ZtWVhWc2RDNXpkbU11WTJ4MWMzUmxjaTVzYjJOaGJEQ0NBaUl3RFFZSktvWklodmNOQVFFQkJRQUQKZ2dJUEFEQ0NBZ29DZ2dJQkFLK1M5cVRXdGZ3YU1GdjI3aU5Ob3FJU2FaMXJ4R0VlZlpTd29ZTCtBZEVpTXhEMgpxN0dvbkc3QlZaOVoxQWpMTlhtcGwyeFF1ZVZBN25RYXpXa2JJazhhQytJOWwwQ1NudXNiK1UwMUV6V0g5MVJ2CnNRM0pFdlV1WXlma3loNnRGeGoyNTJFMTQ3TE1HcTlXOHVlRDNJYnNWVjRZWlBDY0VHN1c3aEYzSTVObllYa1kKYzVKWFFtKzNTODl5V2hWUDg1MHpGTEpTVUFkcHhuMW9qdmFhV3FZWHVrODd4ajU3U1VueGpzR3pTN1dxMDJxVAo0WjVBNENjUGR5Y2Vha2MzcDRLQXVQaHZSYnltK2l1ME13WWFDZ3ZDZnV2eTZLa0l5c1lCL0dUUmhKcFltU2FKCmNsUXFxT3NRTjd6U3crZDlrOFNlSnFONGVCMHVFbmNkSkRwcDJTa05qa3ZRaEhkRllncm5KN1dZVmZBSmxibkIKSUc3VVBmRCtIZDduRkhFdWIvdE4zN20xRnkyZjhjUnNkNWZ1Q29NNHhoelRucm1wbDFiTEZYN2dOLzgrNGZmZwpzc2VCSmZqbExKbCtzTTA4RlI0aVFicGMzK0xZbkpzOXdsUE82VTBzQ2VJUXB4SnVTUEh5aENtN2hFR2N5NGdKCi9SdENwZHVabitrMDdnTXVnUlVVdUxNV2JNWjhaTEh2cWNwbTliY05aQXNxRERzdkIxMTdldFh4Q1luVC9DRGMKYVJBMVh3a2ptNS9DY0k0U0JqNzQrZC8rMnJDbHBEZGl0dWxyTnJ6WjcrVTZFSU1pdDV3SnJ0V09nVlpYdGNxOAp3UlFWdHZINVNkZjZnWVlvOE9HejhRenAzZEJ5TEVaWDU1V0FzT084dWhhcUJGenk1TEVaSlk1WEo2SlpBZ01CCkFBR2pVekJSTUIwR0ExVWREZ1FXQkJUcmR5VTZzRVhRWHR5S1doRFhVbHJaMmpjQXhEQWZCZ05WSFNNRUdEQVcKZ0JUcmR5VTZzRVhRWHR5S1doRFhVbHJaMmpjQXhEQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BMEdDU3FHU0liMwpEUUVCQ3dVQUE0SUNBUUNTTDNFcDBrWUFBUnIrSStVcjhRdmhkdDg1NUtIbFIwenMyZkN3elFORjArUlFXTm1WCjF5ZitUaEkwRW13NnVaWk5rYXNWenFQcENZNU81VWkwK2ZhL09LaUs1eGdXVjVlais1SEJndFk1VitVVE9yYm0KTlpiODE2UnBya1NadTBFWlpqNVNnUEhmSzUraVNuWnVKL0J2Nk13WjNYR2F1N3pHdlBBRGpqSVBwMEJqczluVwpWUXQyQWRmZlQrUXd1UHgreVJEZTFEdHhpSkEvMlQxN0c3eUN3NnBDWHJsVHdNckk5dTVtSHhmM0FSaVJOZ01mCndPSHJBNXNJYkhFVVQ5QXc3WXp6OFZMNHk0d29la3IzRmwxUjh2OVkzKzJaNmw2TWNIS3FGV2owVkxCU0JFdnAKZzFBSGtZcUdRL2NzbXBOSWMvQ2ZUdWFSdzVsTFZqNlpVMjNmaENsYW9ldWUyejk2U0w5NGltZnFlbUJNOGtwNQp3djNoS3dqdVBsMGxEbkxKd21IOHFMK094L0Y3eTZseWhnSnRVeGtsc0hWQXlPeDlrekM4ZkNZL3BMbDhqc1lvCnh0c29ZMlRhcW1uSEUwNk5KaUk4VVBLd3NzM1M1cUFEV2xzSk4rc2ZURzViVTFZWlVUcjhybi9yc2VlQ1dpOFkKTFdXV3JHeVBjeVd3ZDNJY0pZSVIyWEdXNHNDYkpUdHllMGszRm9WUHV3VHdSVGlkVnVaWjN4OXIzbkpuSk84WAphdkpXa1Z3b0paK1ZRd1AyN1BPc1RFZVR2cFNWMjZkNENJYlZmSnRDZldhd1cybUU5QlozZ1RBbmFuK2pEQTl4CndTektWSW0yYnBIZCtHU0QwUXJvZkNXL2h1OUN2Q2p4aGR0aHlSYzJKOWd2SzFXMjAwZW9HdFl6d0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
8 changes: 8 additions & 0 deletions examples/common-secrets/keycloak-tls-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: keycloak-tls
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZ4ekNDQTYrZ0F3SUJBZ0lVSnIxb2VDQTcxTmhjQ3VIVmh1NHVQcXNEVDhjd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2N6RUxNQWtHQTFVRUJoTUNTVVV4RFRBTEJnTlZCQWdNQkVOdmNtc3hEVEFMQmdOVkJBY01CRU52Y21zeApDekFKQmdOVkJBb01Ba1kxTVF3d0NnWURWUVFMREFOT1NVTXhLekFwQmdOVkJBTU1JbXRsZVdOc2IyRnJMbVJsClptRjFiSFF1YzNaakxtTnNkWE4wWlhJdWJHOWpZV3d3SGhjTk1qVXhNVEUzTVRJeU9EVTVXaGNOTXpVeE1URTEKTVRJeU9EVTVXakJ6TVFzd0NRWURWUVFHRXdKSlJURU5NQXNHQTFVRUNBd0VRMjl5YXpFTk1Bc0dBMVVFQnd3RQpRMjl5YXpFTE1Ba0dBMVVFQ2d3Q1JqVXhEREFLQmdOVkJBc01BMDVKUXpFck1Da0dBMVVFQXd3aWEyVjVZMnh2CllXc3VaR1ZtWVhWc2RDNXpkbU11WTJ4MWMzUmxjaTVzYjJOaGJEQ0NBaUl3RFFZSktvWklodmNOQVFFQkJRQUQKZ2dJUEFEQ0NBZ29DZ2dJQkFLK1M5cVRXdGZ3YU1GdjI3aU5Ob3FJU2FaMXJ4R0VlZlpTd29ZTCtBZEVpTXhEMgpxN0dvbkc3QlZaOVoxQWpMTlhtcGwyeFF1ZVZBN25RYXpXa2JJazhhQytJOWwwQ1NudXNiK1UwMUV6V0g5MVJ2CnNRM0pFdlV1WXlma3loNnRGeGoyNTJFMTQ3TE1HcTlXOHVlRDNJYnNWVjRZWlBDY0VHN1c3aEYzSTVObllYa1kKYzVKWFFtKzNTODl5V2hWUDg1MHpGTEpTVUFkcHhuMW9qdmFhV3FZWHVrODd4ajU3U1VueGpzR3pTN1dxMDJxVAo0WjVBNENjUGR5Y2Vha2MzcDRLQXVQaHZSYnltK2l1ME13WWFDZ3ZDZnV2eTZLa0l5c1lCL0dUUmhKcFltU2FKCmNsUXFxT3NRTjd6U3crZDlrOFNlSnFONGVCMHVFbmNkSkRwcDJTa05qa3ZRaEhkRllncm5KN1dZVmZBSmxibkIKSUc3VVBmRCtIZDduRkhFdWIvdE4zN20xRnkyZjhjUnNkNWZ1Q29NNHhoelRucm1wbDFiTEZYN2dOLzgrNGZmZwpzc2VCSmZqbExKbCtzTTA4RlI0aVFicGMzK0xZbkpzOXdsUE82VTBzQ2VJUXB4SnVTUEh5aENtN2hFR2N5NGdKCi9SdENwZHVabitrMDdnTXVnUlVVdUxNV2JNWjhaTEh2cWNwbTliY05aQXNxRERzdkIxMTdldFh4Q1luVC9DRGMKYVJBMVh3a2ptNS9DY0k0U0JqNzQrZC8rMnJDbHBEZGl0dWxyTnJ6WjcrVTZFSU1pdDV3SnJ0V09nVlpYdGNxOAp3UlFWdHZINVNkZjZnWVlvOE9HejhRenAzZEJ5TEVaWDU1V0FzT084dWhhcUJGenk1TEVaSlk1WEo2SlpBZ01CCkFBR2pVekJSTUIwR0ExVWREZ1FXQkJUcmR5VTZzRVhRWHR5S1doRFhVbHJaMmpjQXhEQWZCZ05WSFNNRUdEQVcKZ0JUcmR5VTZzRVhRWHR5S1doRFhVbHJaMmpjQXhEQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BMEdDU3FHU0liMwpEUUVCQ3dVQUE0SUNBUUNTTDNFcDBrWUFBUnIrSStVcjhRdmhkdDg1NUtIbFIwenMyZkN3elFORjArUlFXTm1WCjF5ZitUaEkwRW13NnVaWk5rYXNWenFQcENZNU81VWkwK2ZhL09LaUs1eGdXVjVlais1SEJndFk1VitVVE9yYm0KTlpiODE2UnBya1NadTBFWlpqNVNnUEhmSzUraVNuWnVKL0J2Nk13WjNYR2F1N3pHdlBBRGpqSVBwMEJqczluVwpWUXQyQWRmZlQrUXd1UHgreVJEZTFEdHhpSkEvMlQxN0c3eUN3NnBDWHJsVHdNckk5dTVtSHhmM0FSaVJOZ01mCndPSHJBNXNJYkhFVVQ5QXc3WXp6OFZMNHk0d29la3IzRmwxUjh2OVkzKzJaNmw2TWNIS3FGV2owVkxCU0JFdnAKZzFBSGtZcUdRL2NzbXBOSWMvQ2ZUdWFSdzVsTFZqNlpVMjNmaENsYW9ldWUyejk2U0w5NGltZnFlbUJNOGtwNQp3djNoS3dqdVBsMGxEbkxKd21IOHFMK094L0Y3eTZseWhnSnRVeGtsc0hWQXlPeDlrekM4ZkNZL3BMbDhqc1lvCnh0c29ZMlRhcW1uSEUwNk5KaUk4VVBLd3NzM1M1cUFEV2xzSk4rc2ZURzViVTFZWlVUcjhybi9yc2VlQ1dpOFkKTFdXV3JHeVBjeVd3ZDNJY0pZSVIyWEdXNHNDYkpUdHllMGszRm9WUHV3VHdSVGlkVnVaWjN4OXIzbkpuSk84WAphdkpXa1Z3b0paK1ZRd1AyN1BPc1RFZVR2cFNWMjZkNENJYlZmSnRDZldhd1cybUU5QlozZ1RBbmFuK2pEQTl4CndTektWSW0yYnBIZCtHU0QwUXJvZkNXL2h1OUN2Q2p4aGR0aHlSYzJKOWd2SzFXMjAwZW9HdFl6d0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRZ0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1N3d2dna29BZ0VBQW9JQ0FRQ3ZrdmFrMXJYOEdqQmIKOXU0alRhS2lFbW1kYThSaEhuMlVzS0dDL2dIUklqTVE5cXV4cUp4dXdWV2ZXZFFJeXpWNXFaZHNVTG5sUU81MApHczFwR3lKUEdndmlQWmRBa3A3ckcvbE5OUk0xaC9kVWI3RU55UkwxTG1NbjVNb2VyUmNZOXVkaE5lT3l6QnF2ClZ2TG5nOXlHN0ZWZUdHVHduQkJ1MXU0UmR5T1RaMkY1R0hPU1YwSnZ0MHZQY2xvVlQvT2RNeFN5VWxBSGFjWjkKYUk3Mm1scW1GN3BQTzhZK2UwbEo4WTdCczB1MXF0TnFrK0dlUU9BbkQzY25IbXBITjZlQ2dMajRiMFc4cHZvcgp0RE1HR2dvTHduN3I4dWlwQ01yR0FmeGswWVNhV0prbWlYSlVLcWpyRURlODBzUG5mWlBFbmlhamVIZ2RMaEozCkhTUTZhZGtwRFk1TDBJUjNSV0lLNXllMW1GWHdDWlc1d1NCdTFEM3cvaDNlNXhSeExtLzdUZCs1dFJjdG4vSEUKYkhlWDdncURPTVljMDU2NXFaZFd5eFYrNERmL1B1SDM0TExIZ1NYNDVTeVpmckROUEJVZUlrRzZYTi9pMkp5YgpQY0pUenVsTkxBbmlFS2NTYmtqeDhvUXB1NFJCbk11SUNmMGJRcVhibVovcE5PNERMb0VWRkxpekZtekdmR1N4Cjc2bktadlczRFdRTEtndzdMd2RkZTNyVjhRbUowL3dnM0drUU5WOEpJNXVmd25DT0VnWSsrUG5mL3Rxd3BhUTMKWXJicGF6YTgyZS9sT2hDRElyZWNDYTdWam9GV1Y3WEt2TUVVRmJieCtVblgrb0dHS1BEaHMvRU02ZDNRY2l4RwpWK2VWZ0xEanZMb1dxZ1JjOHVTeEdTV09WeWVpV1FJREFRQUJBb0lDQUI4YzFtczRoekJBL2MvV0xyWC8xSEdPCi9MdENOUjhXdFo5TE81dklZazhLbGUwTUlUbk96TVhOcWR3ZW9YWGJlTUx4L0J6Y0kwME9XQk1vQ3IxMDZ2d0UKZkJXZjMzVTRaa1A0aFpHYWRhaDNTeXRoellqSldId3RON0lDbDVTZkRLaEdYSk03NXZrd3RRdmNSeGdpcEVvZQppRFF2ODNjMTJLMmpsYlZ2bk5US3JabTFiUW1DUUFvbSs1NnJ2MjNtYUorelJSZ2lnUDhIVGY2OE1CVmdIZTh2CjVqcVRONXFyNHoxZ3VuRDEwbFZEaThwbm9VUVhjQUZMK3N2cVZsLy9hMFl6aEZPMStEQXBrTXg4MXN2ZWdtZzYKRTU3QlFWeHU2K3Z4dnlXb2dTeU94Ymp3QTF3SjRUd2llQllVYlZYUXlZWStsazlDa2xwdFp5VkhlenVFdFZBRwoxSkdDb3NyZkl0eUE3YXdSVXFSMWFwajJPS0Jyd1dROW5nK0dvR0RRNnBLTHdzN1F6TUlkUVNMZGY2SVZMZWE4CjJTZ1AyK0hycUIrR1g5ZlJGdUFPRTRQNGprNGtMT3FTSkR4OENvdkdiS3NSUm1RR1lGL282dFhJYm5zZFVNaUsKZUVuYUVINXRmVWZ1WXNlWTlmR3pTUGFJT1FLeXI4TWJtanp3NFprNE9VUmxRWE41K3kzOGxXREVyb3NybG9VUwpZSkxucU5sVUEvNk96d3RTaCtRenFLeGEzcS9jYndFbU54NDYzOTlzRDNxSndXQlBXcng1REtiSlp4SWtQNFVOCnE1YUVGZW5kY09mZTNxNG9uQm1FQ28weW1zRko0eGNTUFdYUGJtRk91dHJiTnN5R0xDRURlUjNpRmM0Qk12aTYKZURwSHl0MTlXTDloLysyVC9OeG5Bb0lCQVFEd2JLaTd3TUgyWUlUYzBkTi82SEtiaVdJQXVFZkZMbzZSOUxuTwpaWnR2QW5tQTNSTmpJanB3R1BGQ3A0QjdjdEpiVWtIbzlSemdZQ3RKeGF6ZE5wWXRRaFhMRFZ0c0ExZ21zQUo0CmJwcVUrVnhoeTd5R1lzZDNZVDRVcUVRZFNtbHdYRHhuSFlsb0hWd3ExQlRxeWNZSjNYZGV6bE9BS0JzZFR6eGwKZmJGdjJjSXM3aTBUcU1VbzZSd1hVcnhSSU85TGNJL2ZaSWROVmd4MVk2Nm9SaXoxdnlVYVlISUNGM3V6OHl6VgpYc1JoMVZzNFdXVXRhTmlzbXBWTkFnTEtHTUthWGlKNU9pRjA1RWljcUs0bWtNR2lUMFFaZElpNkdyd3dEMkU5Cm5qZjY2bkwycmtGQXRudHpaVkdXMTVwVkUxc2ZJV0UrbHcycGlhSjFESTVNVm4zYkFvSUJBUUM2OHNtWTZFK2QKQmVzTUpuSXhVaXdPUWZ6a256OG5udGVvMzFSb2c5R1dDejBnNlJFVnBRdDZkVU43Wi8yQnZKVUtkNk0yZmlYYwppUmNFRTF6RHpaZ3RMVmhWbG5ZaWZnT1lsbU9NRlRZbHRGUnQ0emgzbUM1TUxOVThVVmhNUjE2cHYvVkF2RmhpCnlxbVNQVG1acjNVZkNKdjZjeFZRbFZJYlVTTVI3bmlSSW13T2JPYUJ3OFpCV0JaOUVCYi90N0ROUVpCV2ZKdTIKejdESXhsUXBxcFM1aWZLRlZpT2pCL0hXcnJ1RHpLUjROdFFDSkRoZERoUlZCWWxsSkpKRE9GV3VCbm5hS3EveQpiZWlrV2dSZmI3YUNSOU5LUVk3aHM4d2dDL0MzNFVBR2lyN2pJNURpQmFKOGVlM0xoMk5GRTI4VWFUTW5mSDNJClFQbVN1MUI2NjJqYkFvSUJBSEZ6QktnY0RDckRYczZJWUtIeHdPcnVDQVhJNzJ6M1RDVkpjc2dYSUNKZzY0N0kKUTFhN0Z4SkFZdEFPRkUyc1grRGh6dUlyajZXOUc1QWpMQy95aXlqdUR6U1NwL292RmRDanEzYkMwa1RMNmpEbgpuNTFXVFVOaTZwVjYxVEZ4SkpIMXBEY1FNLytpSXhTK29PUXR0RHFCZTh1TDF0RVptN25YNHVzTlJjWSszaWF2CmVTdldycnBnVFhZZi8yYlZBTFg3ZHBoMmFuWXV6WkF6S242VEpySUxzV2xoNjBwYlpHOEVwN3BEanEyUHJRekkKK2pwVVNESWllNk1yK0w3K3NnMS9zQXErU0gxTkg0cDAra0NPZkNDb0FMMTJSUEowblNxY2gwazVPTGM1SEdpVQp6NHZHMERnaXJqNWNuS0hha1Z2K04xSCttMTdONkpBTkRiU3Q5NU1DZ2dFQkFLS3R4UG4zSmRoSkh4bEtsMUlOCjVHSmZ6N1lPVVVHaitweHNBcUtVR3B4TG1WejdFeS9YbUI1dXpsTWowYmpFcHBrZU5IdWwyRUtKVk9ycUFtNHMKaVFDL0ZjQWNseDQ2czl4aSthc2JoaXZYT1NVS2RjZTBPSTEyOGZOMEFiY1czK3d0S3ppeTdPTEM0ajVzWXFRMgp4MTlDK2FBOTVzMWhzcm9zcDZ6aDdDNjNXbnBQRDJMYVBybjc4azNQNDRPUWtCeDhzaUpnZW92aFBUL3BQYkdvClM1VU0wbXB1NDhIcGx1dXV6MlBJZjFKUXU3cEZWSHE5VnJvSmdGN3dMUXFyaWZ0T2pWaG9qd1VSMlVDelNGelgKOUdSNEpnZlc5b08zRnFqSVd5ZFhyb1JDMWdzSGx2cm4xbFlsTCtWTklmZ3BDaDhqMEN6TEt4VklYU1R2TlFCUgp1OE1DZ2dFQWJuQ0JSdmtNUnh6S1NkM1V5USszV0tBdnYzVHk2L1phNy8rVVBhZzFRTE5RSksram50L3AvNWpzCkRCRWhPSDhMT2MxTnEzSEpWUVBjV2kwVXo1aDhOWVJwUWx2QWNlRjRTb3d4ZGIrWWRTS1RIN3daVDFwa1dkY0kKWTNib1N0Y0hsOVlCM1Q5STVIZWR4dHgzMytNSkppMXcwVGswK3lNcHBJUmV2VFg3WjVpbnZpbHE3eWJITzd6NwoxbHRHaGJJTjZSblVzcy9mZE1ianJ4N3dreEtqMmlKLzYyM3B1Z0tsakdndUlVR1l3ODNaeUREaGp3ZVJleTlMCmphQVViN01CQ2xsR0NNS3hEZGtsUUN3eHh0YmpySk1QZ0JSbVRjK093QzNwUDNqemJLNlRuQWhnL3JxeEpaYnAKN3hFL0lyd3lxOXJtY3lqZzlsVkh0RHRFN1RDSVRnPT0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=
Loading
Loading