Piloto · v0.1.0 · Debian/Ubuntu
PythonLinuxOpenSSHsystemdjournaldsystemd-logindHTTPSJSONSSH

Problema

Depois de centralizar sessões RDP, o próximo passo natural foi responder a mesma pergunta para servidores Linux: quem acessou por SSH, de onde veio e por quanto tempo a sessão existiu?

A implementação, porém, não poderia simplesmente transformar cada linha de sshd em um evento.

Uma conexão SSH normal costuma produzir várias evidências para a mesma sessão:

Accepted publickey/password ... from <ip> port <port>
pam_unix(sshd:session): session opened for user <usuario>
pam_unix(sshd:session): session closed for user <usuario>

Ao mesmo tempo, sessões muito curtas podem abrir e fechar entre duas observações do estado atual.

O problema passou a ser identificar uma única sessão lógica a partir de fontes que possuem propósitos diferentes.

Solução

SSH Session Agent é o collector Linux da Remote Session API.

A versão 0.1.0 combina:

  • systemd-logind como autoridade principal para sessões persistentes;
  • journald/OpenSSH como evidência de autenticação e origem;
  • sinais PAM para abertura e fechamento;
  • boot_id do kernel para separar sessões entre reboots;
  • spool local durável;
  • snapshots periódicos;
  • contrato genérico /api/v2.

O Agent é escrito em Python 3.10+ e não possui dependências Python de runtime externas.

Arquitetura local

Arquitetura local do SSH Session Agent
01 OpenSSH / sshd Produz evidências de autenticação, PAM e origem remota já disponíveis no sistema operacional.
02 SSH Session Agent Daemon Python executado por uma conta de sistema dedicada, com HTTPS outbound e sem shell interativo.
03 Correlação logind + journald/PAM systemd-logind é autoridade de sessão; journald adiciona usuário, IP, porta, método, PID e timestamps quando disponíveis.
04A state.json Persiste boot_id, cursor do journal, sessões ativas e observações recentes de autenticação de forma atômica.
04B Remote Session API /api/v2 Recebe LOGON/LOGOFF normalizados e snapshots periódicos usando credencial exclusiva por servidor.
04C spool + dead-letter Fila durável e ordenada sobrevive à indisponibilidade da API; itens locais inválidos são isolados para investigação.
05 Preflight + health systemd Valida Debian/Ubuntu, Python 3.10+, logind, acesso ao journald, OpenSSH, state dir e health HTTPS da API.

A arquitetura evita instrumentar o shell ou o processo de login do usuário.

O Agent observa apenas os metadados que o sistema operacional e o OpenSSH já produzem.

Logind como autoridade de sessão

O collector consulta:

loginctl list-sessions
loginctl show-session <id>

Uma sessão só entra no domínio SSH quando possui características como:

Remote=yes
Service=ssh ou sshd
State=active ou online

A identidade local é convertida para:

provider_session_id = logind:<session-id>

Esse ID não é tratado isoladamente como globalmente único. A API o interpreta junto com o servidor, protocolo e boot_id.

Journald como fonte de evidência

O Agent lê o journal da unit ssh.service ou sshd.service usando saída JSON e cursor persistente.

Dos registros Accepted, ele retém somente informações necessárias para correlação:

  • método de autenticação;
  • usuário;
  • IP de origem;
  • porta TCP de origem;
  • PID;
  • timestamp;
  • cursor do journald.

O restante da linha é deliberadamente descartado.

Isso significa que fingerprint e material de chave não são persistidos apenas porque aparecem no log original.

Por que não emitir um LOGON a cada Accepted

Se Accepted e pam session opened fossem transformados independentemente em lifecycle, uma única conexão poderia virar duas ou mais sessões no sistema central.

A estratégia atual é diferente:

logind       -> prova que a sessão persistente existe
Accepted     -> adiciona IP, porta e método
PAM OPEN     -> melhora timestamp de abertura
PAM CLOSE    -> melhora timestamp de encerramento

O resultado é um único LOGON por sessão persistente.

Matching por PID

Quando uma nova sessão aparece no logind, o Agent tenta associá-la a uma observação de autenticação recente.

A associação mais forte é:

leader PID da sessão logind == PID da observação do sshd

Quando esse join não está disponível, existe um fallback mais restritivo por usuário e IP dentro de uma janela temporal curta.

O objetivo é evitar associar a uma sessão atual uma autenticação anterior do mesmo usuário e endereço.

Janela temporal de autenticação

A configuração padrão utiliza:

{
  "auth_match_window_seconds": 180
}

