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
123 changes: 123 additions & 0 deletions .github/workflows/oci-jit-shadow-preflight.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: OCI JIT Shadow Preflight

on:
workflow_dispatch:
inputs:
require_ready:
description: "Fail unless all redacted OCI variables and attestations are configured"
required: false
default: false
type: boolean

permissions:
contents: read

jobs:
preflight-and-no-order-shadow:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout BinancePlatform
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6

- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.11"

- name: Render redacted non-applying OCI lifecycle plan
env:
OCI_JIT_REGION: ${{ vars.OCI_JIT_REGION }}
OCI_JIT_COMPARTMENT_OCID: ${{ vars.OCI_JIT_COMPARTMENT_OCID }}
OCI_JIT_AVAILABILITY_DOMAIN: ${{ vars.OCI_JIT_AVAILABILITY_DOMAIN }}
OCI_JIT_SUBNET_OCID: ${{ vars.OCI_JIT_SUBNET_OCID }}
OCI_JIT_NAT_GATEWAY_OCID: ${{ vars.OCI_JIT_NAT_GATEWAY_OCID }}
OCI_JIT_RESERVED_PUBLIC_IP_OCID: ${{ vars.OCI_JIT_RESERVED_PUBLIC_IP_OCID }}
OCI_JIT_IMAGE_OCID: ${{ vars.OCI_JIT_IMAGE_OCID }}
OCI_JIT_SHAPE: ${{ vars.OCI_JIT_SHAPE }}
OCI_JIT_RUNNER_GROUP: ${{ vars.OCI_JIT_RUNNER_GROUP }}
OCI_JIT_DEFINED_TAG_NAMESPACE: ${{ vars.OCI_JIT_DEFINED_TAG_NAMESPACE }}
OCI_JIT_DEFINED_TAG_KEY: ${{ vars.OCI_JIT_DEFINED_TAG_KEY }}
OCI_JIT_DEFINED_TAG_VALUE: ${{ vars.OCI_JIT_DEFINED_TAG_VALUE }}
OCI_JIT_RUNTIME_DYNAMIC_GROUP_OCID: ${{ vars.OCI_JIT_RUNTIME_DYNAMIC_GROUP_OCID }}
OCI_JIT_RUNTIME_POLICY_OCID: ${{ vars.OCI_JIT_RUNTIME_POLICY_OCID }}
OCI_JIT_LAUNCHER_PRINCIPAL_OCID: ${{ vars.OCI_JIT_LAUNCHER_PRINCIPAL_OCID }}
OCI_JIT_EVIDENCE_BUCKET_NAME: ${{ vars.OCI_JIT_EVIDENCE_BUCKET_NAME }}
OCI_JIT_ATTESTATIONS_JSON: ${{ vars.OCI_JIT_ATTESTATIONS_JSON }}
REQUIRE_READY: ${{ inputs.require_ready }}
run: |
set -euo pipefail
args=(
preflight
--contract infra/oci-jit-shadow/contract.json
--attestations-env OCI_JIT_ATTESTATIONS_JSON
--output reports/oci_jit_shadow_preflight.json
)
if [[ "$REQUIRE_READY" == "true" ]]; then
args+=(--require-ready)
fi
python scripts/oci_jit_shadow_preflight.py "${args[@]}"

- name: Exercise fail-closed orphan audit contract
run: |
set -euo pipefail
python scripts/oci_jit_shadow_preflight.py audit-orphans \
--contract infra/oci-jit-shadow/contract.json \
--inventory tests/fixtures/oci_jit_shadow/clean_inventory.json \
--output reports/oci_jit_shadow_orphan_audit.json \
--require-ready

- name: Resolve pinned dependency refs
id: refs
shell: bash
run: |
set -euo pipefail
qpk_ref="$(grep -Eo 'QuantPlatformKit\.git@[0-9a-f]+' pyproject.toml | head -n1 | sed 's/.*@//')"
strategies_ref="$(grep -Eo 'CryptoStrategies\.git@[0-9a-f]+' pyproject.toml | head -n1 | sed 's/.*@//')"
test -n "$qpk_ref"
test -n "$strategies_ref"
echo "qpk_ref=$qpk_ref" >> "$GITHUB_OUTPUT"
echo "strategies_ref=$strategies_ref" >> "$GITHUB_OUTPUT"

