All ArticlesSystem Architecture•November 1, 2026•9 min read

Japan Didn't Wait for a Legal Basis. The US Still Doesn't. Pass the Clarity: Technical Architecture & Analysis

Japan codified bounded active cyber defense in 2025 while the US still has no statutory safe harbor — so the legal boundary has to live in your architecture, not your runbook. Here is a production reference design: typed policy gates, Rego-encoded jurisdiction rules, and a tamper-evident evidence ledger built for real-time analysis.

Operator in a modern control room managing technological systems in El Agustino, Lima.Photo by Fernando Narvaez on Pexels

Japan's Diet enacted the Act on the Promotion of Measures for Ensuring Cybersecurity in May 2025 — the so-called "active cyber defense" law. It authorizes government monitoring of communication-infrastructure metadata and, under independent-committee approval, the access-and-neutralization of attacker-controlled servers before an incident escalates. Japan did not wait for a globally settled legal theory; it built a narrow, procedural, time-bounded authority and shipped it.

The United States has not. There is still no federal statute granting defenders — public or private — a safe harbor for reaching outside their own perimeter. 18 U.S.C. § 1030 (the CFAA) still criminalizes "access without authorization," and the Active Cyber Defense Certainty (ACDC) Act lineage has been reintroduced and shelved repeatedly since 2017.

For architects, this is not a policy abstraction. It is a requirements input with the same weight as a latency SLO or a durability target. The question is not "should we hack back." The question is: can your platform prove, per action, that it was authorized, by whom, under which jurisdiction, and at which instant? That is what "Pass the Clarity" means technically — not a slogan, but a policy contract expressed in code and enforced at runtime.

What Japan Actually Authorized — And What It Didn't

Engineering teams habitually over-read legislation. Japan's framework is deliberately narrow:

  • Bounded scope. Government access to communication-infrastructure metadata with operator cooperation obligations — not a blanket private-sector hack-back license.
  • Procedural gate. Intrusive neutralization requires prior approval from an independent third-party committee, not an executive directive.
  • Time-bounded authority. Authorization attaches to specific attributed infrastructure for a specific window, then expires.
  • No corporate immunity. Private firms still cannot unilaterally penetrate third-party systems.

Architecturally, that shape maps to a policy engine with explicit, auditable decision inputs — jurisdiction, authority basis, approval identifier, attribution confidence, blast radius — and never to a boolean ENABLE_ACTIVE_DEFENSE environment variable. If your design cannot express "approved by committee X, valid until T, for host Y," you cannot operate under this law or any successor to it.

The US Gap: Channels, Not Authorities

Without statutory clarity, US-based defenders are practically constrained to non-intrusive techniques plus law-enforcement channels: DNS sinkholing, tarpits, beacon monitoring, and takedown requests routed through the FBI or a registrar abuse desk. Your action taxonomy must therefore encode a distinct class — actions that require an external legal partner — rather than pretending the distinction does not exist. When (not if) a clarity statute passes, the same taxonomy becomes an allow-list expansion, not a rewrite.

Reference Architecture: Legally-Bounded Active Defense

text
┌──────────────┐   ┌───────────────┐   ┌──────────────┐   ┌──────────────┐
│ Telemetry    │──▶│ Correlation & │──▶│ Policy Gate  │──▶│ Action       │
│ (OTel/STIX)  │   │ Attribution   │   │ (OPA/Rego)   │   │ Gateway      │
└──────┬───────┘   └───────┬───────┘   └──────┬───────┘   └──────┬───────┘
       │                   │                  │                  │
       └───────────────────┴──────────────────┴──────────────────┘
                                   │
                          ┌────────▼─────────┐
                          │ Evidence Ledger  │
                          │ (hash-chained)   │
                          └──────────────────┘

Layer 1 — Telemetry Normalization and Evidence Capture

Emit OpenTelemetry spans for every sensor event and normalize indicators to STIX 2.1. Canonicalize before hashing (RFC 8785) so two nodes with different JSON key orderings produce identical evidence digests.

Layer 2 — Correlation, Attribution, and Confidence Scoring

