feat: add audit.session.end NATS pipeline for SSH session tracking

Poller publishes session end events via JetStream when SSH sessions
close (normal disconnect or idle timeout). Backend subscribes with a
durable consumer and writes ssh_session_end audit log entries with
duration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-12 16:07:10 -05:00
parent 7aaaeaa1d1
commit acf1790bed
5 changed files with 276 additions and 3 deletions

View File

@@ -124,6 +124,7 @@ func NewPublisher(natsURL string) (*Publisher, error) {
"config.changed.>",
"config.push.rollback.>",
"config.push.alert.>",
"audit.session.end.>",
},
MaxAge: 24 * time.Hour,
})
@@ -306,6 +307,43 @@ func (p *Publisher) PublishPushAlert(ctx context.Context, event PushAlertEvent)
return nil
}
// SessionEndEvent is the payload published to NATS JetStream when an SSH
// relay session ends. The backend subscribes to audit.session.end.> and
// writes an audit log entry with the session duration.
type SessionEndEvent struct {
SessionID string `json:"session_id"`
UserID string `json:"user_id"`
TenantID string `json:"tenant_id"`
DeviceID string `json:"device_id"`
StartTime string `json:"start_time"` // RFC3339
EndTime string `json:"end_time"` // RFC3339
SourceIP string `json:"source_ip"`
Reason string `json:"reason"` // "normal", "idle_timeout", "shutdown"
}
// PublishSessionEnd publishes an SSH session end event to NATS JetStream.
func (p *Publisher) PublishSessionEnd(ctx context.Context, event SessionEndEvent) error {
data, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("marshalling session end event: %w", err)
}
subject := fmt.Sprintf("audit.session.end.%s", event.SessionID)
_, err = p.js.Publish(ctx, subject, data)
if err != nil {
return fmt.Errorf("publishing to %s: %w", subject, err)
}
slog.Debug("published session end event",
"session_id", event.SessionID,
"device_id", event.DeviceID,
"subject", subject,
)
return nil
}
// Conn returns the raw NATS connection for use by other components
// (e.g., CmdResponder for request-reply subscriptions).
func (p *Publisher) Conn() *nats.Conn {