skip to content

Given VPN concentrator logs and CI runner job logs, how do you date an intrusion's spread?

level: middleimportance: must knowfreq 58%

answer

  1. one row per entity, not per alert
  2. join on the leased address, inside its window
  3. every cell carries an evidence citation
  4. observed versus inferred, always marked
  5. a token identity is a service account, not a person

basics

~20 s

Build one entity table — hosts, accounts, credentials — each with earliest and latest observed adversarial activity and the record proving it. Join the two sources on the concentrator-assigned address inside its session window, and mark every cell observed or inferred.

solid answer

~50 s

The deliverable is a dated entity picture, not a story: one row per host, account or credential, with earliest and latest observed activity, how it was reached, what it reached next, and a citation for each. The concentrator gives you session establishment with the certificate subject and the address it assigned; the runner job logs give you which jobs ran, from which remote address, under which token, and what they read. The join key is the assigned address, and it is only an identity for the life of that lease, so constrain every link by the session start and stop timestamps. Then qualify: a session record shows a tunnel, not its contents; a job log shows what the job definition emitted, not everything a compromised step did; a wiped runner workspace proves nothing either way. Mark inferred steps distinctly from observed ones.

code

text · 10 lines
text
# VPN concentrator authentication record (fields abbreviated)
ts=2026-03-04T22:41:07Z  event=session_established  result=success
  cert_subject_cn=contractor-jlee  cert_serial=0x4A1F...
  src_ip=203.0.113.44  assigned_ip=10.64.7.19
  session_id=8813  ...

# CI runner job log, same estate
ts=2026-03-04T22:53:12Z  runner=build-07  job_id=51204  job=deploy-artifacts
  triggered_by=api_token:svc-release  remote_addr=10.64.7.19
  step=checkout  ref=refs/heads/main  ...

go deeper

for a junior

Know that hosts and accounts are dated from records, and that an internal IP address identifies a session only while its lease lasts.

for a middle

Be ready to name the join keys between a VPN log and a build log, and to say for each record what it does and does not prove.

for a senior

Show the discipline that survives review: a citation per cell, observed and inferred kept apart, a stated stopping rule per entity, and a versioned picture.

for a principal

Own the picture as the organisation's single authoritative dated account of the intrusion, re-checkable by people who were never in the room.

## The artefact you are producing Not a narrative. A **dated entity picture**: one row per entity — host, account, credential, service — with, for each, the earliest observed adversarial activity, the latest, how the entity came to be involved, what it reached next, and a citation to the record supporting every one of those cells. This table is what containment scope, credential resets and every later statement about the intrusion are derived from, and it will be re-read months later by people who were not in the room. Normalise timestamps to a single zone before you join anything. Mixed local times silently reorder a timeline. ## Pivots that actually survive Across a VPN concentrator and a build estate, the durable pivots are: - the **client certificate subject and serial** presented at session establishment; - the **address the concentrator assigned** to that session, bounded by the session start and stop; - the **token or service-account identity** a job ran under, and the **job id**; - the **git ref and commit** a job built, and **artefact hashes** it produced or pulled; - **registry and package pull records**, which often live longer than the job logs themselves. ## The join, and the rule that keeps it honest The concentrator says: at 22:41 UTC a session was established for certificate subject `contractor-jlee`, from an external address, and was assigned an internal address. The runner job log says: at 22:53 UTC a job ran on runner `build-07`, its remote address was that same internal address, and it authenticated with a service token. That is a strong link — **inside the lease**. Assigned addresses are recycled, often within the same day. So the rule is: an address is an identity only between the session's start and stop timestamps, and every join must be constrained by that window. Where a lease boundary is ambiguous, downgrade the link to inferred and go looking for a second, independent pivot such as the certificate serial or a shared artefact hash. ## What each source cannot tell you - **Concentrator records** show sessions: established, duration, bytes if you are lucky. They do not show what moved inside the tunnel. Bytes moved is not what the bytes were. - **Job logs** show what the job definition ran and what its steps wrote to standard output. A step that was tampered with can do work that never appears there, and log output can be suppressed by the step itself. - **Runner workspaces** are ephemeral by design and are wiped or reused between jobs. Absence of dropped files on a runner is not evidence that nothing was dropped; it is evidence that you looked too late. - **Token identity is a service account**, not a person. `svc-release` in a job log identifies the credential used, and a human name attached to it in some inventory is an inference, not an observation. ## Observed versus inferred Keep them visually distinct in the same table. An observed cell has a record behind it. An inferred cell has reasoning behind it and names the evidence that is missing. Anyone reading the picture, including counsel, must be able to strip the inferences out and still have a coherent, defensible core. Mixing the two is how an investigation ends up asserting things nobody can support. ## Both frontiers, and when to stop The picture grows in two directions: backwards toward first foothold and forwards through spread. An entity enters when there is evidence tying it to adversary-controlled activity, and it carries a note of *why* you believe that activity was adversary-driven rather than routine — build estates are full of automation that looks alarming out of context. A workable stopping rule for an entity: its inbound and outbound activity across the relevant window is accounted for, either as adversary activity you have dated or as routine activity you have explained. Unaccounted activity keeps the entity open. 'No alerts on it' is not an account of its activity. ## Keeping it usable Version the picture and date each revision. Facts change: a link is downgraded, a host is cleared, an entity is added a week later. If the table is edited in place with no history, two people quoting it a month apart will quote different things, and neither will know why.

  • The same assigned address was reused by a different session later that day. How do you avoid a false link?
    Constrain every join by the session start and stop timestamps from the concentrator, and treat the address as an identity only inside that lease. Where the boundary is ambiguous, mark the link inferred and look for a second independent pivot — the certificate serial, a token id, an artefact hash — before you promote it to observed.
  • The runner workspace was wiped by the next job. What have you lost, and what survives?
    The working tree and anything dropped on disk are gone. What survives is the job log, the versioned job definition, artefacts pushed to the store, registry and package pull records, anything the job wrote to a remote system, and host-level process or network telemetry if the runner had any. An empty workspace is not evidence the step was benign.
  • How do you record a step you believe happened but cannot show?
    As an inference, in the same table but visually distinct, with the reasoning written out and the missing evidence named. Never as a dated fact. A reader must be able to remove every inference and still be left with a defensible core, because that is exactly what a challenge to the report will do.

saying these in an interview costs you the question

  • Joins records on IP address without bounding the lease window
  • Reads a service-account token as proof of the named engineer
  • Says an empty runner workspace shows nothing malicious ran
  • Builds the timeline from alerts rather than from entities
  • Writes inferred steps into the picture as observed facts
  • Leaves the table edited in place with no version history

context