Never let a single indicator authorize an action. Require at least three independent signals (passive DNS, TLS JA4 fingerprint reuse, sandbox C2 callback) and carry a calibrated attributionConfidence into the policy decision. This is where real-time analysis earns its keep: the trend signals you see on social during a breaking security event are hypotheses, not evidence.

Layer 3 — Policy-as-Code Jurisdiction Gate

Open Policy Agent, not application logic. The gate is synchronous and fail-closed: if it cannot answer, the answer is no.

Layer 4 — Action Gateway with Human-in-the-Loop

Irreversible actions (remote cleanup, packet resets) require a signed human approval token, an idempotency key, and a global kill switch that revokes in-flight authorizations.

Implementing the Gate: A Typed OPA Client

typescript
export type ActionKind =
  | "passive_sinkhole"
  | "tarpit_redirect"
  | "beacon_monitor"
  | "takedown_request"
  | "remote_cleanup";

export interface TargetDescriptor {
  readonly ip: string;
  readonly asn: number;
  readonly country: string;              // ISO 3166-1 alpha-2
  readonly attributionConfidence: number; // 0..1, calibrated offline
}

export interface ActionRequest {
  readonly requestId: string;
  readonly action: ActionKind;
  readonly target: TargetDescriptor;
  readonly operatorId: string;
  readonly justification: string;
  readonly authorization: { readonly committeeApprovalId?: string; readonly leCaseId?: string };
}

export interface PolicyDecision {
  readonly allow: boolean;
  readonly reason: string;
  readonly requiresHumanApproval: boolean;
  readonly maxBlastRadiusHosts: number;
  readonly policyRevision: string;
}

export class PolicyUnavailableError extends Error {
  constructor(message: string, readonly cause?: unknown) {
    super(message);
    this.name = "PolicyUnavailableError";
  }
}

const OPA_URL = process.env.OPA_URL ?? "http://localhost:8181/v1/data/actdefense/authz/decision";

export async function evaluateAction(
  req: ActionRequest,
  opts: { timeoutMs?: number; maxRetries?: number } = {},
): Promise<PolicyDecision> {
  const { timeoutMs = 1500, maxRetries = 2 } = opts;
  let lastErr: unknown;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const res = await fetch(OPA_URL, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ input: req }),
        signal: controller.signal,
      });
      if (!res.ok) throw new Error(`OPA returned HTTP ${res.status}`);
      const body = (await res.json()) as { result?: PolicyDecision };
      if (!body.result) throw new Error("OPA response missing `result`");
      return body.result;
    } catch (err) {
      lastErr = err;
      await new Promise((r) => setTimeout(r, 100 * 2 ** attempt));
    } finally {
      clearTimeout(timer);
    }
  }
  // Fail closed: the platform must never act when it cannot prove authorization.
  throw new PolicyUnavailableError("policy engine unreachable; action blocked", lastErr);
}

The jurisdiction logic is data, versioned and reviewable by counsel:

rego
package actdefense.authz

import future.keywords.if
import future.keywords.in

revision := "2025-11-01"

default decision := {
  "allow": false,
  "reason": "default_deny",
  "requiresHumanApproval": true,
  "maxBlastRadiusHosts": 0,
  "policyRevision": revision,
}

# Non-intrusive techniques: sinkholes, tarpits, beacon monitoring.
decision := {
  "allow": true,
  "reason": "non_intrusive_confirmed",
  "requiresHumanApproval": false,
  "maxBlastRadiusHosts": 1,
  "policyRevision": revision,
} if {
  input.action in {"passive_sinkhole", "tarpit_redirect", "beacon_monitor"}
  input.target.attributionConfidence >= 0.85
}

# Japan: intrusive neutralization requires independent-committee approval.
decision := {
  "allow": true,
  "reason": "jp_committee_approved",
  "requiresHumanApproval": true,
  "maxBlastRadiusHosts": 1,
  "policyRevision": revision,
} if {
  input.target.country == "JP"
  input.action == "remote_cleanup"
  input.target.attributionConfidence >= 0.95
  startswith(input.authorization.committeeApprovalId, "JP-ACC-")
}