- name: Checkout QuantPlatformKit
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
repository: QuantStrategyLab/QuantPlatformKit
ref: ${{ steps.refs.outputs.qpk_ref }}
path: external/QuantPlatformKit

- name: Checkout CryptoStrategies
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
repository: QuantStrategyLab/CryptoStrategies
ref: ${{ steps.refs.outputs.strategies_ref }}
path: external/CryptoStrategies

- name: Install locked runtime
run: |
set -euo pipefail
python -m pip install --disable-pip-version-check uv==0.11.6
uv sync --frozen --no-dev
uv pip install --no-deps -e external/QuantPlatformKit -e external/CryptoStrategies

- name: Run fixed-input no-order replay
env:
BINANCE_DRY_RUN: "true"
run: |
set -euo pipefail
uv run --no-sync python scripts/run_isolation_shadow_fixture.py \
--output reports/oci_jit_shadow_report.json \
--digest-output reports/oci_jit_shadow_report.sha256

- name: Upload redacted preflight and shadow evidence
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: oci-jit-shadow-preflight-${{ github.run_id }}
path: |
reports/oci_jit_shadow_preflight.json
reports/oci_jit_shadow_orphan_audit.json
reports/oci_jit_shadow_report.json
reports/oci_jit_shadow_report.sha256
if-no-files-found: warn
retention-days: 7
10 changes: 10 additions & 0 deletions docs/operator_runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ and compare semantic report digests. Neither job references Binance credentials,
the `binance-runtime` environment, or Google OIDC. Passing proves fixture parity,
not live readiness or host ephemerality.

`OCI JIT Shadow Preflight` implements the next non-applying preparation step. It
checks the committed private-subnet, reserved-NAT, defined-tag,
instance-principal, one-job runner, and delete-on-termination contract; reports
which operator-owned OCI repository variables are still missing; exercises the
fail-closed orphan audit; and reruns the no-order fixture. It has no OCI OIDC,
broker secret, environment, or mutation authority. The required variables and
redacted attestation format are listed in
[`infra/oci-jit-shadow/README.md`](../infra/oci-jit-shadow/README.md). A clean
fixture audit proves the checker, not the absence of real OCI or GitHub orphans.

## Degraded Mode Ladder

Healthy mode:
Expand Down
8 changes: 8 additions & 0 deletions docs/runtime_isolation_migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ compartment, current instance/VNIC, route table, and allowlisted egress ownershi
outside public artifacts. Confirm the new reserved NAT address and OCI IAM
policy before choosing an image or Vault implementation.

The first implementation batch now lives under `infra/oci-jit-shadow/`. Its
committed contract and preflight script render the bounded launch/terminate
lifecycle, validate redacted private-subnet/NAT/reserved-IP/defined-tag and
instance-principal attestations, and fail closed on stale tagged resources. The
manual workflow has no OCI mutation authority or broker secret and runs the
same fixed-input no-order fixture. This is deployment readiness evidence only;
it does not provision the selected architecture or authorize a live cutover.

### Phase 2 validation evidence

- Host profile run `32644765084`: `PARTIAL`, QEMU VM, no secret value read,
Expand Down
99 changes: 99 additions & 0 deletions infra/oci-jit-shadow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# OCI JIT shadow deployment preflight

This directory is the first implementation batch for the disposable Oracle
Cloud Compute runner selected in `docs/runtime_isolation_migration.md`. It is a
**non-applying control-plane contract**. It does not call OCI, register a GitHub
runner, read a secret, change the Binance allowlist, or alter the current live
runner.

`contract.json` records the reviewed launch and termination invariants. The
preflight script validates those invariants, reports whether the operator-owned
OCI inputs are configured, consumes a redacted read-only attestation, and emits
only variable names and readiness findings. It never emits variable values or
resource OCIDs.

## Repository variables the operator must provide later

Configure these as GitHub repository variables only after the corresponding OCI
resources have been reviewed. None is a broker credential.

