build(rc06): wire hardened publication facade

This commit is contained in:
2026-10-07 01:22:14 +08:00
parent 87a0f27db3
commit 7ac4cf4052
4 changed files with 321 additions and 0 deletions
@@ -0,0 +1,194 @@
from __future__ import annotations
from pathlib import Path
import pytest
import yaml
from yuqei_contract_api.config import (
ContractApiSettings,
validate_rc06_publication_startup,
)
from yuqei_contract_api.main import create_app
REPO_ROOT = Path(__file__).resolve().parents[4]
class _ComposeLoader(yaml.SafeLoader):
pass
_ComposeLoader.add_constructor("!reset", lambda _loader, _node: None)
def _write(path: Path, value: str) -> str:
path.write_text(value, encoding="utf-8")
return str(path.resolve())
def _facade_settings(tmp_path: Path, **overrides) -> ContractApiSettings:
values = {
"environment": "production",
"database_url": f"sqlite:///{tmp_path / 'must-not-be-created.sqlite3'}",
"database_auto_create": False,
"seed_demo_data": False,
"ai_platform_base_url": "https://ai-platform.internal.example.com",
"ai_platform_service_key": "general-ai-key-000000000000000000000001",
"ai_platform_rc06_publication_key": "rc06-ai-key-00000000000000000000000001",
"web_proxy_shared_secret": "web-proxy-key-000000000000000000000001",
"rc06_publication_facade_enabled": True,
"rc06_low_level_coordinator_http_enabled": False,
"rc06_publication_environment_id": "production-cn-1",
"rc06_publication_tenant_id": "rc06-production-tenant",
"rc06_publication_bearer_token_file": _write(
tmp_path / "facade.token", "facade-key-0000000000000000000000000001"
),
"rc06_frozen_candidate_commit": "a" * 40,
"rc06_release_image_digest": "sha256:" + "b" * 64,
"rc06_ai_release_image_digest": "sha256:" + "c" * 64,
"rc06_approval_root_public_key_file": _write(
tmp_path / "approval-root.pem", "approval-root-public-material"
),
"rc06_approval_root_public_key_sha256": "d" * 64,
"rc06_approval_root_key_id": "yuqei-rc06-approval-root-v1",
"rc06_approval_keyring_epoch": 1,
"rc06_apply_service_public_key_file": _write(
tmp_path / "apply-service.pem", "apply-service-public-material"
),
"rc06_apply_service_public_key_sha256": "e" * 64,
"rc06_legal_review_keyring_sha256": "f" * 64,
"rc06_online_signing_provider": "fixture-kms",
"rc06_online_signing_key_reference": "kms://fixture/rc06-online",
"rc06_online_signing_key_version": "version-1",
"rc06_online_signing_algorithm": "RSA_PSS_SHA_256",
"rc06_online_signing_public_key_spki_sha256": "1" * 64,
"reliable_evidence_signing_backend": "kms_gateway",
"reliable_evidence_signing_endpoint": "https://kms.internal.example.com",
"reliable_evidence_signing_credential_file": _write(
tmp_path / "kms.credential", "kms-credential-material"
),
"reliable_evidence_signing_ca_file": _write(
tmp_path / "kms-ca.pem", "kms-ca-public-material"
),
}
values.update(overrides)
return ContractApiSettings(**values)
def test_enabled_facade_startup_checks_all_independent_trust_boundaries(tmp_path: Path) -> None:
validate_rc06_publication_startup(_facade_settings(tmp_path))
with pytest.raises(RuntimeError, match="low-level coordinator HTTP routes"):
create_app(
_facade_settings(
tmp_path,
rc06_low_level_coordinator_http_enabled=True,
rc06_publication_shared_secret="low-level-key-000000000000000000000001",
)
)
assert not (tmp_path / "must-not-be-created.sqlite3").exists()
with pytest.raises(RuntimeError, match="must be independent"):
validate_rc06_publication_startup(
_facade_settings(
tmp_path,
ai_platform_rc06_publication_key=(
tmp_path / "facade.token"
).read_text(encoding="utf-8"),
)
)
with pytest.raises(RuntimeError, match="KMS evidence signing requires"):
validate_rc06_publication_startup(
_facade_settings(tmp_path, reliable_evidence_signing_endpoint="")
)
bearer = (tmp_path / "facade.token").read_text(encoding="utf-8")
with pytest.raises(RuntimeError, match="independent from the KMS credential"):
validate_rc06_publication_startup(
_facade_settings(
tmp_path,
reliable_evidence_signing_credential_file=_write(
tmp_path / "reused-kms.credential", bearer
),
)
)
def test_production_compose_and_edge_expose_only_the_hardened_facade() -> None:
compose_path = REPO_ROOT / "yuqei-infra/compose/docker-compose.production.yml"
model = yaml.load(compose_path.read_text(encoding="utf-8"), Loader=_ComposeLoader)
contract = model["services"]["contract-api"]
assert "ports" in contract and contract["ports"] is None
contract_env = contract["environment"]
assert contract_env["CONTRACT_API_RC06_PUBLICATION_FACADE_ENABLED"] == "true"
assert contract_env["CONTRACT_API_RC06_LOW_LEVEL_COORDINATOR_HTTP_ENABLED"] == "false"
assert contract_env["CONTRACT_API_RC06_PUBLICATION_TENANT_ID"].startswith(
"${RC06_PUBLICATION_TENANT_ID:"
)
assert contract_env["CONTRACT_API_RC06_PUBLICATION_BEARER_TOKEN_FILE"] == (
"/run/secrets/rc06_facade_bearer_token"
)
assert contract_env["CONTRACT_API_RELIABLE_EVIDENCE_SIGNING_BACKEND"] == "kms_gateway"
assert contract_env["CONTRACT_API_RC06_AI_RELEASE_IMAGE_DIGEST"].startswith(
"${RC06_AI_RELEASE_IMAGE_DIGEST:"
)
assert {item["source"] for item in contract["secrets"]} == {
"rc06_facade_bearer_token",
"rc06_kms_credential",
}
assert all(
mount.endswith(":ro")
for mount in contract["volumes"]
if "/run/yuqei/rc06/" in mount
)
assert "config_status" in " ".join(contract["healthcheck"]["test"])
ai_env = model["services"]["ai-platform-api"]["environment"]
assert (
"ports" in model["services"]["ai-platform-api"]
and model["services"]["ai-platform-api"]["ports"] is None
)
assert ai_env["AI_PLATFORM_RC06_PUBLICATION_SHARED_SECRET"].startswith(
"${RC06_AI_PLATFORM_PUBLICATION_SERVICE_KEY:"
)
assert ai_env["AI_PLATFORM_RC06_FROZEN_CANDIDATE_COMMIT"].startswith(
"${RC06_FROZEN_CANDIDATE_COMMIT:"
)
assert ai_env["AI_PLATFORM_RC06_RELEASE_IMAGE_DIGEST"].startswith(
"${RC06_AI_RELEASE_IMAGE_DIGEST:"
)
edge = model["services"]["edge"]
assert edge["environment"]["RC06_PUBLICATION_TENANT_ID"].startswith(
"${RC06_PUBLICATION_TENANT_ID:"
)
nginx = (
REPO_ROOT / "yuqei-infra/nginx/templates/default.conf.template"
).read_text(encoding="utf-8")
assert "location = /internal/rc06/publication-transactions/apply" in nginx
assert "client_max_body_size 16m;" in nginx
assert "proxy_set_header X-RC06-Tenant-ID ${RC06_PUBLICATION_TENANT_ID};" in nginx
assert "location ~ ^/contract/api/v1/internal/rc06-publications" in nginx
assert "location ~ ^/ai/api/v1/internal/rc06-publications" in nginx
facade_block = nginx.split(
"location = /internal/rc06/publication-transactions/apply", 1
)[1].split("location ~", 1)[0]
assert "contract_api_upstream" in facade_block
assert "ai_platform_upstream" not in facade_block
env_example = (
REPO_ROOT / "yuqei-infra/compose/.env.production.example"
).read_text(encoding="utf-8")
for key in (
"RC06_FACADE_BEARER_TOKEN_FILE",
"RC06_APPROVAL_ROOT_PUBLIC_KEY_FILE_HOST",
"RC06_APPLY_SERVICE_PUBLIC_KEY_FILE_HOST",
"RC06_KMS_CREDENTIAL_FILE",
"RC06_KMS_CA_FILE_HOST",
"RC06_AI_PLATFORM_PUBLICATION_SERVICE_KEY",
):
assert f"{key}=" in env_example
compose_source = compose_path.read_text(encoding="utf-8")
assert compose_source.count("ports: !reset []") == 2
@@ -145,6 +145,34 @@ CONTRACT_API_ANTI_ABUSE_BYTES_DAILY_LIMIT=524288000
CONTRACT_API_BATCH_DOWNLOAD_MAX_FILES=20
CONTRACT_API_BATCH_DOWNLOAD_MAX_BYTES=104857600
# RC06 public publication facade. All hashes and deployment identities must be
# regenerated from one frozen candidate; CHANGE_ME values intentionally fail
# startup/compose interpolation. The three service credentials below are
# independent and must not be reused by the web proxy or general AI transport.
RC06_AI_PLATFORM_BASE_URL=https://ai-platform-api.internal.example.com
RC06_AI_PLATFORM_PUBLICATION_SERVICE_KEY=CHANGE_ME_USE_A_DEDICATED_RANDOM_32_BYTE_SECRET
RC06_PUBLICATION_ENVIRONMENT_ID=CHANGE_ME_STABLE_RC06_ENVIRONMENT_ID
RC06_PUBLICATION_TENANT_ID=CHANGE_ME_FIXED_RC06_TENANT_ID
RC06_FACADE_BEARER_TOKEN_FILE=/etc/yuqei/secrets/rc06-facade-bearer-token
RC06_FROZEN_CANDIDATE_COMMIT=CHANGE_ME_40_OR_64_LOWERCASE_HEX_COMMIT
RC06_CONTRACT_RELEASE_IMAGE_DIGEST=sha256:CHANGE_ME_64_HEX_DIGEST
RC06_AI_RELEASE_IMAGE_DIGEST=sha256:CHANGE_ME_64_HEX_DIGEST
RC06_APPROVAL_ROOT_PUBLIC_KEY_FILE_HOST=/etc/yuqei/rc06/approval-root-public.pem
RC06_APPROVAL_ROOT_PUBLIC_KEY_SHA256=CHANGE_ME_64_HEX_SHA256
RC06_APPROVAL_ROOT_KEY_ID=CHANGE_ME_FROZEN_APPROVAL_ROOT_KEY_ID
RC06_APPROVAL_KEYRING_EPOCH=CHANGE_ME_POSITIVE_INTEGER
RC06_APPLY_SERVICE_PUBLIC_KEY_FILE_HOST=/etc/yuqei/rc06/apply-service-public.pem
RC06_APPLY_SERVICE_PUBLIC_KEY_SHA256=CHANGE_ME_64_HEX_SHA256
RC06_LEGAL_REVIEW_KEYRING_SHA256=CHANGE_ME_64_HEX_SHA256
RC06_ONLINE_SIGNING_PROVIDER=CHANGE_ME_KMS_PROVIDER
RC06_ONLINE_SIGNING_KEY_REFERENCE=CHANGE_ME_KMS_SIGNING_KEY_REFERENCE
RC06_ONLINE_SIGNING_KEY_VERSION=CHANGE_ME_IMMUTABLE_KMS_KEY_VERSION
RC06_ONLINE_SIGNING_ALGORITHM=RSA_PSS_SHA_256
RC06_ONLINE_SIGNING_PUBLIC_KEY_SPKI_SHA256=CHANGE_ME_64_HEX_SHA256
RC06_KMS_SIGNING_ENDPOINT=https://kms-gateway.internal.example.com
RC06_KMS_CREDENTIAL_FILE=/etc/yuqei/secrets/rc06-kms-credential
RC06_KMS_CA_FILE_HOST=/etc/yuqei/rc06/kms-ca.pem
# Enterprise dual-cloud evidence trust is a paid optional capability. Keep it
# disabled for the core product release; a dedicated UAT overlay sets required.
CONTRACT_API_RELIABLE_EVIDENCE_EXTERNAL_TRUST_MODE=disabled
@@ -28,9 +28,38 @@ services:
contract-api:
image: ${CONTRACT_API_IMAGE:?CONTRACT_API_IMAGE must reference an immutable production image}
build: !reset null
# The base developer compose publishes 8100. Production traffic must enter
# through the TLS edge only; remove the inherited host binding entirely.
ports: !reset []
pull_policy: always
# SMTP remains opt-in through docker-compose.production.smtp.yml so disabled mode has no secret-file dependency.
environment:
CONTRACT_API_AI_PLATFORM_BASE_URL: ${RC06_AI_PLATFORM_BASE_URL:?RC06_AI_PLATFORM_BASE_URL must be an internal HTTPS endpoint}
CONTRACT_API_AI_PLATFORM_RC06_PUBLICATION_KEY: ${RC06_AI_PLATFORM_PUBLICATION_SERVICE_KEY:?RC06_AI_PLATFORM_PUBLICATION_SERVICE_KEY is required}
CONTRACT_API_RC06_PUBLICATION_FACADE_ENABLED: "true"
CONTRACT_API_RC06_LOW_LEVEL_COORDINATOR_HTTP_ENABLED: "false"
CONTRACT_API_RC06_PUBLICATION_ENVIRONMENT_ID: ${RC06_PUBLICATION_ENVIRONMENT_ID:?RC06_PUBLICATION_ENVIRONMENT_ID is required}
CONTRACT_API_RC06_PUBLICATION_TENANT_ID: ${RC06_PUBLICATION_TENANT_ID:?RC06_PUBLICATION_TENANT_ID is required}
CONTRACT_API_RC06_PUBLICATION_BEARER_TOKEN_FILE: /run/secrets/rc06_facade_bearer_token
CONTRACT_API_RC06_FROZEN_CANDIDATE_COMMIT: ${RC06_FROZEN_CANDIDATE_COMMIT:?RC06_FROZEN_CANDIDATE_COMMIT is required}
CONTRACT_API_RC06_RELEASE_IMAGE_DIGEST: ${RC06_CONTRACT_RELEASE_IMAGE_DIGEST:?RC06_CONTRACT_RELEASE_IMAGE_DIGEST is required}
CONTRACT_API_RC06_AI_RELEASE_IMAGE_DIGEST: ${RC06_AI_RELEASE_IMAGE_DIGEST:?RC06_AI_RELEASE_IMAGE_DIGEST is required}
CONTRACT_API_RC06_APPROVAL_ROOT_PUBLIC_KEY_FILE: /run/yuqei/rc06/approval-root-public.pem
CONTRACT_API_RC06_APPROVAL_ROOT_PUBLIC_KEY_SHA256: ${RC06_APPROVAL_ROOT_PUBLIC_KEY_SHA256:?RC06_APPROVAL_ROOT_PUBLIC_KEY_SHA256 is required}
CONTRACT_API_RC06_APPROVAL_ROOT_KEY_ID: ${RC06_APPROVAL_ROOT_KEY_ID:?RC06_APPROVAL_ROOT_KEY_ID is required}
CONTRACT_API_RC06_APPROVAL_KEYRING_EPOCH: ${RC06_APPROVAL_KEYRING_EPOCH:?RC06_APPROVAL_KEYRING_EPOCH is required}
CONTRACT_API_RC06_APPLY_SERVICE_PUBLIC_KEY_FILE: /run/yuqei/rc06/apply-service-public.pem
CONTRACT_API_RC06_APPLY_SERVICE_PUBLIC_KEY_SHA256: ${RC06_APPLY_SERVICE_PUBLIC_KEY_SHA256:?RC06_APPLY_SERVICE_PUBLIC_KEY_SHA256 is required}
CONTRACT_API_RC06_LEGAL_REVIEW_KEYRING_SHA256: ${RC06_LEGAL_REVIEW_KEYRING_SHA256:?RC06_LEGAL_REVIEW_KEYRING_SHA256 is required}
CONTRACT_API_RC06_ONLINE_SIGNING_PROVIDER: ${RC06_ONLINE_SIGNING_PROVIDER:?RC06_ONLINE_SIGNING_PROVIDER is required}
CONTRACT_API_RC06_ONLINE_SIGNING_KEY_REFERENCE: ${RC06_ONLINE_SIGNING_KEY_REFERENCE:?RC06_ONLINE_SIGNING_KEY_REFERENCE is required}
CONTRACT_API_RC06_ONLINE_SIGNING_KEY_VERSION: ${RC06_ONLINE_SIGNING_KEY_VERSION:?RC06_ONLINE_SIGNING_KEY_VERSION is required}
CONTRACT_API_RC06_ONLINE_SIGNING_ALGORITHM: ${RC06_ONLINE_SIGNING_ALGORITHM:-RSA_PSS_SHA_256}
CONTRACT_API_RC06_ONLINE_SIGNING_PUBLIC_KEY_SPKI_SHA256: ${RC06_ONLINE_SIGNING_PUBLIC_KEY_SPKI_SHA256:?RC06_ONLINE_SIGNING_PUBLIC_KEY_SPKI_SHA256 is required}
CONTRACT_API_RELIABLE_EVIDENCE_SIGNING_BACKEND: kms_gateway
CONTRACT_API_RELIABLE_EVIDENCE_SIGNING_ENDPOINT: ${RC06_KMS_SIGNING_ENDPOINT:?RC06_KMS_SIGNING_ENDPOINT is required}
CONTRACT_API_RELIABLE_EVIDENCE_SIGNING_CREDENTIAL_FILE: /run/secrets/rc06_kms_credential
CONTRACT_API_RELIABLE_EVIDENCE_SIGNING_CA_FILE: /run/yuqei/rc06/kms-ca.pem
CONTRACT_API_PUBLIC_POLICY_CANDIDATE_DIR: /run/yuqei/public-policy/candidate
CONTRACT_API_PUBLIC_POLICY_LEGAL_APPROVAL_FILE: /run/yuqei/public-policy/named-policy-legal-approval.json
CONTRACT_API_PUBLIC_POLICY_TRUSTED_KEYRING_FILE: /run/yuqei/public-policy/policy-legal-keyring.trusted.json
@@ -41,6 +70,19 @@ services:
- ${PUBLIC_POLICY_CANDIDATE_DIR_HOST:?PUBLIC_POLICY_CANDIDATE_DIR_HOST is required}:/run/yuqei/public-policy/candidate:ro
- ${PUBLIC_POLICY_LEGAL_APPROVAL_FILE_HOST:?PUBLIC_POLICY_LEGAL_APPROVAL_FILE_HOST is required}:/run/yuqei/public-policy/named-policy-legal-approval.json:ro
- ${PUBLIC_POLICY_TRUSTED_KEYRING_FILE_HOST:?PUBLIC_POLICY_TRUSTED_KEYRING_FILE_HOST is required}:/run/yuqei/public-policy/policy-legal-keyring.trusted.json:ro
- ${RC06_APPROVAL_ROOT_PUBLIC_KEY_FILE_HOST:?RC06_APPROVAL_ROOT_PUBLIC_KEY_FILE_HOST is required}:/run/yuqei/rc06/approval-root-public.pem:ro
- ${RC06_APPLY_SERVICE_PUBLIC_KEY_FILE_HOST:?RC06_APPLY_SERVICE_PUBLIC_KEY_FILE_HOST is required}:/run/yuqei/rc06/apply-service-public.pem:ro
- ${RC06_KMS_CA_FILE_HOST:?RC06_KMS_CA_FILE_HOST is required}:/run/yuqei/rc06/kms-ca.pem:ro
secrets:
- source: rc06_facade_bearer_token
target: rc06_facade_bearer_token
- source: rc06_kms_credential
target: rc06_kms_credential
healthcheck:
test: ["CMD", "python", "-c", "import json,urllib.request; d=json.load(urllib.request.urlopen('http://127.0.0.1:8100/api/v1/health',timeout=5)); assert d['status']=='ok' and d['config_status']!='error'"]
interval: 10s
timeout: 5s
retries: 6
contract-review-worker:
image: ${CONTRACT_API_IMAGE:?CONTRACT_API_IMAGE must reference an immutable production image}
@@ -70,9 +112,15 @@ services:
ai-platform-api:
image: ${AI_PLATFORM_API_IMAGE:?AI_PLATFORM_API_IMAGE must reference an immutable production image}
build: !reset null
# Keep participant/internal routes off the host network. Contract reaches
# this service only over the private Compose network.
ports: !reset []
pull_policy: always
environment:
AI_PLATFORM_SECRET_FILE_ROOT: /run/secrets
AI_PLATFORM_RC06_PUBLICATION_SHARED_SECRET: ${RC06_AI_PLATFORM_PUBLICATION_SERVICE_KEY:?RC06_AI_PLATFORM_PUBLICATION_SERVICE_KEY is required}
AI_PLATFORM_RC06_FROZEN_CANDIDATE_COMMIT: ${RC06_FROZEN_CANDIDATE_COMMIT:?RC06_FROZEN_CANDIDATE_COMMIT is required}
AI_PLATFORM_RC06_RELEASE_IMAGE_DIGEST: ${RC06_AI_RELEASE_IMAGE_DIGEST:?RC06_AI_RELEASE_IMAGE_DIGEST is required}
AI_PLATFORM_PROVIDER_LEGAL_ATTESTATION_SECRET: ""
AI_PLATFORM_PROVIDER_ACCEPTANCE_RECEIPT_FILE: /run/yuqei/provider-acceptance/receipt.json
AI_PLATFORM_PROVIDER_ACCEPTANCE_TRUSTED_KEYS_FILE: /run/yuqei/provider-acceptance/trusted-keys.json
@@ -105,6 +153,7 @@ services:
restart: unless-stopped
environment:
BACKEND_SERVER_NAME: ${BACKEND_SERVER_NAME:?BACKEND_SERVER_NAME is required}
RC06_PUBLICATION_TENANT_ID: ${RC06_PUBLICATION_TENANT_ID:?RC06_PUBLICATION_TENANT_ID is required}
NGINX_ENVSUBST_OUTPUT_DIR: /etc/nginx/conf.d
ports:
- "${BACKEND_HTTP_BIND_ADDRESS:-0.0.0.0}:${BACKEND_HTTP_PORT:-80}:80"
@@ -178,3 +227,7 @@ volumes:
secrets:
ai_provider_api_key:
file: ${AI_PROVIDER_API_KEY_FILE:?AI_PROVIDER_API_KEY_FILE must point to a protected provider key file}
rc06_facade_bearer_token:
file: ${RC06_FACADE_BEARER_TOKEN_FILE:?RC06_FACADE_BEARER_TOKEN_FILE must point to a protected token file}
rc06_kms_credential:
file: ${RC06_KMS_CREDENTIAL_FILE:?RC06_KMS_CREDENTIAL_FILE must point to a protected credential file}
@@ -58,6 +58,52 @@ server {
proxy_send_timeout 660s;
}
# RC06 has one public service boundary. The tenant is deployment-bound;
# a caller-supplied tenant header is never forwarded as authority.
location = /internal/rc06/publication-transactions/apply {
client_max_body_size 16m;
limit_req zone=yuqei_api_per_ip burst=5 nodelay;
proxy_pass http://contract_api_upstream/internal/rc06/publication-transactions/apply;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-RC06-Tenant-ID ${RC06_PUBLICATION_TENANT_ID};
proxy_set_header X-Tenant-Id "";
proxy_set_header X-User-Id "";
proxy_set_header Connection "";
proxy_connect_timeout 5s;
proxy_read_timeout 660s;
proxy_send_timeout 660s;
}
location ~ ^/internal/rc06/publication-transactions/[A-Za-z0-9][A-Za-z0-9._:-]{7,63}/(status|rollback|finalize)$ {
client_max_body_size 16m;
limit_req zone=yuqei_api_per_ip burst=10 nodelay;
proxy_pass http://contract_api_upstream$request_uri;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-RC06-Tenant-ID ${RC06_PUBLICATION_TENANT_ID};
proxy_set_header X-Tenant-Id "";
proxy_set_header X-User-Id "";
proxy_set_header Connection "";
proxy_connect_timeout 5s;
proxy_read_timeout 660s;
proxy_send_timeout 660s;
}
location ~ ^/contract/api/v1/internal/rc06-publications(?:/|$) {
return 404;
}
location ~ ^/ai/api/v1/internal/rc06-publications(?:/|$) {
return 404;
}
location /contract/ {
limit_req zone=yuqei_api_per_ip burst=40 nodelay;
proxy_pass http://contract_api_upstream/;