# US: no safe harbor — only law-enforcement channels.
decision := {
  "allow": true,
  "reason": "us_law_enforcement_channel",
  "requiresHumanApproval": true,
  "maxBlastRadiusHosts": 0,
  "policyRevision": revision,
} if {
  input.target.country == "US"
  input.action == "takedown_request"
  input.authorization.leCaseId != ""
}

Note the failure mode: a missing committeeApprovalId makes the rule body undefined, so the rule does not fire and default_deny wins. Deny-by-omission is exactly the semantics you want from legal controls.

Tamper-Evident Evidence: The Hash-Chained Ledger

If a regulator asks in 2027 why you touched a host in 2025, a mutable Postgres row is not an answer. Chain every decision into an append-only ledger:

typescript
import { createHash } from "node:crypto";

export interface EvidencePayload {
  readonly requestId: string;
  readonly action: ActionKind;
  readonly target: TargetDescriptor;
  readonly decision: PolicyDecision;
  readonly occurredAt: string; // RFC 3339 / ISO 8601
}

export interface EvidenceRecord extends EvidencePayload {
  readonly seq: number;
  readonly prevHash: string;
  readonly recordHash: string;
}

/** RFC 8785-style canonicalization: sorted keys, no whitespace. */
function canonicalize(value: unknown): string {
  if (value === null || typeof value !== "object") return JSON.stringify(value);
  if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
  const entries = Object.entries(value as Record<string, unknown>)
    .filter(([, v]) => v !== undefined)
    .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
  return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`).join(",")}}`;
}

export class EvidenceLedger {
  private seq = 0;
  private head = "0".repeat(64);

  constructor(private readonly durableAppend: (r: EvidenceRecord) => Promise<void>) {}

  async record(payload: EvidencePayload): Promise<EvidenceRecord> {
    const body = { ...payload, seq: this.seq, prevHash: this.head };
    const recordHash = createHash("sha256").update(canonicalize(body), "utf8").digest("hex");
    const record: EvidenceRecord = { ...body, recordHash };
    await this.durableAppend(record); // durable write BEFORE the head advances
    this.head = recordHash;
    this.seq += 1;
    return record;
  }
}

Persist the head hash to WORM object storage (S3 Object Lock) hourly. Verification is then a single sequential scan — cheap enough to run nightly.

Streaming Backbone Trade-offs

ConcernKafkaNATS JetStreamApache Pulsar
Replay for legal holdExcellent (tiered storage)Moderate (retention limits)Excellent (per-topic tiered)
Operational complexityHighLowHighest
Exactly-once sinksTransactionsDedup windowTransactions + dedup
Best fitEvidence pipelineEdge sensors, low latencyMulti-tenant SaaS

Regardless of broker, assume at-least-once delivery and make the action gateway idempotent on requestId. Duplicate sinkhole writes are harmless; duplicate remote_cleanup invocations are an incident.

Operational Checklist

  • Policy decisions are fail-closed and covered by a chaos test that kills OPA mid-flight.
  • Every action carries a jurisdiction tag resolved from ASN + registry data, refreshed daily.
  • Irreversible actions require a signed approval token with a TTL under 60 minutes.
  • Evidence ledger replication lag is alerted at >5s; legal hold retention is set per case, not globally.
  • A global kill switch revokes in-flight authorizations within one policy revision push.
  • Counsel reviews Rego diffs in the same PR as the code change.

Pitfalls to Avoid

  • Encoding law in application branches. if (country === 'JP') scattered across services guarantees drift. Centralize in the policy bundle.
  • Treating confidence as a boolean. Thresholds differ per action class; a sinkhole at 0.85 and a cleanup at 0.95 are different risk surfaces.
  • Logging decisions but not inputs. Without the exact policy input snapshot, neither counsel nor an incident reviewer can reproduce the decision.
  • Assuming a US clarity statute is imminent. Design for the current gap so that new authority is a data change, not a migration.

Sources & Technical References

0 views•0 comments

Discussion (0)

Join the conversation

Loading comments...

Continue Reading

More production-grade engineering deep dives and architectural guides

Explore all articles