cryptolabs-proxy 1.4.0__tar.gz → 1.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cryptolabs_proxy-1.4.0/src/cryptolabs_proxy.egg-info → cryptolabs_proxy-1.5.0}/PKG-INFO +1 -1
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/pyproject.toml +1 -1
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/__init__.py +1 -1
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/config.py +93 -2
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/setup.py +30 -7
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/standard_adoption.py +115 -11
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/templates/docker-compose.yml.j2 +15 -3
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/templates/nginx.conf.j2 +49 -5
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0/src/cryptolabs_proxy.egg-info}/PKG-INFO +1 -1
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy.egg-info/SOURCES.txt +1 -0
- cryptolabs_proxy-1.5.0/tests/test_hostname_policy.py +497 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_standard_adoption.py +1 -1
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/LICENSE +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/README.md +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/setup.cfg +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/auth.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/cli.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/controller_recovery.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/custom_config.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/exporter_manager.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/identity.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/migration.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/release_selection.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/services.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/ssl.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/templates/acme-challenge.conf.j2 +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/templates/exporter-auth-routes.conf.j2 +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/templates/fleet-api-routes.conf.j2 +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/templates/host-forwarding.conf.j2 +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/templates/legacy-bootstrap-nginx.conf +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/templates/legacy-bootstrap-provenance.json +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/templates/standard-service-routes.conf.j2 +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/templates/vast-price-manager-auth-blocks.conf.j2 +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/templates/vast-price-manager.conf.j2 +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/updates.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/vpm_prerequisite.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy.egg-info/dependency_links.txt +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy.egg-info/entry_points.txt +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy.egg-info/requires.txt +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy.egg-info/top_level.txt +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_auth.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_controller_guard_transport.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_controller_recovery_guard.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_edge_ownership.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_exporter_auth_protections.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_host_forwarding.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_identity_host_package.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_image_release_promotion.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_netbox_link.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_offline_image.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_proxy_customer_route_repairs.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_proxy_lifecycle_review_repairs.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_proxy_recovery_tls_repairs.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_release_identifier_boundary.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_release_publication.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_release_selection.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_release_workflow_temp_paths.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_routes.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_updates.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_vast_price_manager.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_version_consistency.py +0 -0
- {cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/tests/test_vpm_prerequisite.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cryptolabs-proxy"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.5.0"
|
|
8
8
|
description = "Unified reverse proxy and fleet management landing page for CryptoLabs products"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from jinja2 import Environment, PackageLoader, select_autoescape
|
|
6
6
|
|
|
7
7
|
CONFIG_DIR = Path("/etc/cryptolabs-proxy")
|
|
8
|
+
_UNSET = object()
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def get_jinja_env():
|
|
@@ -17,13 +18,19 @@ def get_jinja_env():
|
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
def generate_nginx_config(config_dir: Path, domain: str, letsencrypt: bool = False, services: dict = None, *, host_forward_groups=None, acme_challenge=None):
|
|
21
|
+
def generate_nginx_config(config_dir: Path, domain: str, letsencrypt: bool = False, services: dict = None, *, host_forward_groups=None, acme_challenge=None, hostname_policy=_UNSET, http_policy=_UNSET):
|
|
21
22
|
"""Generate nginx.conf from template."""
|
|
22
23
|
from .services import ServiceRegistry
|
|
23
24
|
if host_forward_groups is None:
|
|
24
25
|
host_forward_groups = ServiceRegistry(config_dir).config.get('host_forward_groups', [])
|
|
25
26
|
if acme_challenge is None:
|
|
26
27
|
acme_challenge = ServiceRegistry(config_dir).config.get('acme_challenge')
|
|
28
|
+
if hostname_policy is _UNSET:
|
|
29
|
+
hostname_policy = ServiceRegistry(config_dir).config.get("hostname_policy")
|
|
30
|
+
hostname_policy = validate_hostname_policy(hostname_policy, domain)
|
|
31
|
+
if http_policy is _UNSET:
|
|
32
|
+
http_policy = ServiceRegistry(config_dir).config.get("http_policy")
|
|
33
|
+
http_policy = validate_http_policy(http_policy, domain, acme_challenge)
|
|
27
34
|
acme_challenge = validate_acme_challenge(acme_challenge)
|
|
28
35
|
host_forward_groups = validate_host_forward_groups(host_forward_groups, domain)
|
|
29
36
|
env = get_jinja_env()
|
|
@@ -35,6 +42,8 @@ def generate_nginx_config(config_dir: Path, domain: str, letsencrypt: bool = Fal
|
|
|
35
42
|
services=services or {},
|
|
36
43
|
host_forward_groups=host_forward_groups,
|
|
37
44
|
acme_challenge=acme_challenge,
|
|
45
|
+
hostname_policy=hostname_policy,
|
|
46
|
+
http_policy=http_policy,
|
|
38
47
|
)
|
|
39
48
|
|
|
40
49
|
(config_dir / "nginx.conf").write_text(content)
|
|
@@ -57,7 +66,12 @@ def generate_docker_compose(config_dir: Path, domain: str = None, use_letsencryp
|
|
|
57
66
|
template = env.get_template("docker-compose.yml.j2")
|
|
58
67
|
from .release_selection import load, targets, SelectionError
|
|
59
68
|
from .services import ServiceRegistry
|
|
60
|
-
|
|
69
|
+
registry = ServiceRegistry(config_dir)
|
|
70
|
+
if domain is None: domain = registry.config.get('domain')
|
|
71
|
+
release_selection_dir = release_selection_dir or registry.config.get('release_selection_dir')
|
|
72
|
+
http_policy = validate_http_policy(registry.config.get('http_policy'), domain, registry.config.get('acme_challenge'))
|
|
73
|
+
healthcheck = policy_healthcheck(registry.config.get('standard_adoption',{}).get('healthcheck'),domain,registry.config.get('hostname_policy'),http_policy,letsencrypt=use_letsencrypt)
|
|
74
|
+
health_command = healthcheck['Test'][1] if healthcheck else None
|
|
61
75
|
proxy_image = 'ghcr.io/cryptolabsza/cryptolabs-proxy:latest'
|
|
62
76
|
proxy_origin = None
|
|
63
77
|
if release_selection_dir:
|
|
@@ -73,6 +87,9 @@ def generate_docker_compose(config_dir: Path, domain: str = None, use_letsencryp
|
|
|
73
87
|
release_selection_dir=str(Path(release_selection_dir).resolve()) if release_selection_dir else None,
|
|
74
88
|
proxy_image=proxy_image,
|
|
75
89
|
proxy_origin=proxy_origin,
|
|
90
|
+
http_policy=http_policy,
|
|
91
|
+
policy_healthcheck=health_command,
|
|
92
|
+
policy_healthcheck_config=healthcheck,
|
|
76
93
|
)
|
|
77
94
|
|
|
78
95
|
(config_dir / "docker-compose.yml").write_text(content)
|
|
@@ -143,3 +160,77 @@ def validate_acme_challenge(value):
|
|
|
143
160
|
|
|
144
161
|
def render_acme_challenge(value, *, legacy_profile=False):
|
|
145
162
|
return get_jinja_env().get_template('acme-challenge.conf.j2').render(acme_challenge=value,legacy_forward_profile=legacy_profile).encode()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def validate_hostname_policy(policy, domain):
|
|
166
|
+
"""Only the observed literal-primary $host guard with fixed 444 is supported."""
|
|
167
|
+
import re
|
|
168
|
+
import ipaddress
|
|
169
|
+
if policy is None:
|
|
170
|
+
return None
|
|
171
|
+
if not isinstance(policy, str) or policy != 'primary-only':
|
|
172
|
+
raise ValueError('invalid_hostname_policy')
|
|
173
|
+
literal = re.compile(r'(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+')
|
|
174
|
+
if not isinstance(domain, str) or not literal.fullmatch(domain):
|
|
175
|
+
raise ValueError('hostname_policy_requires_literal_domain')
|
|
176
|
+
try:
|
|
177
|
+
ipaddress.ip_address(domain)
|
|
178
|
+
except ValueError:
|
|
179
|
+
return policy
|
|
180
|
+
raise ValueError('hostname_policy_requires_literal_domain')
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def validate_http_policy(policy, domain, acme_challenge=None):
|
|
184
|
+
"""Closed primary HTTPS redirect and local HTTP ACME transport."""
|
|
185
|
+
if policy is None:
|
|
186
|
+
return None
|
|
187
|
+
if not isinstance(policy, str) or policy != 'redirect-primary-https':
|
|
188
|
+
raise ValueError('invalid_http_policy')
|
|
189
|
+
validate_hostname_policy('primary-only', domain)
|
|
190
|
+
if acme_challenge is not None:
|
|
191
|
+
raise ValueError('http_policy_local_acme_conflicts_with_upstream')
|
|
192
|
+
return policy
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def render_policy_healthcheck(domain, hostname_policy=None, http_policy=None, *, letsencrypt=False):
|
|
196
|
+
"""Public HTTPS loopback uses the declared Host/SNI; internal API stays checked.
|
|
197
|
+
|
|
198
|
+
The stored certificate is the explicit local trust anchor. Hostname
|
|
199
|
+
verification remains enabled; browser and forwarding trust are unchanged.
|
|
200
|
+
"""
|
|
201
|
+
validate_hostname_policy(hostname_policy, domain)
|
|
202
|
+
validate_http_policy(http_policy, domain)
|
|
203
|
+
if hostname_policy is None and http_policy is None:
|
|
204
|
+
return None
|
|
205
|
+
validate_hostname_policy('primary-only', domain)
|
|
206
|
+
certificate='/etc/letsencrypt/live/'+domain+'/fullchain.pem' if letsencrypt else '/etc/nginx/ssl/server.crt'
|
|
207
|
+
return ('curl -fsS --cacert '+certificate+' --resolve '+domain+':443:127.0.0.1 https://'+domain+
|
|
208
|
+
'/api/health && curl -f http://127.0.0.1:8080/api/health || exit 1')
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def policy_healthcheck(current, domain, hostname_policy=None, http_policy=None, *, letsencrypt=False):
|
|
212
|
+
"""Keep the exact independently observed verified probe; map image defaults.
|
|
213
|
+
|
|
214
|
+
Recognition compares fixed raw shell syntax, with spaces/tabs only. It
|
|
215
|
+
never accepts arbitrary operations, flags, URLs, credentials or snippets.
|
|
216
|
+
"""
|
|
217
|
+
from copy import deepcopy
|
|
218
|
+
import re
|
|
219
|
+
command=render_policy_healthcheck(domain,hostname_policy,http_policy,letsencrypt=letsencrypt)
|
|
220
|
+
if command is None: return None
|
|
221
|
+
default=['CMD-SHELL','curl -f http://localhost/api/health && curl -f http://localhost:8080/api/health || exit 1']
|
|
222
|
+
if current is None: return {'Test':['CMD-SHELL',command]}
|
|
223
|
+
fields={'Test','Interval','Timeout','StartPeriod','StartInterval','Retries'}
|
|
224
|
+
if not isinstance(current,dict) or set(current)-fields or not isinstance(current.get('Test'),list): raise ValueError('source_healthcheck_unmapped')
|
|
225
|
+
if any(type(v) is not int or not 0<=v<=2**63-1 for k,v in current.items() if k!='Test'): raise ValueError('source_healthcheck_unmapped')
|
|
226
|
+
test=current['Test']
|
|
227
|
+
trusted='curl -fsS --resolve '+domain+':443:127.0.0.1 https://'+domain+'/api/health >/dev/null && curl -fsS http://localhost:8080/api/health >/dev/null || exit 1'
|
|
228
|
+
if len(test)==2 and test[0]=='CMD-SHELL' and isinstance(test[1],str) and len(test[1])<=4096:
|
|
229
|
+
# Shell operators must remain operators, not quoted/escaped curl
|
|
230
|
+
# arguments. Only spaces/tabs may vary in this closed raw skeleton.
|
|
231
|
+
# The validated domain is escaped with every other literal position.
|
|
232
|
+
skeleton=re.escape(trusted).replace(r'\ ',r'[ ]+')
|
|
233
|
+
if re.fullmatch(skeleton,test[1]): return deepcopy(current)
|
|
234
|
+
if test not in (default,['CMD-SHELL',command]): raise ValueError('source_healthcheck_unmapped')
|
|
235
|
+
result=deepcopy(current);result['Test']=['CMD-SHELL',command]
|
|
236
|
+
return result
|
|
@@ -14,7 +14,7 @@ from dataclasses import dataclass
|
|
|
14
14
|
from typing import Optional, Dict, List, Tuple
|
|
15
15
|
from datetime import datetime
|
|
16
16
|
|
|
17
|
-
from .config import generate_nginx_config, generate_docker_compose, CONFIG_DIR
|
|
17
|
+
from .config import generate_nginx_config, generate_docker_compose, validate_hostname_policy, validate_http_policy, render_policy_healthcheck, CONFIG_DIR
|
|
18
18
|
from .ssl import generate_self_signed_cert
|
|
19
19
|
from .services import ServiceRegistry
|
|
20
20
|
from .release_selection import SELECTION_CONTRACT, SelectionError, load as load_selection, targets as selection_targets
|
|
@@ -45,8 +45,12 @@ class ProxyConfig:
|
|
|
45
45
|
# Internal API token - shared secret for service-to-service config API
|
|
46
46
|
internal_api_token: str = ""
|
|
47
47
|
release_selection_dir: Optional[Path] = None
|
|
48
|
+
hostname_policy: Optional[str] = None
|
|
49
|
+
http_policy: Optional[str] = None
|
|
48
50
|
|
|
49
51
|
def __post_init__(self):
|
|
52
|
+
self.hostname_policy = validate_hostname_policy(self.hostname_policy, self.domain)
|
|
53
|
+
self.http_policy = validate_http_policy(self.http_policy, self.domain)
|
|
50
54
|
if not self.auth_secret:
|
|
51
55
|
self.auth_secret = secrets.token_hex(32)
|
|
52
56
|
if not self.internal_api_token:
|
|
@@ -452,6 +456,13 @@ def setup_proxy(
|
|
|
452
456
|
|
|
453
457
|
target_image = 'ghcr.io/cryptolabsza/cryptolabs-proxy:latest'
|
|
454
458
|
recorded = ServiceRegistry(CONFIG_DIR).config
|
|
459
|
+
try:
|
|
460
|
+
policy = config.hostname_policy if config.hostname_policy is not None else recorded.get('hostname_policy')
|
|
461
|
+
config.hostname_policy = validate_hostname_policy(policy, config.domain)
|
|
462
|
+
http_policy = config.http_policy if config.http_policy is not None else recorded.get("http_policy")
|
|
463
|
+
config.http_policy = validate_http_policy(http_policy, config.domain, recorded.get("acme_challenge"))
|
|
464
|
+
except ValueError:
|
|
465
|
+
return False, 'Invalid hostname policy; setup not started'
|
|
455
466
|
if recorded.get('standard_adoption'):
|
|
456
467
|
from .standard_adoption import StandardAdoption
|
|
457
468
|
return StandardAdoption(CONFIG_DIR).reconfigure(config)
|
|
@@ -459,6 +470,8 @@ def setup_proxy(
|
|
|
459
470
|
if config.release_selection_dir is None and recorded_directory:
|
|
460
471
|
config.release_selection_dir = Path(recorded_directory)
|
|
461
472
|
baseline = config.release_selection_dir is not None
|
|
473
|
+
if (config.hostname_policy or config.http_policy) and not baseline:
|
|
474
|
+
return False, 'Hostname policy requires approved standard setup or configuration adoption'
|
|
462
475
|
if baseline and recorded.get('custom_config'):
|
|
463
476
|
from .updates import PROXY_ADOPTION_MESSAGE
|
|
464
477
|
return False, 'Custom Nginx configuration requires host adoption. '+PROXY_ADOPTION_MESSAGE
|
|
@@ -568,16 +581,18 @@ def setup_proxy(
|
|
|
568
581
|
candidate_dir = Path(temporary)
|
|
569
582
|
generate_nginx_config(candidate_dir, config.domain, config.use_letsencrypt and ssl_success, registry.services,
|
|
570
583
|
host_forward_groups=registry.config.get('host_forward_groups', []),
|
|
571
|
-
acme_challenge=registry.config.get('acme_challenge')
|
|
584
|
+
acme_challenge=registry.config.get('acme_challenge'),
|
|
585
|
+
hostname_policy=config.hostname_policy, http_policy=config.http_policy)
|
|
572
586
|
validation = ['docker', 'run', '--rm', '--network', DOCKER_NETWORK_NAME,
|
|
573
587
|
'--entrypoint', '']
|
|
574
588
|
validation.extend(['-v', f'{CONFIG_DIR / "ssl"}:/etc/nginx/ssl:ro',
|
|
575
589
|
'-v', f'{candidate_dir / "nginx.conf"}:/etc/nginx/nginx.conf:ro'])
|
|
576
590
|
if config.use_letsencrypt and ssl_success:
|
|
591
|
+
validation.extend(['-v', '/etc/letsencrypt:/etc/letsencrypt:ro'])
|
|
592
|
+
if config.use_letsencrypt and ssl_success or config.http_policy:
|
|
577
593
|
webroot = CONFIG_DIR / 'certbot' / 'www'
|
|
578
594
|
webroot.mkdir(parents=True, exist_ok=True)
|
|
579
|
-
validation.extend(['-v', '/
|
|
580
|
-
'-v', f'{webroot}:/var/www/certbot:ro'])
|
|
595
|
+
validation.extend(['-v', f'{webroot}:/var/www/certbot:ro'])
|
|
581
596
|
validation.extend([target_image, 'nginx', '-t'])
|
|
582
597
|
try:
|
|
583
598
|
tested = subprocess.run(validation, capture_output=True, timeout=60)
|
|
@@ -614,8 +629,12 @@ def setup_proxy(
|
|
|
614
629
|
'-e', 'RELEASE_SELECTION_REQUIRED=true', '--label', 'io.cryptolabs.update.reference=' + target_image,
|
|
615
630
|
'--label', 'io.cryptolabs.update.origin=' + proxy_target['origin']])
|
|
616
631
|
if config.use_letsencrypt and ssl_success:
|
|
617
|
-
cmd.extend(['-v', '/etc/letsencrypt:/etc/letsencrypt:ro'
|
|
618
|
-
|
|
632
|
+
cmd.extend(['-v', '/etc/letsencrypt:/etc/letsencrypt:ro'])
|
|
633
|
+
if config.use_letsencrypt and ssl_success or config.http_policy:
|
|
634
|
+
cmd.extend(['-v', f'{CONFIG_DIR / "certbot" / "www"}:/var/www/certbot:ro'])
|
|
635
|
+
healthcheck = render_policy_healthcheck(config.domain, config.hostname_policy, config.http_policy, letsencrypt=config.use_letsencrypt and ssl_success)
|
|
636
|
+
if healthcheck:
|
|
637
|
+
cmd.extend(['--health-cmd', healthcheck])
|
|
619
638
|
cmd.append(target_image)
|
|
620
639
|
|
|
621
640
|
log("Freeing ports 80/443...")
|
|
@@ -681,6 +700,10 @@ def setup_proxy(
|
|
|
681
700
|
# Step 6: Initialize service registry
|
|
682
701
|
registry = ServiceRegistry(CONFIG_DIR)
|
|
683
702
|
registry.config["domain"] = config.domain
|
|
703
|
+
if config.hostname_policy is not None:
|
|
704
|
+
registry.config["hostname_policy"] = config.hostname_policy
|
|
705
|
+
if config.http_policy is not None:
|
|
706
|
+
registry.config["http_policy"] = config.http_policy
|
|
684
707
|
registry.config["letsencrypt"] = config.use_letsencrypt and ssl_success
|
|
685
708
|
if baseline:
|
|
686
709
|
registry.config['release_selection_dir'] = str(Path(config.release_selection_dir).resolve())
|
|
@@ -731,7 +754,7 @@ def update_proxy_credentials(
|
|
|
731
754
|
fleet_admin_user=fleet_admin_user, fleet_admin_pass=fleet_admin_pass, auth_secret=auth_secret,
|
|
732
755
|
watchdog_api_key=watchdog_api_key, watchdog_url=watchdog_url,
|
|
733
756
|
internal_api_token=internal_api_token, site_name=existing.get('SITE_NAME', 'DC Overview'),
|
|
734
|
-
release_selection_dir=Path(release_directory)))
|
|
757
|
+
release_selection_dir=Path(release_directory), hostname_policy=registry.config.get('hostname_policy'), http_policy=registry.config.get('http_policy')))
|
|
735
758
|
|
|
736
759
|
# Recreate with new credentials
|
|
737
760
|
subprocess.run(["docker", "rm", "-f", "cryptolabs-proxy"], capture_output=True)
|
|
@@ -16,7 +16,7 @@ import tarfile
|
|
|
16
16
|
import time
|
|
17
17
|
from urllib.parse import quote, urlsplit
|
|
18
18
|
|
|
19
|
-
from .config import get_jinja_env, validate_host_forward_groups, render_host_forward_groups, validate_acme_challenge, render_acme_challenge
|
|
19
|
+
from .config import get_jinja_env, validate_host_forward_groups, render_host_forward_groups, validate_acme_challenge, render_acme_challenge, validate_hostname_policy, validate_http_policy, render_policy_healthcheck, policy_healthcheck
|
|
20
20
|
from .custom_config import render_managed_vpm_config
|
|
21
21
|
from .migration import (DockerEngine, CustomConfigMigrator, MigrationError, SwitchController,
|
|
22
22
|
build_recreate_request, endpoint_configurations, wait_for_container_healthy,
|
|
@@ -139,7 +139,7 @@ def _safe_upstream(words):
|
|
|
139
139
|
|
|
140
140
|
def _declaration(value):
|
|
141
141
|
required={'schema_version','domain','tls','release_selection_dir','services','obsolete_overlays','obsolete_owner_labels'}
|
|
142
|
-
if not isinstance(value,dict) or set(value)-required-{'host_forward_groups','acme_challenge'} or not required<=set(value) or type(value['schema_version']) is not int or value['schema_version']!=1:
|
|
142
|
+
if not isinstance(value,dict) or set(value)-required-{'host_forward_groups','acme_challenge','hostname_policy','http_policy'} or not required<=set(value) or type(value['schema_version']) is not int or value['schema_version']!=1:
|
|
143
143
|
raise AdoptionError('declaration_schema_invalid')
|
|
144
144
|
domain=value['domain']
|
|
145
145
|
if not isinstance(domain,str) or not DOMAIN.fullmatch(domain) or '..' in domain or domain.endswith('.') or value['tls'] not in ('self-signed','letsencrypt'):
|
|
@@ -170,12 +170,74 @@ def _declaration(value):
|
|
|
170
170
|
try:
|
|
171
171
|
groups=validate_host_forward_groups(value.get('host_forward_groups',[]),domain)
|
|
172
172
|
acme=validate_acme_challenge(value.get('acme_challenge'))
|
|
173
|
+
policy=validate_hostname_policy(value.get('hostname_policy'),domain)
|
|
174
|
+
http_policy=validate_http_policy(value.get('http_policy'),domain,acme)
|
|
173
175
|
except ValueError as error: raise AdoptionError(str(error)) from None
|
|
174
|
-
|
|
176
|
+
canonical=deepcopy(value)
|
|
177
|
+
for name,setting in [('hostname_policy',policy),('http_policy',http_policy)]:
|
|
178
|
+
if setting is None: canonical.pop(name,None)
|
|
179
|
+
else: canonical[name]=setting
|
|
180
|
+
return {**canonical,'host_forward_groups':groups,'acme_challenge':acme,'release_selection_dir':str(Path(directory).resolve()),'services':services}
|
|
175
181
|
|
|
176
182
|
|
|
177
183
|
def render_standard(declaration):
|
|
178
|
-
return get_jinja_env().get_template('nginx.conf.j2').render(domain=declaration['domain'],letsencrypt=declaration['tls']=='letsencrypt',services=declaration['services'],host_forward_groups=declaration.get('host_forward_groups',[]),acme_challenge=declaration.get('acme_challenge')).encode()
|
|
184
|
+
return get_jinja_env().get_template('nginx.conf.j2').render(domain=declaration['domain'],letsencrypt=declaration['tls']=='letsencrypt',services=declaration['services'],host_forward_groups=declaration.get('host_forward_groups',[]),acme_challenge=declaration.get('acme_challenge'),hostname_policy=validate_hostname_policy(declaration.get('hostname_policy'),declaration['domain']),http_policy=validate_http_policy(declaration.get('http_policy'),declaration['domain'],declaration.get('acme_challenge'))).encode()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _policy_healthcheck(source, declaration):
|
|
188
|
+
try:
|
|
189
|
+
return policy_healthcheck(source.get('Config',{}).get('Healthcheck'),declaration['domain'],declaration.get('hostname_policy'),declaration.get('http_policy'),letsencrypt=declaration['tls']=='letsencrypt')
|
|
190
|
+
except ValueError: raise AdoptionError('source_healthcheck_unmapped') from None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _guarded_bootstrap_profile(legacy, declaration):
|
|
194
|
+
"""Exact observed legacy delta, checked against an independent full fixture.
|
|
195
|
+
|
|
196
|
+
This constructs the supported tree; it never strips unknown source nodes.
|
|
197
|
+
Only the declared primary literal is substituted, in explicit positions.
|
|
198
|
+
"""
|
|
199
|
+
domain=declaration['domain']
|
|
200
|
+
tree=parse_nginx(render_managed_vpm_config(legacy,True))
|
|
201
|
+
http=next(c for w,c in tree if w==['http'])
|
|
202
|
+
server=next(c for w,c in http if w==['server'])
|
|
203
|
+
# The captured HTTPS server is the released bootstrap minus HTTP listen.
|
|
204
|
+
server.remove([['listen','80','default_server'],None])
|
|
205
|
+
name=next(n for n in server if n[0]==['server_name','_'])
|
|
206
|
+
name[0][1]=domain
|
|
207
|
+
guard=parse_nginx(('if ($host != '+domain+') { return 444; }').encode())[0]
|
|
208
|
+
server.insert(server.index(name)+1,guard)
|
|
209
|
+
auth=next(n for n in server if n[0]==['location','=','/_auth_check'])
|
|
210
|
+
protected=parse_nginx(b"""
|
|
211
|
+
location = /auth/headers { return 404; }
|
|
212
|
+
location = /auth/headers/ { return 404; }
|
|
213
|
+
location = /auth/exporters/authorize { return 404; }
|
|
214
|
+
location = /auth/exporters/authorize/ { return 404; }
|
|
215
|
+
location = /_exporter_admin {
|
|
216
|
+
internal;
|
|
217
|
+
proxy_pass http://auth_server/auth/exporters/authorize;
|
|
218
|
+
proxy_pass_request_body off;
|
|
219
|
+
proxy_set_header Content-Length "";
|
|
220
|
+
proxy_set_header Cookie $http_cookie;
|
|
221
|
+
proxy_set_header X-Original-URI $request_uri;
|
|
222
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
223
|
+
proxy_pass_header X-Mgmt-Token;
|
|
224
|
+
}
|
|
225
|
+
""")
|
|
226
|
+
index=server.index(auth)+1;server[index:index]=protected
|
|
227
|
+
login=next(c for w,c in server if w==['location','/auth/'])
|
|
228
|
+
upstream=next(n for n in login if n[0]==['proxy_pass','http://auth_server/auth/'])
|
|
229
|
+
index=login.index(upstream)+1
|
|
230
|
+
login[index:index]=parse_nginx(b'proxy_hide_header X-Mgmt-Token; proxy_cookie_flags fleet_session secure httponly samesite=lax;')
|
|
231
|
+
for route in ('^/vastai-api/(.*)$','^/runpod-api/(.*)$'):
|
|
232
|
+
children=next(c for w,c in server if w==['location','~',route])
|
|
233
|
+
request=next(w for w,c in children if w==['auth_request','/_auth_check'])
|
|
234
|
+
request[1]='/_exporter_admin'
|
|
235
|
+
public=parse_nginx(('server { listen 80 default_server; server_name '+domain+'; if ($host != '+domain+') { return 444; } location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; default_type text/plain; try_files $uri =404; } location / { return 308 https://'+domain+'$request_uri; } }').encode())[0]
|
|
236
|
+
public[1].extend(parse_nginx(get_jinja_env().get_template('vast-price-manager-auth-blocks.conf.j2').render().encode()))
|
|
237
|
+
index=next(i for i,n in enumerate(http) if n[0]==['server'])
|
|
238
|
+
http.insert(index,public)
|
|
239
|
+
http.extend(parse_nginx(render_host_forward_groups(declaration.get('host_forward_groups',[]),legacy_profile=True)))
|
|
240
|
+
return tree
|
|
179
241
|
|
|
180
242
|
|
|
181
243
|
class AdoptionEngine(DockerEngine):
|
|
@@ -409,10 +471,20 @@ while True: time.sleep(60)
|
|
|
409
471
|
self._request('POST','/images/create?fromImage='+quote(reference,safe=''),allowed=(200,))
|
|
410
472
|
def validate(self,image,candidate,inspect,identity):
|
|
411
473
|
CustomConfigMigrator(candidate.parent,candidate.parent,engine=self)._validate_candidate(image,candidate,inspect,identity)
|
|
412
|
-
def anonymous_auth(self,name,tls):
|
|
474
|
+
def anonymous_auth(self,name,tls,domain=None):
|
|
413
475
|
# Internal fixed executable probes both auth server and public Nginx.
|
|
414
476
|
import subprocess
|
|
415
477
|
probe="import http.client; c=http.client.HTTPConnection('127.0.0.1',8081,timeout=5); c.request('GET','/auth/exporters/authorize'); assert c.getresponse().status==401; c.close(); c=http.client.HTTPSConnection('127.0.0.1',443,context=__import__('ssl')._create_unverified_context(),timeout=5); c.request('GET','/api/versions'); assert c.getresponse().status==401; c.close(); c=http.client.HTTPSConnection('127.0.0.1',443,context=__import__('ssl')._create_unverified_context(),timeout=5); c.request('GET','/auth/vast-price-manager/session'); assert c.getresponse().status==404"
|
|
478
|
+
if domain is not None:
|
|
479
|
+
validate_hostname_policy('primary-only',domain)
|
|
480
|
+
# Connect only to loopback, while TLS SNI and HTTP Host use the
|
|
481
|
+
# same declared literal. Preserve existing local unverified probe.
|
|
482
|
+
connect="class PrimaryHTTPS(http.client.HTTPSConnection):\n def connect(self):\n sock=__import__('socket').create_connection(('127.0.0.1',443),self.timeout); self.sock=self._context.wrap_socket(sock,server_hostname=self.host)\n"
|
|
483
|
+
probe=probe.replace("c=http.client.HTTPSConnection('127.0.0.1',443,", "c=PrimaryHTTPS("+repr(domain)+",443,")
|
|
484
|
+
certificate='/etc/letsencrypt/live/'+domain+'/fullchain.pem' if tls=='letsencrypt' else '/etc/nginx/ssl/server.crt'
|
|
485
|
+
probe=probe.replace("context=__import__('ssl')._create_unverified_context()", "context=ctx")
|
|
486
|
+
connect+="ctx=__import__('ssl').create_default_context(); ctx.load_verify_locations(cafile="+repr(certificate)+");\n"
|
|
487
|
+
probe=probe.replace("import http.client; ", "import http.client\n"+connect+"\n")
|
|
416
488
|
try:
|
|
417
489
|
result=subprocess.run(['docker','exec',name,'python3','-c',probe],capture_output=True,timeout=self.timeout)
|
|
418
490
|
return result.returncode==0
|
|
@@ -500,7 +572,7 @@ class StandardAdoption:
|
|
|
500
572
|
def visit(items):
|
|
501
573
|
for words,children in items:
|
|
502
574
|
if words[0]=='server_name':
|
|
503
|
-
if words[1:]
|
|
575
|
+
if words[1:]==[declaration['domain']] or words[1:]==['_'] and not declaration.get('hostname_policy'): words[1:]=['DECLARED_DOMAIN']
|
|
504
576
|
elif words[1:] not in [g['hostnames'] for g in declaration.get('host_forward_groups',[])]: raise AdoptionError('source_domain_unmapped')
|
|
505
577
|
if children is not None: visit(children)
|
|
506
578
|
visit(nodes); return nodes
|
|
@@ -520,14 +592,22 @@ class StandardAdoption:
|
|
|
520
592
|
server[index:index]=parse_nginx(render_acme_challenge(declaration['acme_challenge'],legacy_profile=True))
|
|
521
593
|
next(children for words,children in profile_tree if words==['http']).extend(deepcopy(forwarding_tree))
|
|
522
594
|
profile_trees.append(profile_tree)
|
|
523
|
-
if any(actual==normalize(p) for p in profile_trees):
|
|
595
|
+
if not declaration.get('hostname_policy') and any(actual==normalize(p) for p in profile_trees):
|
|
524
596
|
required={'ipmi-monitor','dc-overview','grafana','prometheus','vastai-exporter','runpod-exporter'}
|
|
525
597
|
if b'location = /vast-pricing' in active: required.add('vast-price-manager')
|
|
526
598
|
if not required<=set(declaration['services']) or declaration['tls']!='self-signed': raise AdoptionError('legacy_services_or_tls_unmapped')
|
|
527
599
|
for name in required:
|
|
528
600
|
if any(declaration['services'][name][k]!=DEFAULT_SERVICES[name][k] for k in ('container_name','port','path')): raise AdoptionError('legacy_target_unmapped')
|
|
529
601
|
return 'released-bootstrap-1.2.0'
|
|
530
|
-
|
|
602
|
+
if declaration.get('hostname_policy')=='primary-only' and declaration.get('http_policy')=='redirect-primary-https' and declaration['tls']=='self-signed' and not declaration.get('acme_challenge'):
|
|
603
|
+
guarded=_guarded_bootstrap_profile(legacy,declaration)
|
|
604
|
+
if actual==normalize(guarded):
|
|
605
|
+
required={'ipmi-monitor','dc-overview','grafana','prometheus','vastai-exporter','runpod-exporter','vast-price-manager'}
|
|
606
|
+
if not required<=set(declaration['services']): raise AdoptionError('legacy_services_or_tls_unmapped')
|
|
607
|
+
for name in required:
|
|
608
|
+
if any(declaration['services'][name][k]!=DEFAULT_SERVICES[name][k] for k in ('container_name','port','path')): raise AdoptionError('legacy_target_unmapped')
|
|
609
|
+
return 'released-bootstrap-primary-only'
|
|
610
|
+
supported=[expected]+([normalize(p) for p in profile_trees] if not declaration.get('hostname_policy') else [])
|
|
531
611
|
actual_words=list(_walk(actual)); actual_set={tuple(w) for w in actual_words}
|
|
532
612
|
closest=min(supported,key=lambda profile:len(actual_set^{tuple(w) for w in _walk(profile)}))
|
|
533
613
|
known={tuple(w) for w in _walk(closest)}
|
|
@@ -538,6 +618,7 @@ class StandardAdoption:
|
|
|
538
618
|
def plan(self,settings,*,environment=None):
|
|
539
619
|
declaration=_declaration(settings); source=self.engine.inspect('cryptolabs-proxy')
|
|
540
620
|
if not source.get('State',{}).get('Running'): raise AdoptionError('source_not_running')
|
|
621
|
+
healthcheck=_policy_healthcheck(source,declaration)
|
|
541
622
|
active=self.engine.read_config('cryptolabs-proxy')
|
|
542
623
|
recorded=ServiceRegistry(self.config_dir).config.get('standard_adoption',{})
|
|
543
624
|
source_declaration=recorded.get('declaration',declaration)
|
|
@@ -568,6 +649,11 @@ class StandardAdoption:
|
|
|
568
649
|
'declaration_sha256':sha256(_canonical(declaration)),'candidate_sha256':sha256(candidate),'selection_id':snapshot['selection_id'],'selection_sha256':snapshot['selection_sha256'],
|
|
569
650
|
'manifest_sha256':snapshot['manifest_sha256'],'image':artifact['reference'],'platform':platform,'origin':artifact['origin'],'source_profile':profile,
|
|
570
651
|
'environment_sha256':sha256(_canonical(environment or {}))}
|
|
652
|
+
if healthcheck is not None:
|
|
653
|
+
original=source.get('Config',{}).get('Healthcheck')
|
|
654
|
+
plan['healthcheck_sha256']=sha256(_canonical(healthcheck))
|
|
655
|
+
plan['source_healthcheck_sha256']=sha256(_canonical(original))
|
|
656
|
+
plan['healthcheck_action']='preserved-verified-source' if original==healthcheck else 'generated-policy-probe'
|
|
571
657
|
plan['plan_id']=sha256(_canonical(plan))[:24]
|
|
572
658
|
return plan
|
|
573
659
|
|
|
@@ -612,6 +698,8 @@ class StandardAdoption:
|
|
|
612
698
|
auth_mount=next(m for m in source['Mounts'] if m['Destination']=='/data/auth')
|
|
613
699
|
clone['Mounts'].append({'Type':'bind','Source':declaration['release_selection_dir'],'Destination':'/etc/cryptolabs/releases','RW':False})
|
|
614
700
|
nginx=self.config_dir/'nginx.conf'; request=build_recreate_request(clone,plan['image'],str(nginx))
|
|
701
|
+
if declaration.get('hostname_policy') or declaration.get('http_policy'):
|
|
702
|
+
request['Config']['Healthcheck']=_policy_healthcheck(source,declaration)
|
|
615
703
|
config=request['Config']; config['Labels']=deepcopy(config.get('Labels') or {})
|
|
616
704
|
config['Labels'].update({'com.centurylinklabs.watchtower.enable':'false','io.cryptolabs.update.origin':plan['origin'],'io.cryptolabs.update.reference':plan['image']})
|
|
617
705
|
for label in declaration['obsolete_owner_labels']: config['Labels'].pop(label,None)
|
|
@@ -698,7 +786,7 @@ class StandardAdoption:
|
|
|
698
786
|
guard()
|
|
699
787
|
def candidate_health(name):
|
|
700
788
|
guard()
|
|
701
|
-
ready=wait_for_proxy_ready(engine,name,lambda:engine.anonymous_auth(name,declaration['tls']),PROXY_READY_TIMEOUT)
|
|
789
|
+
ready=wait_for_proxy_ready(engine,name,lambda:engine.anonymous_auth(name,declaration['tls'],declaration['domain']) if declaration.get('hostname_policy') or declaration.get('http_policy') else engine.anonymous_auth(name,declaration['tls']),PROXY_READY_TIMEOUT)
|
|
702
790
|
if not ready: engine.stop(name,ignore_missing=True)
|
|
703
791
|
guard()
|
|
704
792
|
return ready
|
|
@@ -715,7 +803,12 @@ class StandardAdoption:
|
|
|
715
803
|
# proven candidate/config and private recovery record for repair.
|
|
716
804
|
registry.services=declaration['services']; registry.config.pop('custom_config',None)
|
|
717
805
|
registry.config.update(domain=declaration['domain'],letsencrypt=declaration['tls']=='letsencrypt',release_selection_dir=declaration['release_selection_dir'],host_forward_groups=declaration.get('host_forward_groups',[]),acme_challenge=declaration.get('acme_challenge'))
|
|
806
|
+
for name in ('hostname_policy','http_policy'):
|
|
807
|
+
if name in declaration: registry.config[name]=declaration[name]
|
|
808
|
+
else: registry.config.pop(name,None)
|
|
718
809
|
registry.config['standard_adoption']={'plan_id':identity,'serving_sha256':_runtime_identity(engine.inspect('cryptolabs-proxy')),'declaration_sha256':plan['declaration_sha256'],'selection_sha256':plan['selection_sha256'],'auth_mount':{k:auth_mount[k] for k in ('Type','Name','Source','Destination') if k in auth_mount},'declaration':declaration}
|
|
810
|
+
if declaration.get('hostname_policy') or declaration.get('http_policy'):
|
|
811
|
+
registry.config['standard_adoption']['healthcheck']=deepcopy(request['Config']['Healthcheck'])
|
|
719
812
|
try: registry.save()
|
|
720
813
|
except OSError:
|
|
721
814
|
write(backup/'outcome.json',_canonical({'state':'candidate_retained','reason':'registry_forward_repair_required'}))
|
|
@@ -750,6 +843,8 @@ class StandardAdoption:
|
|
|
750
843
|
declaration=deepcopy(registry.config['standard_adoption']['declaration'])
|
|
751
844
|
declaration.update(domain=config.domain,tls='letsencrypt' if config.use_letsencrypt else 'self-signed',services={n:{k:v for k,v in s.items() if k in ('container_name','path','port','display_name')} for n,s in registry.services.items()},obsolete_overlays=[],obsolete_owner_labels=[])
|
|
752
845
|
if config.release_selection_dir: declaration['release_selection_dir']=str(config.release_selection_dir)
|
|
846
|
+
if config.hostname_policy is not None: declaration['hostname_policy']=config.hostname_policy
|
|
847
|
+
if config.http_policy is not None: declaration['http_policy']=config.http_policy
|
|
753
848
|
source=self.engine.inspect('cryptolabs-proxy'); current={e.split('=',1)[0]:e.split('=',1)[1] for e in source['Config'].get('Env',[]) if '=' in e}
|
|
754
849
|
environment={}
|
|
755
850
|
for name,value in [('FLEET_ADMIN_USER',config.fleet_admin_user),('FLEET_ADMIN_PASS',config.fleet_admin_pass),('SITE_NAME',config.site_name),('WATCHDOG_API_KEY',config.watchdog_api_key),('WATCHDOG_URL',config.watchdog_url)]:
|
|
@@ -758,8 +853,17 @@ class StandardAdoption:
|
|
|
758
853
|
# default generated values in ProxyConfig are not rotation intent.
|
|
759
854
|
normalized=_declaration(declaration)
|
|
760
855
|
selected=targets(load(normalized['release_selection_dir']),self.engine.image(source['Image'])['Os']+'/'+self.engine.image(source['Image'])['Architecture'])['cryptolabs-proxy']
|
|
856
|
+
health_current=True
|
|
857
|
+
if normalized.get('hostname_policy') or normalized.get('http_policy'):
|
|
858
|
+
# No-op is a positive preservation result; validate both the
|
|
859
|
+
# runtime probe and its saved policy metadata before claiming it.
|
|
860
|
+
runtime_health=_policy_healthcheck(source,normalized)
|
|
861
|
+
recorded_health=registry.config['standard_adoption'].get('healthcheck')
|
|
862
|
+
if recorded_health is None: raise AdoptionError('recorded_healthcheck_unmapped')
|
|
863
|
+
saved_health=_policy_healthcheck({'Config':{'Healthcheck':recorded_health}},normalized)
|
|
864
|
+
health_current=(runtime_health==source['Config'].get('Healthcheck') and saved_health==recorded_health and runtime_health==saved_health)
|
|
761
865
|
labels=source['Config'].get('Labels') or {}
|
|
762
|
-
if not environment and sha256(self.engine.read_config('cryptolabs-proxy'))==sha256(render_standard(normalized)) and labels.get('io.cryptolabs.update.reference')==selected['reference'] and labels.get('com.centurylinklabs.watchtower.enable')=='false':
|
|
866
|
+
if health_current and not environment and sha256(self.engine.read_config('cryptolabs-proxy'))==sha256(render_standard(normalized)) and labels.get('io.cryptolabs.update.reference')==selected['reference'] and labels.get('com.centurylinklabs.watchtower.enable')=='false':
|
|
763
867
|
return True,'Adopted proxy configuration already current; state preserved'
|
|
764
868
|
plan=self.plan(declaration,environment=environment)
|
|
765
869
|
result=self.adopt(declaration,plan['plan_id'],environment=environment)
|
|
@@ -774,5 +878,5 @@ class StandardAdoption:
|
|
|
774
878
|
registry=ServiceRegistry(self.config_dir)
|
|
775
879
|
source=self.engine.inspect('cryptolabs-proxy')
|
|
776
880
|
environment={e.split('=',1)[0]:e.split('=',1)[1] for e in source['Config'].get('Env',[]) if '=' in e}
|
|
777
|
-
config=ProxyConfig(registry.config['domain'],use_letsencrypt=registry.config.get('letsencrypt',False),fleet_admin_user=environment.get('FLEET_ADMIN_USER','admin'),fleet_admin_pass=environment.get('FLEET_ADMIN_PASS',''),site_name=environment.get('SITE_NAME',''),watchdog_api_key=environment.get('WATCHDOG_API_KEY',''),watchdog_url=environment.get('WATCHDOG_URL',''),release_selection_dir=Path(registry.config['release_selection_dir']))
|
|
881
|
+
config=ProxyConfig(registry.config['domain'],use_letsencrypt=registry.config.get('letsencrypt',False),fleet_admin_user=environment.get('FLEET_ADMIN_USER','admin'),fleet_admin_pass=environment.get('FLEET_ADMIN_PASS',''),site_name=environment.get('SITE_NAME',''),watchdog_api_key=environment.get('WATCHDOG_API_KEY',''),watchdog_url=environment.get('WATCHDOG_URL',''),release_selection_dir=Path(registry.config['release_selection_dir']),hostname_policy=registry.config.get('hostname_policy'),http_policy=registry.config.get('http_policy'))
|
|
778
882
|
return self.reconfigure(config)
|
|
@@ -19,6 +19,8 @@ services:
|
|
|
19
19
|
{% endif %}
|
|
20
20
|
{% if use_letsencrypt %}
|
|
21
21
|
- ./certbot/conf:/etc/letsencrypt:ro
|
|
22
|
+
{% endif %}
|
|
23
|
+
{% if use_letsencrypt or http_policy %}
|
|
22
24
|
- ./certbot/www:/var/www/certbot:ro
|
|
23
25
|
{% endif %}
|
|
24
26
|
environment:
|
|
@@ -29,10 +31,20 @@ services:
|
|
|
29
31
|
networks:
|
|
30
32
|
- cryptolabs
|
|
31
33
|
healthcheck:
|
|
34
|
+
{% if policy_healthcheck %}
|
|
35
|
+
test: ["CMD-SHELL", {{ policy_healthcheck | tojson }}]
|
|
36
|
+
{% else %}
|
|
32
37
|
test: ["CMD", "curl", "-f", "http://localhost/api/health"]
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
{% endif %}
|
|
39
|
+
interval: {{ (policy_healthcheck_config.Interval | string + 'ns') if policy_healthcheck_config and 'Interval' in policy_healthcheck_config else '30s' }}
|
|
40
|
+
timeout: {{ (policy_healthcheck_config.Timeout | string + 'ns') if policy_healthcheck_config and 'Timeout' in policy_healthcheck_config else '10s' }}
|
|
41
|
+
retries: {{ policy_healthcheck_config.Retries if policy_healthcheck_config and 'Retries' in policy_healthcheck_config else 3 }}
|
|
42
|
+
{% if policy_healthcheck_config and 'StartPeriod' in policy_healthcheck_config %}
|
|
43
|
+
start_period: {{ policy_healthcheck_config.StartPeriod }}ns
|
|
44
|
+
{% endif %}
|
|
45
|
+
{% if policy_healthcheck_config and 'StartInterval' in policy_healthcheck_config %}
|
|
46
|
+
start_interval: {{ policy_healthcheck_config.StartInterval }}ns
|
|
47
|
+
{% endif %}
|
|
36
48
|
labels:
|
|
37
49
|
com.centurylinklabs.watchtower.enable: "{{ 'false' if release_selection_dir else 'true' }}"
|
|
38
50
|
{% if release_selection_dir %}
|
{cryptolabs_proxy-1.4.0 → cryptolabs_proxy-1.5.0}/src/cryptolabs_proxy/templates/nginx.conf.j2
RENAMED
|
@@ -28,12 +28,28 @@ http {
|
|
|
28
28
|
# Rate limiting
|
|
29
29
|
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
|
30
30
|
|
|
31
|
-
{% if letsencrypt %}
|
|
31
|
+
{% if letsencrypt or http_policy %}
|
|
32
32
|
# Redirect HTTP to HTTPS
|
|
33
33
|
server {
|
|
34
|
-
listen 80;
|
|
34
|
+
listen 80{% if hostname_policy %} default_server{% endif %};
|
|
35
35
|
server_name {{ domain }};
|
|
36
|
+
{% if hostname_policy %}
|
|
37
|
+
if ($host != {{ domain }}) {
|
|
38
|
+
return 444;
|
|
39
|
+
}
|
|
40
|
+
{% endif %}
|
|
36
41
|
|
|
42
|
+
{% if http_policy %}
|
|
43
|
+
location ^~ /.well-known/acme-challenge/ {
|
|
44
|
+
root /var/www/certbot;
|
|
45
|
+
default_type text/plain;
|
|
46
|
+
try_files $uri =404;
|
|
47
|
+
}
|
|
48
|
+
location / {
|
|
49
|
+
return 308 https://{{ domain }}$request_uri;
|
|
50
|
+
}
|
|
51
|
+
{% include "vast-price-manager-auth-blocks.conf.j2" %}
|
|
52
|
+
{% else %}
|
|
37
53
|
location /.well-known/acme-challenge/ {
|
|
38
54
|
root /var/www/certbot;
|
|
39
55
|
}
|
|
@@ -41,23 +57,41 @@ http {
|
|
|
41
57
|
location / {
|
|
42
58
|
return 301 https://$host$request_uri;
|
|
43
59
|
}
|
|
60
|
+
{% endif %}
|
|
44
61
|
}
|
|
45
62
|
|
|
46
63
|
server {
|
|
47
|
-
listen 443 ssl http2;
|
|
64
|
+
listen 443 ssl http2{% if hostname_policy %} default_server{% endif %};
|
|
48
65
|
server_name {{ domain }};
|
|
66
|
+
{% if hostname_policy %}
|
|
67
|
+
if ($host != {{ domain }}) {
|
|
68
|
+
return 444;
|
|
69
|
+
}
|
|
70
|
+
{% endif %}
|
|
49
71
|
|
|
72
|
+
{% if letsencrypt %}
|
|
50
73
|
ssl_certificate /etc/letsencrypt/live/{{ domain }}/fullchain.pem;
|
|
51
74
|
ssl_certificate_key /etc/letsencrypt/live/{{ domain }}/privkey.pem;
|
|
52
75
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
53
76
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
|
54
77
|
ssl_prefer_server_ciphers off;
|
|
55
78
|
ssl_session_cache shared:SSL:10m;
|
|
79
|
+
{% else %}
|
|
80
|
+
ssl_certificate /etc/nginx/ssl/server.crt;
|
|
81
|
+
ssl_certificate_key /etc/nginx/ssl/server.key;
|
|
82
|
+
ssl_protocols TLSv1.2 TLSv1.3;
|
|
83
|
+
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
84
|
+
{% endif %}
|
|
56
85
|
{% else %}
|
|
57
86
|
server {
|
|
58
|
-
listen 80;
|
|
59
|
-
listen 443 ssl http2;
|
|
87
|
+
listen 80{% if hostname_policy %} default_server{% endif %};
|
|
88
|
+
listen 443 ssl http2{% if hostname_policy %} default_server{% endif %};
|
|
60
89
|
server_name {{ domain }};
|
|
90
|
+
{% if hostname_policy %}
|
|
91
|
+
if ($host != {{ domain }}) {
|
|
92
|
+
return 444;
|
|
93
|
+
}
|
|
94
|
+
{% endif %}
|
|
61
95
|
|
|
62
96
|
ssl_certificate /etc/nginx/ssl/server.crt;
|
|
63
97
|
ssl_certificate_key /etc/nginx/ssl/server.key;
|
|
@@ -91,6 +125,10 @@ http {
|
|
|
91
125
|
# Auth server endpoints (public)
|
|
92
126
|
location /auth/ {
|
|
93
127
|
proxy_pass http://127.0.0.1:8081/auth/;
|
|
128
|
+
{% if http_policy %}
|
|
129
|
+
proxy_hide_header X-Mgmt-Token;
|
|
130
|
+
proxy_cookie_flags fleet_session secure httponly samesite=lax;
|
|
131
|
+
{% endif %}
|
|
94
132
|
proxy_http_version 1.1;
|
|
95
133
|
proxy_set_header Host $host;
|
|
96
134
|
proxy_set_header X-Real-IP $remote_addr;
|
|
@@ -98,6 +136,12 @@ http {
|
|
|
98
136
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
99
137
|
}
|
|
100
138
|
|
|
139
|
+
{% if http_policy %}
|
|
140
|
+
location = /auth/headers { return 404; }
|
|
141
|
+
location = /auth/headers/ { return 404; }
|
|
142
|
+
location = /auth/exporters/authorize { return 404; }
|
|
143
|
+
location = /auth/exporters/authorize/ { return 404; }
|
|
144
|
+
{% endif %}
|
|
101
145
|
# Internal auth verification endpoint (for auth_request)
|
|
102
146
|
location = /_auth_check {
|
|
103
147
|
internal;
|