| Variable | Required meaning |
| --- | --- |
| `OCI_JIT_REGION` | OCI region containing the candidate runtime |
| `OCI_JIT_COMPARTMENT_OCID` | Dedicated candidate compartment |
| `OCI_JIT_AVAILABILITY_DOMAIN` | Availability domain for the on-demand instance |
| `OCI_JIT_SUBNET_OCID` | Private subnet with no instance public IP |
| `OCI_JIT_NAT_GATEWAY_OCID` | NAT gateway used by that private subnet |
| `OCI_JIT_RESERVED_PUBLIC_IP_OCID` | Reserved public IP attached to the NAT gateway |
| `OCI_JIT_IMAGE_OCID` | Reviewed immutable custom image |
| `OCI_JIT_SHAPE` | Right-sized on-demand flexible shape |
| `OCI_JIT_RUNNER_GROUP` | Dedicated GitHub runner group that accepts only the reviewed workflow |
| `OCI_JIT_DEFINED_TAG_NAMESPACE` | Defined-tag namespace for candidate ownership |
| `OCI_JIT_DEFINED_TAG_KEY` | Defined-tag key matched by IAM and orphan audit |
| `OCI_JIT_DEFINED_TAG_VALUE` | Defined-tag value for the Binance candidate runtime |
| `OCI_JIT_RUNTIME_DYNAMIC_GROUP_OCID` | Dynamic group limited by compartment and defined tag |
| `OCI_JIT_RUNTIME_POLICY_OCID` | Runtime policy with no Compute/IAM/Vault administration |
| `OCI_JIT_LAUNCHER_PRINCIPAL_OCID` | Separate launcher identity, unable to read broker secrets |
| `OCI_JIT_EVIDENCE_BUCKET_NAME` | External destination for redacted runner and terminal evidence |
| `OCI_JIT_ATTESTATIONS_JSON` | Redacted JSON matching `preflight-attestations.example.json` |

Keep the exact OCIDs and the reserved public address out of repository files and
workflow artifacts. Do not store `BINANCE_API_KEY`, `BINANCE_API_SECRET`, GitHub
JIT registration tokens, OCI API private keys, or Vault secret material in any
of these variables. Broker credentials remain prohibited in the fixture-shadow
phase.

## What the attestation means

The example attestation is a schema example, not evidence that any OCI resource
exists. Before setting `OCI_JIT_ATTESTATIONS_JSON`, use the OCI Console or a
read-only OCI identity to verify all represented facts:

- the subnet is private, assigns no public IP, and routes outbound traffic
through the expected NAT gateway;
- the NAT gateway is available and owns the reviewed reserved public IP;
- no inbound security rule is required for the runner;
- the dynamic-group rule includes both the dedicated compartment and defined
tag;
- the no-order instance principal can write only the evidence destination and
cannot read the broker secret or manage Compute, IAM, dynamic groups,
policies, Vault, or secret versions;
- the launcher is limited to tagged candidates and cannot read the broker
secret or manage network/IAM resources;
- the image is pinned, capacity is on-demand, runner assignment is one job, and
instance termination deletes the boot volume.

The preflight validates the redacted statements. It does not replace an OCI IAM
policy review and does not prove the raw OCI resources by itself.

## Safe preflight and orphan audit

The manual `OCI JIT Shadow Preflight` workflow has `contents: read`, no OIDC,
environment, or secret references. It renders a redacted launch/terminate plan,
exercises the orphan-audit engine with a clean fixture, and runs the fixed-input
no-order strategy replay. With `require_ready=false`, missing OCI inputs produce
a `PARKED` terminal report while the no-order replay still runs. Use
`require_ready=true` only after all variables and the read-only attestation are
configured.

To check a real inventory without mutating it, export a JSON object matching
`qsl.oci_jit_shadow_inventory.v1` from read-only OCI and GitHub API queries, then
run:

```bash
python scripts/oci_jit_shadow_preflight.py audit-orphans \
--contract infra/oci-jit-shadow/contract.json \
--inventory /secure/path/oci-jit-inventory.json \
--output reports/oci_jit_shadow_orphan_audit.json \
--require-ready
```

Only resources matching the candidate defined tag belong in that inventory.
The report replaces resource identifiers with short one-way fingerprints. It
does not delete anything: a stale instance, unattached boot volume, detached
VNIC attachment, lingering GitHub runner registration, incomplete collection,
or malformed evidence results in `PARKED`.

