Skip to content
Merged
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
17 changes: 9 additions & 8 deletions docs/spec-alpha-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,22 @@ Feature states:
| `gateway_portal.real_internet.upstream_resolver_enabled` | `experimental` | `false` | `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | CoreDNS upstream forwarding stubs are appended and reloaded; duplicate-stub reconciliation remains alpha. |
| `gateway_portal.cross_world.mode` | `experimental` | `none` | `netengine.handlers.gateway_handler`, `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | PEERED/FEDERATED lifecycle hooks install peer routing, DNS stubs, trust anchors, runtime artifacts, rollback, and healthchecks; interoperability remains alpha. |
| `gateway_portal.cross_world.peers` | `experimental` | `[]` | `netengine.handlers.gateway_handler`, `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | Peer add/update/remove, trust-anchor rotation, and routing reapply are implemented with mocked two-world DNS coverage; live multi-host federation is not yet guaranteed. |
| `ands.profiles.*.dynamic_ip` | `unsupported` | profile default | `netengine.spec.loader` | Dynamic IP allocation in AND profiles is not implemented. |
| `ands.profiles.*.reverse_dns` | `unsupported` | profile default | `netengine.spec.loader` | Reverse DNS delegation from AND profiles is not implemented. |
| `ands.profiles.*.bgp` | `unsupported` | profile default | `netengine.spec.loader` | BGP profile configuration is not implemented. |
| `ands.profiles.*.dynamic_ip` | `experimental` | profile default | `netengine.phases.phase_ands`, `netengine.handlers.gateway_handler` | DHCP setup is wired through dnsmasq in the gateway container; requires dnsmasq in the gateway image and remains alpha. |
| `ands.profiles.*.reverse_dns` | `experimental` | profile default | `netengine.phases.phase_ands`, `netengine.handlers.dns` | in-addr.arpa zone provisioning is available for AND profiles; propagation to external resolvers is not provided. |
| `ands.profiles.*.bgp` | `experimental` | profile default | `netengine.phases.phase_ands`, `netengine.handlers.gateway_handler` | Bird2 sidecar provisioning is wired; optional mode tolerates startup failure, required mode aborts provisioning, and the image must be available. |

## PKI alpha notes

- **DNSSEC lifetimes**: `pki.dnssec_ksk_lifetime_days` and
`pki.dnssec_zsk_lifetime_days` are persisted as generated-key metadata by
`PKIHandler.setup_dnssec`, but they do not schedule rotation and do not wire
generated keys into signed CoreDNS zones.
`PKIHandler.setup_dnssec` and used by the rotation worker; CoreDNS online
signing is wired, but signed-zone cutover validation remains alpha.
- **CRL**: `pki.crl_enabled` reaches step-ca config injection code, but alpha
support does not provide complete CRL publication, distribution-point
integration, or client validation guarantees.
publishes the configured distribution URL in Phase 3 output; client validation
guarantees are still being hardened.
- **OCSP**: `pki.ocsp_enabled` reaches step-ca config injection code, but alpha
support does not provide a fully managed OCSP responder lifecycle.
publishes the configured responder URL in Phase 3 output; responder lifecycle
verification is still being hardened.
- **Intermediate CA**: `pki.intermediate_ca_enabled` stores and exposes the
generated intermediate certificate when available, but remains stabilizing and
should not be treated as a stable trust-chain management interface.
Expand Down
29 changes: 15 additions & 14 deletions docs/support-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,34 @@ Feature states:

| Dotted field path | Feature state | Default value | Implementation owner/module | Alpha caveat |
|---|---:|---|---|---|
| `pki.intermediate_ca_enabled` | `reserved` / registry: `experimental` | `false` | `netengine.handlers.pki_handler`, `netengine.handlers.phase_pki`, `netengine.api.routes` | step-ca's generated intermediate certificate can be read, exposed in Phase 3 output, and fetched through `GET /pki/intermediate-ca-cert`; behavior remains alpha/stabilizing and the model metadata is still conservative. |
| `pki.dnssec_enabled` | `unsupported` | `true` | `netengine.handlers.phase_pki`, `netengine.handlers.pki_handler`, spec loader validation | Active non-default enabling is unsupported in alpha validation. The handler can generate KSK/ZSK key material, but DNS zone signing and CoreDNS activation are not integrated, so this is not end-to-end DNSSEC support. |
| `pki.dnssec_ksk_lifetime_days` | `unsupported` | `365` | `netengine.handlers.pki_handler` | Lifetime is recorded in DNSSEC output metadata only; no automatic KSK rotation or signed-zone publication is implemented. |
| `pki.dnssec_zsk_lifetime_days` | `unsupported` | `30` | `netengine.handlers.pki_handler` | Lifetime is recorded in DNSSEC output metadata only; no automatic ZSK rotation or signed-zone publication is implemented. |
| `pki.crl_enabled` | `unsupported` | `false` | `netengine.handlers.pki_handler`, `netengine.handlers.phase_pki` | Handler code can inject a step-ca CRL config and report a URL, but CRL publication/distribution-point plumbing is incomplete and active use is unsupported in alpha validation. |
| `pki.ocsp_enabled` | `unsupported` | `false` | `netengine.handlers.pki_handler`, `netengine.handlers.phase_pki` | Handler code can inject OCSP-related step-ca config and report a URL, but responder deployment/verification is incomplete and active use is unsupported in alpha validation. |
| `pki.intermediate_ca_enabled` | `experimental` | `false` | `netengine.handlers.pki_handler`, `netengine.handlers.phase_pki`, `netengine.api.routes` | step-ca's generated intermediate certificate can be read, exposed in Phase 3 output, and fetched through `GET /pki/intermediate-ca-cert`; trust-chain management remains stabilizing. |
| `pki.dnssec_enabled` | `experimental` | `false` | `netengine.handlers.phase_pki`, `netengine.handlers.pki_handler`, `netengine.handlers.dns` | KSK/ZSK keys are generated and wired into CoreDNS online signing; promotion to stable is gated on signed-zone validation in CI e2e and operational hardening. |
| `pki.dnssec_ksk_lifetime_days` | `experimental` | `365` | `netengine.handlers.pki_handler`, `netengine.workers.pki_cert_rotation_worker` | Lifetime is recorded in DNSSEC output metadata and used by the rotation worker; signed-zone cutover validation remains alpha. |
| `pki.dnssec_zsk_lifetime_days` | `experimental` | `30` | `netengine.handlers.pki_handler`, `netengine.workers.pki_cert_rotation_worker` | Lifetime is recorded in DNSSEC output metadata and used by the rotation worker; signed-zone cutover validation remains alpha. |
| `pki.crl_enabled` | `experimental` | `false` | `netengine.handlers.pki_handler`, `netengine.handlers.phase_pki` | step-ca CRL generation is enabled in `ca.json` and the distribution URL is published in Phase 3 output; client-validation coverage is still being hardened in CI e2e. |
| `pki.ocsp_enabled` | `experimental` | `false` | `netengine.handlers.pki_handler`, `netengine.handlers.phase_pki` | step-ca OCSP config is injected and the responder URL is published in Phase 3 output; responder lifecycle/verification is still being hardened in CI e2e. |
| `pki.rotation_policy` | `experimental` | `{enabled: true, default_interval_hours: 24, default_warning_days: 30, cert_type_overrides: {}}` | `netengine.handlers.phase_pki`, `netengine.workers.pki_cert_rotation_worker`, `netengine.api.routes` | Wired from the spec into worker registration and live-reloaded from runtime state; policy shape and cert-type semantics may change during alpha. |
| `gateway_portal.real_internet.mode` | `experimental` | `isolated` | `netengine.handlers.gateway_handler`, `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | Isolated, shadowed, mirrored, and exposed nftables policies are wired; real-host integration remains alpha. |
| `gateway_portal.real_internet.service_mirrors` | `experimental` | `[]` | `netengine.handlers.gateway_handler`, `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | Mirrored-mode allowlists and operator output are wired; `in_world_service` must be an IPv4 address until automatic DNS aliasing for real hostnames lands. |
| `gateway_portal.real_internet.upstream_resolver_enabled` | `experimental` | `false` | `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | CoreDNS upstream forwarding stubs are appended and reloaded; duplicate-stub reconciliation remains alpha. |
| `gateway_portal.cross_world.mode` | `experimental` | `none` | `netengine.handlers.gateway_handler`, `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | PEERED/FEDERATED lifecycle hooks install peer routing, DNS stubs, trust anchors, runtime artifacts, rollback, and healthchecks; interoperability remains alpha. |
| `gateway_portal.cross_world.peers` | `experimental` | `[]` | `netengine.handlers.gateway_handler`, `netengine.handlers.gateway_portal_handler`, `netengine.spec.loader` | Peer add/update/remove, trust-anchor rotation, and routing reapply are implemented with mocked two-world DNS coverage; live multi-host federation is not yet guaranteed. |
| `ands.profiles.*.dynamic_ip` | `unsupported` | profile default | `netengine.spec.loader` | Dynamic IP allocation in AND profiles is not implemented. |
| `ands.profiles.*.reverse_dns` | `unsupported` | profile default | `netengine.spec.loader` | Reverse DNS delegation from AND profiles is not implemented. |
| `ands.profiles.*.bgp` | `unsupported` | profile default | `netengine.spec.loader` | BGP profile configuration is not implemented. |
| `ands.profiles.*.dynamic_ip` | `experimental` | profile default | `netengine.phases.phase_ands`, `netengine.handlers.gateway_handler` | DHCP setup is wired through dnsmasq in the gateway container; requires dnsmasq in the gateway image and remains alpha. |
| `ands.profiles.*.reverse_dns` | `experimental` | profile default | `netengine.phases.phase_ands`, `netengine.handlers.dns` | in-addr.arpa zone provisioning is available for AND profiles; propagation to external resolvers is not provided. |
| `ands.profiles.*.bgp` | `experimental` | profile default | `netengine.phases.phase_ands`, `netengine.handlers.gateway_handler` | Bird2 sidecar provisioning is wired; optional mode tolerates startup failure, required mode aborts provisioning, and the image must be available. |

## PKI alpha notes

- **DNSSEC lifetimes**: `pki.dnssec_ksk_lifetime_days` and
`pki.dnssec_zsk_lifetime_days` are persisted as generated-key metadata by
`PKIHandler.setup_dnssec`, but they do not schedule rotation and do not wire
generated keys into signed CoreDNS zones.
`PKIHandler.setup_dnssec` and used by the rotation worker; CoreDNS online
signing is wired, but signed-zone cutover validation remains alpha.
- **CRL**: `pki.crl_enabled` reaches step-ca config injection code, but alpha
support does not provide complete CRL publication, distribution-point
integration, or client validation guarantees.
publishes the configured distribution URL in Phase 3 output; client validation
guarantees are still being hardened.
- **OCSP**: `pki.ocsp_enabled` reaches step-ca config injection code, but alpha
support does not provide a fully managed OCSP responder lifecycle.
publishes the configured responder URL in Phase 3 output; responder lifecycle
verification is still being hardened.
- **Intermediate CA**: `pki.intermediate_ca_enabled` stores and exposes the
generated intermediate certificate when available, but remains stabilizing and
should not be treated as a stable trust-chain management interface.
Expand Down
31 changes: 30 additions & 1 deletion netengine/spec/feature_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ class FeatureStateEntry:
"hardened in CI e2e"
),
),

FeatureStateEntry(
path="pki.dnssec_ksk_lifetime_days",
state="experimental",
stage="alpha",
reason=(
"DNSSEC KSK lifetime is recorded with generated key metadata and "
"used by the rotation worker; signed-zone cutover validation remains alpha"
),
),
FeatureStateEntry(
path="pki.dnssec_zsk_lifetime_days",
state="experimental",
stage="alpha",
reason=(
"DNSSEC ZSK lifetime is recorded with generated key metadata and "
"used by the rotation worker; signed-zone cutover validation remains alpha"
),
),
FeatureStateEntry(
path="pki.crl_enabled",
state="experimental",
Expand All @@ -45,13 +64,23 @@ class FeatureStateEntry:
"responder lifecycle/verification is still being hardened in CI e2e"
),
),

FeatureStateEntry(
path="pki.rotation_policy",
state="experimental",
stage="alpha",
reason=(
"rotation policy is wired into the certificate rotation worker and "
"operator API; cert-type coverage and graceful cutover behavior remain alpha"
),
),
FeatureStateEntry(
path="gateway_portal.real_internet.mode",
state="experimental",
stage="alpha",
reason=(
"nftables policies for isolated/shadowed/mirrored/exposed modes are "
"implemented; requires gateway container with nft available"
"implemented; requires gateway container with nft available; "
"gateway policies for isolated, shadowed, mirrored, and exposed modes "
"are wired through nftables, but real-host integration remains alpha"
),
Expand Down
58 changes: 45 additions & 13 deletions tests/test_feature_states.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Feature-state metadata tests for spec fields."""

from pathlib import Path

from netengine.spec.feature_state import FEATURE_STATE_REGISTRY
from netengine.spec.models import (
FEATURE_STATE_JSON_SCHEMA_KEY,
Expand All @@ -11,7 +10,11 @@
PKIPhase,
)

_SUPPORT_MATRIX = Path(__file__).resolve().parents[1] / "docs" / "spec-alpha-support.md"
_DOCS_DIR = Path(__file__).resolve().parents[1] / "docs"
_SUPPORT_MATRIXES = (
_DOCS_DIR / "support-matrix.md",
_DOCS_DIR / "spec-alpha-support.md",
)

PKI_FIELD_NAMES = {
"pki.intermediate_ca_enabled": "intermediate_ca_enabled",
Expand All @@ -26,7 +29,10 @@

def test_required_pki_fields_have_explicit_registry_feature_states() -> None:
"""Every alpha-sensitive PKI field is explicitly tracked in the registry."""
registry_paths = {entry.path for entry in FEATURE_STATE_REGISTRY}

assert set(PKI_FEATURE_STATES) == set(PKI_FIELD_NAMES)
assert set(PKI_FIELD_NAMES).issubset(registry_paths)
assert all(isinstance(state, FeatureState) for state in PKI_FEATURE_STATES.values())


Expand All @@ -49,17 +55,43 @@ def test_required_pki_fields_expose_feature_state_in_json_schema() -> None:
)


def test_every_registry_entry_is_documented_in_support_matrix() -> None:
"""Each gated field must have a row in docs/spec-alpha-support.md.
def _support_matrix_rows(path: Path) -> dict[str, dict[str, str]]:
"""Return support-matrix table rows keyed by dotted field path."""
rows: dict[str, dict[str, str]] = {}
for line in path.read_text(encoding="utf-8").splitlines():
if not line.startswith("| `"):
continue
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
if len(cells) < 5:
continue
dotted_path = cells[0].strip("`")
rows[dotted_path] = {
"state": cells[1].strip("`"),
"default": cells[2],
"owner": cells[3],
"caveat": cells[4],
}
return rows


