Compare commits
No commits in common. "main" and "cdn-ops-2024-11-13-v07" have entirely different histories.
main
...
cdn-ops-20
11 changed files with 71 additions and 200 deletions
|
@ -1,25 +1,10 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. /conf/init-cdn-db.conf
|
||||
|
||||
# Create database named after user, then create a schema named the same as the
|
||||
# user which is also owned by that user. Because search_path (SHOW
|
||||
# search_path;) starts with "$user" by default this means any tables will be
|
||||
# created in that user-specific SCHEMA by default instead of falling back to
|
||||
# "public". This follows the "secure schema usage pattern" summarized as
|
||||
# "Constrain ordinary users to user-private schemas" from
|
||||
# https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATTERNS
|
||||
#
|
||||
# "In PostgreSQL 15 and later, the default configuration supports this usage
|
||||
# pattern. In prior versions, or when using a database that has been upgraded
|
||||
# from a prior version, you will need to remove the public CREATE privilege
|
||||
# from the public schema"
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
CREATE USER cdn WITH PASSWORD '${cdn_password:?}';
|
||||
CREATE USER cdn WITH PASSWORD \'"$cdn_password"\';
|
||||
CREATE DATABASE cdn;
|
||||
GRANT ALL PRIVILEGES ON DATABASE cdn TO cdn;
|
||||
\c cdn;
|
||||
CREATE SCHEMA cdn AUTHORIZATION cdn;
|
||||
EOSQL
|
||||
|
|
|
@ -13,9 +13,7 @@ pylint sunet-l4lb-namespace
|
|||
mypy --strict sunet-l4lb-namespace
|
||||
"""
|
||||
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
@ -23,7 +21,6 @@ import sys
|
|||
|
||||
def run_command(cmd: str) -> subprocess.CompletedProcess[str]:
|
||||
"""Execute subprocess command"""
|
||||
print(f"{cmd}")
|
||||
args = shlex.split(cmd)
|
||||
try:
|
||||
proc = subprocess.run(args, capture_output=True, check=True, encoding="utf-8")
|
||||
|
@ -38,15 +35,12 @@ def run_command(cmd: str) -> subprocess.CompletedProcess[str]:
|
|||
return proc
|
||||
|
||||
|
||||
def configure_interfaces( # pylint: disable=too-many-locals,too-many-branches
|
||||
def configure_interfaces(
|
||||
namespace: str, if_data: dict[str, dict[str, list[str]]]
|
||||
) -> None:
|
||||
"""Configure interfaces"""
|
||||
proc = run_command(f"ip netns exec {namespace} ip -j addr show")
|
||||
proc = run_command("ip netns exec l4lb ip -j addr show")
|
||||
namespace_ifs = json.loads(proc.stdout)
|
||||
|
||||
ipv4_key = "ipv4"
|
||||
ipv6_key = "ipv6"
|
||||
for if_name, data in if_data.items():
|
||||
if_exists = next(
|
||||
(True for interface in namespace_ifs if interface["ifname"] == if_name),
|
||||
|
@ -60,73 +54,38 @@ def configure_interfaces( # pylint: disable=too-many-locals,too-many-branches
|
|||
else:
|
||||
run_command(f"ip link set {if_name} netns {namespace}")
|
||||
|
||||
run_command(f"ip netns exec {namespace} ip link set {if_name} up")
|
||||
|
||||
proc = run_command(f"ip netns exec {namespace} ip -j addr show dev {if_name}")
|
||||
if_conf = json.loads(proc.stdout)
|
||||
|
||||
# Add missing addresses from config
|
||||
if ipv4_key in data:
|
||||
for configured_ipv4_cidr in data[ipv4_key]:
|
||||
ip4, prefix = configured_ipv4_cidr.split("/")
|
||||
v4_addr_exists = next(
|
||||
(
|
||||
True
|
||||
for addr in if_conf[0]["addr_info"]
|
||||
if addr["local"] == ip4 and addr["prefixlen"] == int(prefix)
|
||||
),
|
||||
False,
|
||||
)
|
||||
if not v4_addr_exists:
|
||||
run_command(
|
||||
f"ip netns exec {namespace} ip addr add {configured_ipv4_cidr} dev {if_name}" # pylint: disable=line-too-long
|
||||
)
|
||||
if ipv6_key in data:
|
||||
for ipv6_cidr in data[ipv6_key]:
|
||||
ip6, prefix = ipv6_cidr.split("/")
|
||||
v6_addr_exists = next(
|
||||
(
|
||||
True
|
||||
for addr in if_conf[0]["addr_info"]
|
||||
if addr["local"] == ip6 and addr["prefixlen"] == int(prefix)
|
||||
),
|
||||
False,
|
||||
)
|
||||
if not v6_addr_exists:
|
||||
run_command(
|
||||
f"ip netns exec {namespace} ip addr add {ipv6_cidr} dev {if_name}"
|
||||
)
|
||||
|
||||
# Remove no longer configured addresseses
|
||||
for addr_info in if_conf[0]["addr_info"]:
|
||||
# Ignore addresses like fe80
|
||||
if addr_info["scope"] != "global":
|
||||
continue
|
||||
|
||||
cidr = "/".join((addr_info["local"], str(addr_info["prefixlen"])))
|
||||
|
||||
# We need strict=False because otherwise ip_network() gets angry if
|
||||
# there are host bits set in the address (which of course there is
|
||||
# because we are parsing actual interface configs, not pure
|
||||
# "networks")
|
||||
cidr_net = ipaddress.ip_network(cidr, strict=False)
|
||||
|
||||
needs_removal = False
|
||||
if cidr_net.version == 4:
|
||||
if ipv4_key not in data or cidr not in data[ipv4_key]:
|
||||
needs_removal = True
|
||||
elif cidr_net.version == 6:
|
||||
if ipv6_key not in data or cidr not in data[ipv6_key]:
|
||||
needs_removal = True
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Expected IPv4 or IPv6, got something else: {cidr_net.version}"
|
||||
)
|
||||
|
||||
if needs_removal:
|
||||
for ipv4_cidr in data["ipv4"]:
|
||||
ip4, prefix = ipv4_cidr.split("/")
|
||||
v4_addr_exists = next(
|
||||
(
|
||||
True
|
||||
for addr in if_conf[0]["addr_info"]
|
||||
if addr["local"] == ip4 and addr["prefixlen"] == int(prefix)
|
||||
),
|
||||
False,
|
||||
)
|
||||
if not v4_addr_exists:
|
||||
run_command(
|
||||
f"ip netns exec {namespace} ip addr del {cidr} dev {if_name}"
|
||||
f"ip netns exec {namespace} ip addr add {ipv4_cidr} dev {if_name}"
|
||||
)
|
||||
for ipv6_cidr in data["ipv6"]:
|
||||
ip6, prefix = ipv6_cidr.split("/")
|
||||
v6_addr_exists = next(
|
||||
(
|
||||
True
|
||||
for addr in if_conf[0]["addr_info"]
|
||||
if addr["local"] == ip6 and addr["prefixlen"] == int(prefix)
|
||||
),
|
||||
False,
|
||||
)
|
||||
if not v6_addr_exists:
|
||||
run_command(
|
||||
f"ip netns exec {namespace} ip addr add {ipv6_cidr} dev {if_name}"
|
||||
)
|
||||
|
||||
run_command(f"ip netns exec {namespace} ip link set {if_name} up")
|
||||
|
||||
|
||||
def setup_namespaces(netns_data: dict[str, dict[str, dict[str, list[str]]]]) -> None:
|
||||
|
@ -140,17 +99,8 @@ def setup_namespaces(netns_data: dict[str, dict[str, dict[str, list[str]]]]) ->
|
|||
if not netns_exists:
|
||||
run_command(f"ip netns add {namespace}")
|
||||
|
||||
# Make localhost available
|
||||
run_command(f"ip netns exec {namespace} ip link set lo up")
|
||||
|
||||
# (Re)load the nft ruleset for the given namespace
|
||||
nft_ruleset = f"/opt/sunet-cdn/l4lb/conf/nft-{namespace}.conf"
|
||||
if os.path.isfile(nft_ruleset):
|
||||
run_command(f"ip netns exec {namespace} nft -f {nft_ruleset}")
|
||||
else:
|
||||
print(
|
||||
f"WARNING: no nft ruleset found for namespace '{namespace}' ({nft_ruleset}), the namespace will not be firewalled" # pylint: disable=line-too-long
|
||||
)
|
||||
# Make localhost available
|
||||
run_command(f"ip netns exec {namespace} ip link set lo up")
|
||||
|
||||
configure_interfaces(namespace, if_data)
|
||||
|
||||
|
@ -179,30 +129,10 @@ def main() -> None:
|
|||
# }
|
||||
# }
|
||||
# }
|
||||
with open("/opt/sunet-cdn/l4lb/conf/netns.json", encoding="utf-8") as f:
|
||||
netns_data = json.load(f)
|
||||
|
||||
input_files = [
|
||||
"/opt/sunet-cdn/l4lb/conf/netns-base.json",
|
||||
"/opt/sunet-cdn/l4lb/conf/netns-sunet-cdn-agent.json",
|
||||
]
|
||||
|
||||
merged_netns_data: dict[str, dict[str, dict[str, list[str]]]] = {}
|
||||
for input_file in input_files:
|
||||
try:
|
||||
with open(input_file, encoding="utf-8") as f:
|
||||
netns_data = json.load(f)
|
||||
|
||||
# Combine interface config from multiple files belonging to the same namespace
|
||||
for ns, ns_data in netns_data.items():
|
||||
if ns in merged_netns_data:
|
||||
merged_netns_data[ns].update(ns_data)
|
||||
else:
|
||||
merged_netns_data[ns] = ns_data
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"skipping nonexistant file '{input_file}'")
|
||||
continue
|
||||
|
||||
setup_namespaces(merged_netns_data)
|
||||
setup_namespaces(netns_data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -3,7 +3,7 @@ class cdn::cache(
|
|||
Hash[String, Integer] $customers = {
|
||||
customer1 => 1000000000,
|
||||
},
|
||||
String $sunet_cdn_purger_version = '0.0.8',
|
||||
String $sunet_cdnp_version = '0.0.6',
|
||||
Hash[String, String] $acme_url = {
|
||||
test => 'https://internal-sto3-test-ca-1.cdn.sunet.se:9000/acme/acme/directory'
|
||||
},
|
||||
|
@ -155,51 +155,51 @@ class cdn::cache(
|
|||
creates => "/etc/letsencrypt/live/${my_fqdn}/fullchain.pem"
|
||||
}
|
||||
|
||||
$sunet_cdn_purger_dir = '/var/lib/sunet-cdn-purger'
|
||||
$sunet_cdn_purger_file = "sunet-cdn-purger_${sunet_cdn_purger_version}_linux_${facts[os][architecture]}.tar.gz"
|
||||
$sunet_cdn_purger_url = "https://github.com/SUNET/sunet-cdn-purger/releases/download/v${sunet_cdn_purger_version}/${sunet_cdn_purger_file}"
|
||||
$sunet_cdnp_dir = '/var/lib/sunet-cdnp'
|
||||
$sunet_cdnp_file = "sunet-cdnp_${sunet_cdnp_version}_linux_${facts[os][architecture]}.tar.gz"
|
||||
$sunet_cdnp_url = "https://github.com/SUNET/sunet-cdnp/releases/download/v${sunet_cdnp_version}/${sunet_cdnp_file}"
|
||||
# Create directory for managing CDP purger
|
||||
file { $sunet_cdn_purger_dir:
|
||||
file { $sunet_cdnp_dir:
|
||||
ensure => directory,
|
||||
owner => 'root',
|
||||
group => 'root',
|
||||
mode => '0755',
|
||||
}
|
||||
|
||||
exec { "curl -LO ${sunet_cdn_purger_url}":
|
||||
creates => "${sunet_cdn_purger_dir}/${sunet_cdn_purger_file}",
|
||||
cwd => $sunet_cdn_purger_dir,
|
||||
notify => Exec['extract sunet-cdn-purger'],
|
||||
exec { "curl -LO ${sunet_cdnp_url}":
|
||||
creates => "${sunet_cdnp_dir}/${sunet_cdnp_file}",
|
||||
cwd => $sunet_cdnp_dir,
|
||||
notify => Exec['extract sunet-cdnp'],
|
||||
}
|
||||
|
||||
exec { 'extract sunet-cdn-purger':
|
||||
command => "tar -xzf ${sunet_cdn_purger_file} sunet-cdn-purger",
|
||||
cwd => $sunet_cdn_purger_dir,
|
||||
exec { 'extract sunet-cdnp':
|
||||
command => "tar -xzf ${sunet_cdnp_file} sunet-cdnp",
|
||||
cwd => $sunet_cdnp_dir,
|
||||
refreshonly => true,
|
||||
notify => Service['sunet-cdn-purger'],
|
||||
notify => Service['sunet-cdnp'],
|
||||
}
|
||||
|
||||
file { "${sunet_cdn_purger_dir}/sunet-cdn-purger":
|
||||
file { "${sunet_cdnp_dir}/sunet-cdnp":
|
||||
owner => 'root',
|
||||
group => 'root',
|
||||
mode => '0755',
|
||||
}
|
||||
|
||||
file { '/usr/local/bin/sunet-cdn-purger':
|
||||
file { '/usr/local/bin/sunet-cdnp':
|
||||
ensure => link,
|
||||
target => "${sunet_cdn_purger_dir}/sunet-cdn-purger",
|
||||
target => "${sunet_cdnp_dir}/sunet-cdnp",
|
||||
}
|
||||
|
||||
file { '/etc/systemd/system/sunet-cdn-purger.service':
|
||||
file { '/etc/systemd/system/sunet-cdnp.service':
|
||||
ensure => file,
|
||||
owner => 'root',
|
||||
group => 'root',
|
||||
mode => '0644',
|
||||
content => template('cdn/cache/sunet-cdn-purger.service.erb'),
|
||||
content => template('cdn/cache/sunet-cdnp.service.erb'),
|
||||
notify => [Class['sunet::systemd_reload']],
|
||||
}
|
||||
|
||||
service { 'sunet-cdn-purger':
|
||||
service { 'sunet-cdnp':
|
||||
ensure => 'running',
|
||||
enable => true,
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ class cdn::db(
|
|||
}
|
||||
|
||||
file { '/opt/sunet-cdn/db/conf/init-cdn-db.conf':
|
||||
ensure => file,
|
||||
ensure => directory,
|
||||
owner => '999',
|
||||
group => '999',
|
||||
mode => '0640',
|
||||
|
@ -52,7 +52,7 @@ class cdn::db(
|
|||
}
|
||||
|
||||
file { '/opt/sunet-cdn/db/docker-entrypoint-initdb.d/init-cdn-db.sh':
|
||||
ensure => file,
|
||||
ensure => directory,
|
||||
owner => '999',
|
||||
group => '999',
|
||||
mode => '0750',
|
||||
|
|
|
@ -17,8 +17,6 @@ class cdn::l4lb(
|
|||
|
||||
include sunet::systemd_reload
|
||||
|
||||
package {'conntrack': ensure => installed }
|
||||
|
||||
package {'bird2': ensure => installed }
|
||||
|
||||
file { '/opt/sunet-cdn':
|
||||
|
@ -56,20 +54,12 @@ class cdn::l4lb(
|
|||
mode => '0640',
|
||||
}
|
||||
|
||||
file { '/opt/sunet-cdn/l4lb/conf/netns-base.json':
|
||||
file { '/opt/sunet-cdn/l4lb/conf/netns.json':
|
||||
ensure => file,
|
||||
owner => 'root',
|
||||
group => 'root',
|
||||
mode => '0644',
|
||||
content => template('cdn/l4lb/netns-base.json.erb'),
|
||||
}
|
||||
|
||||
file { '/opt/sunet-cdn/l4lb/conf/nft-l4lb.conf':
|
||||
ensure => file,
|
||||
owner => 'root',
|
||||
group => 'root',
|
||||
mode => '0644',
|
||||
content => template('cdn/l4lb/nft-l4lb.conf.erb'),
|
||||
content => template('cdn/l4lb/netns.json.erb'),
|
||||
}
|
||||
|
||||
file { '/usr/local/bin/sunet-l4lb-namespace':
|
||||
|
|
|
@ -40,7 +40,7 @@ services:
|
|||
# We build our own varnish with the slash vmod present. We use the slash
|
||||
# "fellow" storage backend to be able to persist cached content to disk, so
|
||||
# it is retained in case of a restart of the container or machine.
|
||||
image: "platform.sunet.se/sunet-cdn/cdn-varnish@sha256:248b1ca861f1a8bb548845b656526210ef7015ba71c0e264dc4619da16407b40"
|
||||
image: "platform.sunet.se/sunet-cdn/cdn-varnish:af7f7d11e61acf9f6113811615d1baa46daf3bd1"
|
||||
# Use the same custom user as is used for haproxy.
|
||||
user: <%= @customer_uid %>:<%= @customer_uid %>
|
||||
volumes:
|
||||
|
|
|
@ -6,7 +6,7 @@ After=docker.service
|
|||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/sunet-cdn-purger \
|
||||
ExecStart=/usr/local/bin/sunet-cdnp \
|
||||
-mqtt-ca-file /usr/local/share/ca-certificates/step_ca_root.crt \
|
||||
-mqtt-client-key-file /etc/letsencrypt/live/<%= @networking['fqdn'] %>/privkey.pem \
|
||||
-mqtt-client-cert-file /etc/letsencrypt/live/<%= @networking['fqdn'] %>/fullchain.pem \
|
|
@ -3,11 +3,9 @@ services:
|
|||
image: "postgres:<%= @postgres_version %>"
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=<%= @db_secrets['postgres_password'] %>
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- /opt/sunet-cdn/db/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
|
||||
- /opt/sunet-cdn/db/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.dh
|
||||
- /opt/sunet-cdn/db/conf:/conf
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
|
|
@ -15,6 +15,14 @@
|
|||
"ipv6": [
|
||||
"2001:6b0:2006:75::1/127"
|
||||
]
|
||||
},
|
||||
"dummy0": {
|
||||
"ipv4": [
|
||||
"188.240.152.1/32"
|
||||
],
|
||||
"ipv6": [
|
||||
"2001:6b0:2100::1/128"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
#!/usr/sbin/nft -f
|
||||
|
||||
flush ruleset
|
||||
|
||||
table inet filter {
|
||||
chain input {
|
||||
type filter hook input priority 0; policy drop;
|
||||
|
||||
# accept any localhost traffic
|
||||
iif lo counter accept
|
||||
|
||||
# accept icmp
|
||||
ip protocol icmp counter accept
|
||||
ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded,
|
||||
parameter-problem, echo-request, mld-listener-query,
|
||||
nd-router-solicit, nd-router-advert, nd-neighbor-solicit,
|
||||
nd-neighbor-advert } counter accept
|
||||
|
||||
# accept traffic originated from us
|
||||
ct state established counter accept
|
||||
# silently drop invalid packets
|
||||
ct state invalid counter drop
|
||||
}
|
||||
chain forward {
|
||||
type filter hook forward priority 0; policy drop;
|
||||
}
|
||||
chain output {
|
||||
type filter hook output priority 0;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTP and HTTPS
|
||||
add rule inet filter input tcp dport 80 counter accept comment "l4lb HTTP"
|
||||
add rule inet filter input tcp dport 443 counter accept comment "l4lb HTTPS"
|
||||
|
||||
# BGP
|
||||
add rule inet filter input ip saddr { 130.242.64.232 } tcp dport 179 counter accept comment "tug-r11-v4"
|
||||
add rule inet filter input ip saddr { 130.242.64.234 } tcp dport 179 counter accept comment "tug-r12-v4"
|
||||
add rule inet filter input ip6 saddr { 2001:6b0:2006:74:: } tcp dport 179 counter accept comment "tug-r11-v6"
|
||||
add rule inet filter input ip6 saddr { 2001:6b0:2006:75:: } tcp dport 179 counter accept comment "tug-r12-v6"
|
|
@ -1,4 +1,4 @@
|
|||
---
|
||||
cdn::db-secrets:
|
||||
postgres_password: ENC[PKCS7,MIIDCAYJKoZIhvcNAQcDoIIC+TCCAvUCAQAxggKQMIICjAIBADB0MFwxCzAJBgNVBAYTAlNFMQ4wDAYDVQQKDAVTVU5FVDEOMAwGA1UECwwFRVlBTUwxLTArBgNVBAMMJGludGVybmFsLXN0bzMtdGVzdC1kYi0xLmNkbi5zdW5ldC5zZQIUbbUXduFvDLw3OUVWiGrIvFBkkJMwDQYJKoZIhvcNAQEBBQAEggIAGKLk12OT5zsVKd04qsLkFtawdauLYUERXUC3d9FtZNVpwCFDNMnSsruUfasOvyvdaRbm8AUk/nAGuBNjD9HJj8J45KfQUEAPstnZPHndkF51LwU1twFrZvcSnvFANvxh61MzccMz6NVQL5CXsw4IWMDNhUbkhO5cRfxc0SOVugeTZ74BWwpEww9uKVPtfPRKCgJayBq1o/fyQblGsjJbmu/dRCm32gcdZu1lqfDU0DLsnjk14GJpqpP5h6sEfSrdXyFcWzFzdjtLZLL6TfUWYNYX6CnjjRMv1zZ73877DPXt+vvi0Nvqld5CDTXM9ggDWwZKvluVGn7sTyZdwtWLvs1qK4nui7NLfENtBrUi/GOWsxoFa9tmfeeX/cticzzQcUdDNkfaDgmBa/C7lyjlkwyDGvhYdBHycSEJJ8rxncjBGKl79mpWlK0YTsppgD5eXWZSK7gC3PecRqQ7Jri3aBAWymlM7wfJYP8Z5ocEEq7+BLKSk0Z+Npj8PqJkQ0mw96QC5kNY2LfnGTPbBPpyBZRfZh3F2x1fLN+JQN+IgMpzLW2Oce5HWlPn9iTgCiZU/7mRDJKe8wI4gSg+Mf6hWqlC+NIcMOdAcrfvobzx3PVDHletTd0xgdgGEJpOvtpkYCqcdDbFX4kyEntMaMmp32XBcgsUy1b09uHMB2klzncwXAYJKoZIhvcNAQcBMB0GCWCGSAFlAwQBKgQQereGW4ZbKx3BV6f/PqkgSYAwcqDeq645K3JJOx9su/j9qDcAGxJN1CqIJjkYFBI+/2euykTaCvIqUMhljoDjeJeE]
|
||||
cdn_password: ENC[PKCS7,MIIDCAYJKoZIhvcNAQcDoIIC+TCCAvUCAQAxggKQMIICjAIBADB0MFwxCzAJBgNVBAYTAlNFMQ4wDAYDVQQKDAVTVU5FVDEOMAwGA1UECwwFRVlBTUwxLTArBgNVBAMMJGludGVybmFsLXN0bzMtdGVzdC1kYi0xLmNkbi5zdW5ldC5zZQIUbbUXduFvDLw3OUVWiGrIvFBkkJMwDQYJKoZIhvcNAQEBBQAEggIAC2YvfQOuluAonjkj0iM5DABoSNLXLyjlXfkltBOtAWzlFAQfQNlKd9cArL0qthcge+4AYun9edbyrmKjBAqVYIjPZXMaUjN9HXa07vBwUaHUXUP/rSxL6JYWKAvZTUCnSS+rb/nUM8BAAodk2xNnDrd0H/VN2oBQMkFvWJbCX2/NS9zejr4BpcGTTLjr1GXOuRMwORXwNTHVYZBbZzltnXMRClcdUe9oeIfC2W0BJDTlvAsqVN4DAz985hP8b5vch3uTd59Qzr7pIqpdno8hoI75zdVZ+xeH31rYw5/wqHmsvQK3gvVTtp5zmO4lSWwhiyGfICsX1w/8Fa8j/qR17dfqzkWVWJVN4DN2O/iT1muMtP4WP4j5xvWusE1viW4qGnoFlheo9YVTCP08FLMn0BMyO/7r1AZNG0oDNhTw/DcGYRc0q1bF8L6LjprJrl8Ou7fZbxWcIuQ8UMC+aJPOI0vingTip7/nKhGIBSiplxxKPH3jB2G7NXnimi4sxgAXsXwSSHGTMZH6q46Kc7YrtzT6Vur0W8onQgqFw6Hg5kgybyrdU68skxqAHIQEV3bZ68e5f3MyKY5HxSse8IIngAQdF6mOvOf6JB3zQ98m/JXDDV9FvaMLXSu4iUMGmoHJDO6xmMLaPUamJobM7SFA+0gPa3hAV7fejDdzhh6bLFYwXAYJKoZIhvcNAQcBMB0GCWCGSAFlAwQBKgQQCT1soHO5e0vaqTWVkkyhS4Awdqh/mowa1LW46di/aaHZp0wPie3eEZnaDsKrIAo4yMFi/Sd0QCXF8YjicLFt0vK5]
|
||||
cdn_password: ENC[PKCS7,MIIDCAYJKoZIhvcNAQcDoIIC+TCCAvUCAQAxggKQMIICjAIBADB0MFwxCzAJBgNVBAYTAlNFMQ4wDAYDVQQKDAVTVU5FVDEOMAwGA1UECwwFRVlBTUwxLTArBgNVBAMMJGludGVybmFsLXN0bzMtdGVzdC1kYi0xLmNkbi5zdW5ldC5zZQIUbbUXduFvDLw3OUVWiGrIvFBkkJMwDQYJKoZIhvcNAQEBBQAEggIAdeN61oxrXS1J3qqYVotSQx1xMhBmcq73UsFhJ3glLWTbSeXhna57E/zmVkjTGmfH6+mBqw8rM6ewxQ6XfKRC7rAOtM3g7B5uqblzXReUP9b8QeksI6/6qeTSua7qgTP3UnGNSHNVbRcwbwQbwgQBrrrWP5IcxiEkNVnjv37GzpL54kNE4VSS/2jsQkSWom0uGKFfFBm0Rkg6hQor7wb/Vi+f6BbbxZvVDBXQDGuwHp3YE+Odb/zTUEDVtYUI2t/19iYKS/yjSmG1K2QEpj6vaav7IzbhDkCZu+6JTvWZ96tARdSlllkOACFvpdFCNrODHrmVrZduQ7XriohySTxLe43M0bm4a63V5ghKatAd9uAXku5tTU0QFzC+bHc2pGH7rYznGgfhi0+PqslHmWJoH+H3rMOz4FFaywajByAe1XWBKT9MBiKrihKrqC9yInIaDcRA0PMZv6HhBnpUcI1X+wqMvd9qiNi5mTXyv+kPZbZHNmV4u+gO7xlk4XDvE70OOx7x13ICQ50KwjXbBrIKpowj+3I8GciZRjGsn3B+1wAn8clJ5iQ2gORQ2WGa2s9dJKwANi6ByAL1GR0/fMabMajYLvOTrLemrCA2MTbInAfuUTohlEjc145oubHNm2VmnQDZ1mAvvRayPlbsVGx3Q0cm1RwSl+MjzKmKBOK+JtMwXAYJKoZIhvcNAQcBMB0GCWCGSAFlAwQBKgQQwWjF6Qsi2kSEbp3R3QVnE4AwAwLkN8LkiyghIdctO32a37wWJ2m43/gh7l0aHuxrNAQS/qK3odCfoUoy/GjRWawc]
|
||||
|
|
Loading…
Add table
Reference in a new issue