Actual OCI launch/termination, JIT-token delivery, instance-principal secret
retrieval, and automated cleanup remain a later reviewed phase. No live cutover
is authorized by a successful preflight.
86 changes: 86 additions & 0 deletions infra/oci-jit-shadow/contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"schema_version": "qsl.oci_jit_shadow_contract.v1",
"deployment_mode": "no_order_shadow",
"apply_authorized": false,
"required_repository_variables": [
"OCI_JIT_REGION",
"OCI_JIT_COMPARTMENT_OCID",
"OCI_JIT_AVAILABILITY_DOMAIN",
"OCI_JIT_SUBNET_OCID",
"OCI_JIT_NAT_GATEWAY_OCID",
"OCI_JIT_RESERVED_PUBLIC_IP_OCID",
"OCI_JIT_IMAGE_OCID",
"OCI_JIT_SHAPE",
"OCI_JIT_RUNNER_GROUP",
"OCI_JIT_DEFINED_TAG_NAMESPACE",
"OCI_JIT_DEFINED_TAG_KEY",
"OCI_JIT_DEFINED_TAG_VALUE",
"OCI_JIT_RUNTIME_DYNAMIC_GROUP_OCID",
"OCI_JIT_RUNTIME_POLICY_OCID",
"OCI_JIT_LAUNCHER_PRINCIPAL_OCID",
"OCI_JIT_EVIDENCE_BUCKET_NAME",
"OCI_JIT_ATTESTATIONS_JSON"
],
"network": {
"private_subnet_required": true,
"assign_public_ip": false,
"nat_gateway_required": true,
"reserved_public_ip_required": true,
"inbound_rules_allowed": false
},
"compute": {
"capacity_type": "ON_DEMAND",
"runner_assignment": "ONE_JOB",
"pinned_custom_image_required": true,
"max_instance_age_minutes": 60,
"delete_boot_volume_on_termination": true
},
"identity": {
"instance_principal_required": true,
"dynamic_group_requires_compartment": true,
"dynamic_group_requires_defined_tag": true,
"launcher_scope_defined_tag_only": true,
"launcher_can_read_broker_secret": false,
"launcher_can_manage_network": false,
"launcher_can_manage_iam": false,
"runtime_can_read_broker_secret": false,
"runtime_can_write_evidence": true,
"runtime_can_manage_compute": false,
"runtime_can_manage_iam": false,
"runtime_can_manage_vault": false
},
"shadow": {
"broker_secret_allowed": false,
"github_oidc_allowed": false,
"dry_run_required": true,
"executed_call_count_required": 0,
"platform_retries": 0
},
"cleanup": {
"delete_boot_volume": true,
"deregister_runner": true,
"orphan_states": [
"RUNNING",
"STARTING",
"STOPPED",
"STOPPING"
],
"orphan_resource_types": [
"instance",
"boot_volume",
"vnic_attachment",
"github_runner"
]
},
"planned_operations": [
"validate_configuration",
"validate_read_only_oci_attestations",
"launch_tagged_private_instance",
"register_one_job_jit_runner",
"run_no_order_shadow",
"persist_redacted_terminal_evidence",
"deregister_runner",
"terminate_instance_with_boot_volume_deletion",
"audit_orphans"
]
}
34 changes: 34 additions & 0 deletions infra/oci-jit-shadow/preflight-attestations.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"schema_version": "qsl.oci_jit_shadow_attestations.v1",
"evidence_source": "oci_read_only_export",
"collection_complete": true,
"resource_identifiers_recorded": false,
"network": {
"subnet_is_private": true,
"instance_assigns_public_ip": false,
"nat_gateway_state": "AVAILABLE",
"reserved_public_ip_state": "ASSIGNED",
"private_subnet_routes_through_nat": true,
"inbound_rules_present": false
},
"identity": {
"instance_principal_enabled": true,
"dynamic_group_scoped_to_compartment": true,
"dynamic_group_scoped_to_defined_tag": true,
"launcher_scope_is_defined_tag_only": true,
"launcher_can_read_broker_secret": false,
"launcher_can_manage_network": false,
"launcher_can_manage_iam": false,
"runtime_can_read_broker_secret": false,
"runtime_can_write_evidence": true,
"runtime_can_manage_compute": false,
"runtime_can_manage_iam": false,
"runtime_can_manage_vault": false
},
"compute": {
"capacity_type": "ON_DEMAND",
"custom_image_is_pinned": true,
"runner_assignment": "ONE_JOB",
"boot_volume_delete_on_termination": true
}
}
Loading