def test_registry_entries_are_documented_consistently_in_support_matrices() -> None:
"""Registry paths must have matching, non-contradictory rows in both support docs.

Keeps the alpha support contract in lock-step with the validation registry,
so removing a gate (for v1) forces a deliberate matrix update — mirroring
the supply-chain CI check that licenses stay documented.
so removing or changing a gate forces a deliberate matrix update.
"""
matrix_text = _SUPPORT_MATRIX.read_text(encoding="utf-8")
undocumented = [
entry.path for entry in FEATURE_STATE_REGISTRY if f"`{entry.path}`" not in matrix_text
]
assert not undocumented, (
"feature_state registry entries missing from docs/spec-alpha-support.md: " f"{undocumented}"
)
expected_states = {entry.path: entry.state for entry in FEATURE_STATE_REGISTRY}

for matrix_path in _SUPPORT_MATRIXES:
rows = _support_matrix_rows(matrix_path)
missing = sorted(set(expected_states) - set(rows))
assert not missing, f"feature_state registry entries missing from {matrix_path}: {missing}"

contradictions = {
path: {"registry": state, "docs": rows[path]["state"]}
for path, state in expected_states.items()
if rows[path]["state"] != state
}
assert not contradictions, (
f"feature_state registry entries contradicted by {matrix_path}: {contradictions}"
)