Observações antigas são removidas do state para evitar crescimento indefinido e reduzir risco de matching incorreto.

Sessões efêmeras

Nem todo acesso SSH permanece tempo suficiente para aparecer no próximo poll do logind.

Um exemplo é:

ssh servidor 'comando-curto'

Se o journald contém uma sequência completa:

Accepted
PAM OPEN
PAM CLOSE

e nenhuma sessão persistente do logind reivindicou aquela observação, o Agent pode reconstruir um par determinístico:

LOGON
LOGOFF

A identidade utiliza PID e um hash do cursor do evento Accepted.

Assim, sessões curtas não dependem exclusivamente do timing do poll.

Bootstrap sem inventar histórico

No primeiro start, podem existir sessões SSH já abertas antes do Agent começar a observar.

Criar um LOGON retroativo nesse momento seria inventar um evento que não foi realmente observado.

Por isso o bootstrap funciona assim:

primeiro ciclo
 -> observar sessões existentes
 -> enviar snapshot
 -> persistir estado
 -> não emitir LOGON histórico artificial

Essa distinção entre observado e inferido é central no projeto.

boot_id

O Linux fornece um identificador do boot atual em:

/proc/sys/kernel/random/boot_id

O Agent inclui esse valor em eventos e snapshots.

Quando o boot muda:

  • IDs do logind e PIDs antigos não são reutilizados como se fossem a mesma sessão;
  • observações de autenticação do boot anterior não participam do matching;
  • um snapshot do novo boot é enviado imediatamente;
  • a API pode fechar sessões antigas com end_reason=REBOOT.

O Agent não fabrica LOGOFFs antigos após reinicialização.

Lifecycle SSH

O lifecycle produzido diretamente pelo Agent é simples:

LOGON -> ACTIVE
LOGOFF -> CLOSED

Fechamentos por reconciliação ou reboot são responsabilidade da API central.

Isso mantém a coleta local focada em fatos observáveis e deixa a interpretação consolidada no servidor.

Poll e snapshots

Valores padrão:

{
  "poll_seconds": 10,
  "snapshot_seconds": 30,
  "request_timeout_seconds": 10,
  "auth_match_window_seconds": 180,
  "max_batch_events": 100
}

O daemon executa ciclos contínuos.

A cada ciclo ele:

1. lê boot_id e state
2. lê journald após o cursor
3. mescla Accepted e sinais PAM
4. enumera sessões logind
5. resolve novas sessões e encerramentos
6. detecta sessões efêmeras
7. coloca eventos/snapshot no spool
8. persiste o state
9. tenta enviar a fila em ordem

Cursor do journald

O state mantém journal_cursor para leitura incremental.

Quando disponível, o Agent usa:

journalctl --after-cursor <cursor>

Se o cursor deixou de existir por rotação ou vacuum, o Agent volta para um lookback limitado, em vez de tentar reprocessar histórico indefinido.

State atômico

O estado fica em:

/var/lib/ssh-session-agent/state.json

Ele contém:

  • boot_id;
  • cursor do journal;
  • sessões ativas conhecidas;
  • observações recentes de autenticação;
  • timestamp do último snapshot.

A gravação utiliza arquivo temporário, fsync, modo 0600 e os.replace.

Isso reduz o risco de um crash deixar um JSON parcialmente escrito.

Spool antes do checkpoint

Eventos e snapshots são persistidos em:

/var/lib/ssh-session-agent/spool/

A regra de durabilidade é:

produzir telemetria
 -> gravar spool atomicamente
 -> salvar novo state
 -> transmitir
 -> remover somente após sucesso

Essa ordem impede que o cursor avance silenciosamente para além de telemetria que nunca foi colocada em fila.

Uma queda em uma janela pequena pode causar replay, mas a API v2 é idempotente.

Perder silenciosamente o evento seria pior do que reenviá-lo.

Ordenação da fila

O flush processa o spool em ordem.

Se um item falha:

item atual permanece
flush para
itens posteriores aguardam

Isso evita que eventos posteriores ultrapassem um erro anterior e preserva uma ordem operacional previsível.

Dead-letter

Um spool local corrompido não deve bloquear toda a fila para sempre.

Itens que não podem ser desserializados ou não possuem a estrutura mínima são movidos para:

/var/lib/ssh-session-agent/dead-letter/

O item fica disponível para investigação enquanto o restante da fila pode continuar.

Contrato v2

O Agent envia eventos para:

POST /api/v2/agent/events

Com identidade genérica:

