Preparar uma sessão que não dependa das tuas chaves pessoais
O runner abaixo cria chaves descartáveis e inicia oito instâncias temporárias de sshd em loopback. Usa a conta local que o executa, sem criar contas ou alterar o serviço SSH do sistema. A autorização aponta apenas para ficheiros temporários; passwords, agent, PTY, encaminhamento e rc do utilizador estão desativados. O cliente recebe uma configuração explícita e known_hosts construído a partir da chave do host que o próprio ensaio gerou. O servidor aplica um comando Python fixo, que regista o pedido original sem o executar como código. Disponibiliza Python 3 e os cinco executáveis correspondentes de OpenSSH 10.5p1: ssh, sshd, sshd-session, sshd-auth e ssh-keygen. A execução registada compilou essa versão com OpenSSL 3.6.1 e sem PAM. Não é um modelo de configuração de produção nem testa integração de contas empresariais.
Comparar três percursos com resultados distintos
Começa pelas observações rawAccepted, wrongKey e wrongHost. A primeira confirma o host, autentica a chave autorizada e devolve JSON do comando fixo. A segunda chega ao servidor, mas oferece uma chave sem autorização e não executa o comando. A terceira contém uma chave de host deliberadamente diferente em known_hosts; o cliente interrompe o percurso antes de autenticar. Duas recusas acabam com código 255, mas não têm a mesma causa. Lê o diagnóstico do cliente e o log do servidor antes de escolher uma alteração. Numa investigação real, escreve destino, conta, identidade oferecida, configuração e fase alcançada. Não substituas uma chave do utilizador para tratar uma identidade de host desconhecida. O laboratório conhece a proveniência da sua chave de host; num rebuild real é necessária uma referência fiável e uma atualização delimitada da confiança.
Separar autenticação aceite e falha da operação
Na segunda sessão do controlo positivo, o cliente pede fail-seven. O comando fixo imprime o pedido e termina deliberadamente com sete. O log continua a mostrar autenticação publickey aceite. Este resultado demonstra que uma execução pode falhar depois de o acesso ter sido estabelecido. No banco fictício Cais, a proposta de rodar a chave não tratava o motivo do erro do batch. O suporte deve confirmar o contrato da operação, os efeitos já produzidos e se repetir pode duplicar uma entrega. O script do laboratório não processa ficheiros nem transações; apenas cria uma observação limitada e reproduzível. O texto enviado pelo cliente é dado, não uma instrução executada. Guarda esta distinção ao documentar acessos com ForceCommand, onde ter uma sessão autorizada não significa poder abrir uma shell ou invocar qualquer operação.
Repetir o ensaio e registar o alcance
Guarda o código como ssh-sessions.py e usa o comando do guião seguinte, indicando os caminhos dos executáveis da mesma versão. O JSON contém configuração, campos dos certificados, diagnósticos e quarenta verificações. O nome da conta local é substituído por lab-account no texto de evidência; chaves públicas e caminhos temporários continuam identificáveis como artefactos do ensaio. As chaves privadas são removidas com o diretório temporário. Uma repetição gera outras chaves e portas, pelo que a comparação deve usar aceitações, recusas e motivos. Se uma condição falhar, conserva o diagnóstico em vez de transformar a falha num resultado esperado. Resumo: configuração, confiança, autenticação e operação são etapas relacionadas mas distintas. Liga esta aula à configuração efetiva, identidade de hosts e gestão de incidentes. Bastions, túneis, contas de produção e certificados de host continuam fora destas sessões.
#!/usr/bin/env python3
"""Original loopback SSH authentication lab. OpenSSH 10.5p1 tools, disposable keys, fixed forced command."""
import argparse,hashlib,json,os,platform,pwd,re,shlex,shutil,signal,socket,subprocess,sys,tempfile,time
from pathlib import Path
def main():
p=argparse.ArgumentParser(description=__doc__)
for name in ['ssh','sshd','sshd-session','sshd-auth','keygen']:p.add_argument('--'+name,required=True)
p.add_argument('--output',default='ssh-sessions-evidence.json');args=p.parse_args()
user=pwd.getpwuid(os.getuid()).pw_name
def call(argv,expected=0):
x=subprocess.run([str(a)for a in argv],text=True,capture_output=True,timeout=20)
if expected is not None:assert x.returncode==expected,(argv,x.returncode,x.stdout,x.stderr)
return x
v=call([args.ssh,'-V']);version=(v.stdout+v.stderr).strip();assert 'OpenSSH_10.5p1' in version,version
checks=[];observations={};configs=[]
def check(label,condition):assert condition,label;checks.append(label)
with tempfile.TemporaryDirectory(prefix='dr-ssh-sessions-') as temp:
root=Path(temp);root.chmod(0o700)
def key(name):
path=root/name;call([args.keygen,'-q','-t','ed25519','-N','','-C','dr-disposable-'+name,'-f',path]);return path
host=key('host');wronghost=key('wronghost');identity=key('identity');wrong=key('wrong');ca=key('ca')
force=root/'forced.py';force.write_text('import json,os,sys\nrequest=os.environ.get("SSH_ORIGINAL_COMMAND","")\nprint(json.dumps({"forced":True,"requested":request}))\nsys.exit(7 if request=="fail-seven" else 0)\n')
forced_command=shlex.quote(sys.executable)+' '+shlex.quote(str(force))
certs={}
for label,principal,validity,extras,serial in [
('valid',user,'-5m:+1h',[],101),('principal','other-lab-principal','-5m:+1h',[],102),
('expired',user,'-2h:-1h',[],103),('critical',user,'-5m:+1h',['-O','critical:dr-unknown=bounded-lab'],104)]:
path=root/('cert-'+label+'.pub');shutil.copy2(str(identity)+'.pub',path)
call([args.keygen,'-q','-s',ca,'-I','dr-'+label,'-z',str(serial),'-n',principal,'-V',validity,'-O','clear',*extras,path])
certs[label]=path.with_name(path.stem+'-cert.pub')
assert certs[label].exists()
krl=root/'revoked.krl';call([args.keygen,'-k','-f',krl,certs['valid']])
inspect=call([args.keygen,'-Q','-f',krl,certs['valid']],expected=1)
check('KRL identifies valid certificate as revoked','REVOKED' in inspect.stdout+inspect.stderr)
def experiment(label,offered,cert=None,trust_ca=False,bad_host=False,revoked=False,success=False):
folder=root/("run-"+label);folder.mkdir();authorized=folder/'authorized_keys';authorized.write_text(Path(str(identity)+'.pub').read_text()if not trust_ca else '');authorized.chmod(0o600)
with socket.socket()as sock:sock.bind(('127.0.0.1',0));port=sock.getsockname()[1]
known=folder/'known_hosts';pub=Path(str(wronghost if bad_host else host)+'.pub').read_text().split();known.write_text('[127.0.0.1]:'+str(port)+' '+pub[0]+' '+pub[1]+'\n')
conf=folder/'sshd.conf';log=folder/'server.log'
text=f'''ListenAddress 127.0.0.1
Port {port}
HostKey {host}
PidFile {folder}/pid
SshdSessionPath {args.sshd_session}
SshdAuthPath {args.sshd_auth}
AuthorizedKeysFile {authorized}
AuthorizedPrincipalsFile none
TrustedUserCAKeys {str(ca)+'.pub' if trust_ca else 'none'}
RevokedKeys {str(krl) if revoked else 'none'}
AllowUsers {user}
AuthenticationMethods publickey
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PermitUserEnvironment no
PermitUserRC no
PermitTTY no
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
AllowStreamLocalForwarding no
PermitTunnel no
DisableForwarding yes
StrictModes yes
UseDNS no
LogLevel VERBOSE
ForceCommand {forced_command}
SetEnv ZDOTDIR={folder}
'''
conf.write_text(text);call([args.sshd,'-t','-f',conf]);effective=call([args.sshd,'-T','-f',conf]).stdout
configs.append({'label':label,'configuration':text,'effective':effective})
argv=[args.ssh,'-F','/dev/null','-vv','-p',str(port),'-l',user,'-i',str(offered),'-o','BatchMode=yes','-o','IdentitiesOnly=yes','-o','IdentityAgent=none','-o','UserKnownHostsFile='+str(known),'-o','GlobalKnownHostsFile=/dev/null','-o','StrictHostKeyChecking=yes','-o','UpdateHostKeys=no','-o','PreferredAuthentications=publickey','-o','PasswordAuthentication=no','-o','KbdInteractiveAuthentication=no','-o','ConnectTimeout=5','-o','ConnectionAttempts=1','-o','ControlMaster=no','-o','ControlPath=none','-o','ProxyCommand=none','-o','ProxyJump=none','-o','RequestTTY=no','-o','CertificateFile='+str(cert or 'none'),'127.0.0.1']
proc=None
with log.open('w')as lf:
proc=subprocess.Popen([args.sshd,'-D','-e','-f',str(conf)],stdout=lf,stderr=lf,start_new_session=True)
try:
for _ in range(100):
assert proc.poll()is None,log.read_text()
try:
with socket.create_connection(('127.0.0.1',port),timeout=.1):break
except OSError:time.sleep(.03)
else:raise AssertionError('sshd did not listen')
result=call(argv+['requested-check'],expected=None)
check(label+': expected client exit',result.returncode==(0 if success else 255))
if success:
check(label+': forced command executed',json.loads(result.stdout)=={'forced':True,'requested':'requested-check'})
check(label+': publickey authentication observed','Authenticated to 'in result.stderr and 'using "publickey"'in result.stderr)
if label=='rawAccepted':
failed=call(argv+['fail-seven'],expected=7)
check('remote exit 7 follows successful authentication',json.loads(failed.stdout)=={'forced':True,'requested':'fail-seven'}and'Authenticated to 'in failed.stderr)
observations['remoteCommandFailure']={'exit':failed.returncode,'stdout':failed.stdout,'stderr':failed.stderr}
else:
check(label+': no forced command output',result.stdout=='')
check(label+': no authentication success','Authenticated to 'not in result.stderr)
observations[label]={'exit':result.returncode,'stdout':result.stdout,'stderr':result.stderr}
except Exception:
print(json.dumps({'experiment':label,'serverLog':log.read_text()}));raise
finally:
if proc.poll()is None:
os.killpg(proc.pid,signal.SIGTERM)
try:proc.wait(timeout=5)
except subprocess.TimeoutExpired:os.killpg(proc.pid,signal.SIGKILL);proc.wait(timeout=5)
check(label+': daemon stopped',proc.poll()is not None)
observations[label]['serverLog']=log.read_text()
experiment('rawAccepted',identity,success=True)
experiment('wrongKey',wrong)
experiment('wrongHost',identity,bad_host=True)
experiment('certificateAccepted',identity,certs['valid'],trust_ca=True,success=True)
experiment('wrongPrincipal',identity,certs['principal'],trust_ca=True)
experiment('expiredCertificate',identity,certs['expired'],trust_ca=True)
experiment('unknownCriticalOption',identity,certs['critical'],trust_ca=True)
experiment('revokedCertificate',identity,certs['valid'],trust_ca=True,revoked=True)
check('wrong host fails host verification','REMOTE HOST IDENTIFICATION HAS CHANGED' in observations['wrongHost']['stderr'])
check('wrong principal is diagnosed','name is not a listed principal' in observations['wrongPrincipal']['serverLog'])
check('expired certificate is diagnosed','Certificate invalid: expired' in observations['expiredCertificate']['serverLog'])
check('unknown critical option is diagnosed','Certificate critical option "dr-unknown" is not supported' in observations['unknownCriticalOption']['serverLog'])
check('revocation is diagnosed','revoked' in observations['revokedCertificate']['serverLog'].lower())
certificate_details={k:call([args.keygen,'-L','-f',path]).stdout for k,path in certs.items()}
check('temporary keys and configuration removed',not root.exists())
report={'sshVersion':version,'pythonVersion':platform.python_version(),'checks':checks,'observations':observations,'configurations':configs,'certificateDetails':certificate_details,'runnerSha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),'scope':'Eight loopback sshd instances and nine bounded SSH sessions using disposable keys and a fixed ForceCommand. Existing local account, no account creation or system SSH changes. No passwords, personal authorized_keys, agent, user rc, PTY or forwarding. One remote command intentionally exits 7 after successful authentication. Certificates: trusted principal, mismatched principal, expired, unknown critical option and KRL rejection. No production, bastion, host-certificate session, CA rotation or human workshop.','redactions':'Local account name replaced by lab-account in textual evidence; temporary paths and disposable public keys retained.'}
encoded=json.dumps(report,indent=2).replace(user,'lab-account')
Path(args.output).write_text(encoded+'\n');print(json.dumps({'instances':8,'sessions':9,'checks':len(checks),'output':args.output}))
if __name__=='__main__':main()
Cais tem autenticação aceite e código remoto sete. A investigação passa para a operação do batch, mantendo a correlação com a sessão SSH.
Armadilhas comuns
Rodar chaves por qualquer código não zero, remover confiança para contornar um alerta ou assumir que ForceCommand executa livremente o pedido do cliente.
Tópicos relacionados: Identidade do servidor e utilizador · Diagnóstico de jobs
Um código isolado não identifica a causa. A combinação de fases, logs e resultado do comando permite escolher uma intervenção proporcional.
Referência: OpenSSH daemon operation · OpenSSH concepts and OpenBSD-current manuals consulted 2026-09-29; distribution defaults vary