{
  "platform": "linux",
  "protocol": "SSH",
  "boot_id": "...",
  "events": [
    {
      "type": "LOGON",
      "provider_session_id": "logind:42",
      "provider_event_id": "logind:42:logon",
      "username": "example.user",
      "source_ip": "192.0.2.20",
      "source_port": 53122
    }
  ]
}

Snapshots utilizam:

POST /api/v2/agent/snapshot

Autenticação

Cada host Linux recebe credencial própria:

X-Server-ID: <server-id>
Authorization: Bearer <agent-secret>

O Agent não recebe a query API key usada por consumidores de leitura.

O cliente HTTP utiliza a standard library do Python e mantém validação TLS habilitada.

Quando necessário, a configuração pode apontar para um CA bundle específico, sem desabilitar verificação de certificado.

Instalação

O checkout de produção recomendado fica em:

/opt/SSH-Session-Agent

A configuração fica separada em:

/etc/ssh-session-agent/config.json

E o state em:

/var/lib/ssh-session-agent

O instalador cria um usuário de sistema dedicado ssh-session-agent, sem shell interativo, e concede somente o acesso necessário ao journal.

Hardening systemd

A unit possui controles de isolamento como:

NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictNamespaces=true
LockPersonality=true

A escrita fica explicitamente limitada ao state dir e a configuração é montada como read-only para o serviço.

O processo não precisa executar como root.

Preflight

Antes de iniciar produção, o Agent valida:

  • Python 3.10+;
  • Debian/Ubuntu;
  • systemctl, journalctl e loginctl;
  • systemd-logind ativo;
  • unit OpenSSH disponível;
  • acesso read-only ao journal;
  • escrita no state dir;
  • CA configurada, quando usada;
  • health HTTPS da API v2.

O health check de preflight não envia o bearer token do Agent.

Instalação controlada

O instalador habilita a unit, mas não precisa iniciá-la imediatamente.

Isso permite executar:

instalar
 -> revisar preflight
 -> validar credencial/configuração
 -> iniciar serviço

Em infraestrutura, separar “copiar arquivos” de “começar a produzir telemetria” reduz o impacto de um erro de preparação.

Atualização e rollback

O update mantém configuração, state e spool fora do checkout.

Depois de posicionar o repositório no commit aprovado:

sudo bash /opt/SSH-Session-Agent/scripts/update.sh

O script executa preflight como o usuário de serviço, reinicia a unit e confirma que ela ficou ativa.

Para contenção imediata:

sudo systemctl stop ssh-session-agent.service

O uninstall padrão remove a unit mas preserva configuração e state. Purge destrutivo exige flags explícitas.

Privacidade

O Agent não coleta:

  • comandos executados;
  • stdin/stdout;
  • conteúdo de terminal;
  • passwords;
  • chave privada;
  • conteúdo ou fingerprint de chave pública;
  • clipboard.

Ele retém apenas metadados necessários para representar a sessão e sua origem.

Essa fronteira foi definida desde o MVP para evitar que observabilidade de acesso se transforme em gravação de atividade do usuário.

Homologação e rollout

O MVP foi planejado para começar em um único Linux não crítico antes de distribuição ampla.

O gate cobre cenários como:

  • bootstrap sem falso LOGON;
  • autenticação por chave e senha;
  • sessões simultâneas;
  • logout normal;
  • sessão efêmera;
  • indisponibilidade da API e replay do spool;
  • reboot;
  • ausência de duplicação;
  • preservação do caminho RDP existente.

O rollout Linux amplo só ocorre depois de uma janela estável do piloto.

Estado atual

A versão 0.1.0 entrega um collector SSH funcional para Debian/Ubuntu com:

  • logind como autoridade de sessão persistente;
  • journald/OpenSSH para enrichment;
  • sinais PAM;
  • recuperação de sessões efêmeras;
  • boot_id;
  • state e spool atômicos;
  • replay idempotente;
  • snapshots periódicos;
  • TLS validado;
  • service account dedicado;
  • hardening systemd;
  • contrato Remote Session API v2.

O que o projeto demonstra

SSH Session Agent não é apenas um parser de logs.

O projeto demonstra como combinar múltiplas fontes de evidência com níveis de confiança diferentes, evitar duplicação, recuperar eventos curtos, preservar telemetria durante falhas e estabelecer uma fronteira de privacidade clara.

Ele também representa a mudança arquitetural mais importante do sistema de monitoramento: a passagem de uma solução exclusivamente RDP para um domínio comum de sessões remotas, mantendo cada collector especializado no sistema operacional que observa.