# B19 — Jenkinsfile: scp -o StrictHostKeyChecking=no against files.pharo.org

Bug ref      : always.md B.19 ; pharo.md §7
Severity     : HIGH (MITM upload path silently accepts any host key)
File         : Jenkinsfile
Lines (HEAD) : 97-403 (every `scp -o StrictHostKeyChecking=no …` invocation)

## Problem

```groovy
sh "scp -o StrictHostKeyChecking=no \
   ${gtkBundleName} \
   pharo-ci@files.pharo.org:vm/pharo-spur64-headless/win/${gtkBundleName}"
```

`StrictHostKeyChecking=no` tells ssh to accept whatever host key the
remote presents. A network attacker who can redirect connections to
`files.pharo.org` collects the upload (the freshly signed VM
artifact) without the operator noticing.

There are roughly 6-10 of these invocations across the file (all in
the publish stages).

## Fix

Pre-populate a `known_hosts` file with the expected
`files.pharo.org` SSH host key and use
`StrictHostKeyChecking=yes` (or the equivalent `=accept-new` only
on first run).

```diff
diff --git a/Jenkinsfile b/Jenkinsfile
index 412710000..adebcb8e5 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -94,11 +94,11 @@ def buildGTKBundle(){
 				
 				if(!isPullRequest() && isMainBranch()){
 					sshagent (credentials: ['files-pharo-org-inria']) {
-						sh "scp -o StrictHostKeyChecking=no \
+						sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 						${gtkBundleName} \
 						pharo-ci@files.pharo.org:vm/pharo-spur64-headless/win/${gtkBundleName}"
 
-						sh "scp -o StrictHostKeyChecking=no \
+						sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 						${gtkBundleName} \
 						pharo-ci@files.pharo.org:vm/pharo-spur64-headless/win/latest${mainBranchVersion()}-win64-GTK.zip"
 					}
@@ -311,38 +311,38 @@ def upload(platform, configuration, archiveName, isStableRelease = false) {
 	def expandedHeadersFileName = sh(returnStdout: true, script: "ls build/build/packages/PharoVM-*-${archiveName}-include.zip").trim()
 
 	sshagent (credentials: ['files-pharo-org-inria']) {
-		sh "scp -o StrictHostKeyChecking=no \
+		sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 		${expandedBinaryFileName} \
 		pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}-headless/${platform}"
-		sh "scp -o StrictHostKeyChecking=no \
+		sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 		${expandedBinaryFileName} \
 		pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}-headless/${platform}/latest${mainBranchVersion()}.zip"
 
-		sh "scp -o StrictHostKeyChecking=no \
+		sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 		${expandedHeadersFileName} \
 		pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}-headless/${platform}/include"
-		sh "scp -o StrictHostKeyChecking=no \
+		sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 		${expandedHeadersFileName} \
 		pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}-headless/${platform}/include/latest${mainBranchVersion()}.zip"
 
 		// Upload Souces ZIP 
-		sh "scp -o StrictHostKeyChecking=no \
+		sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 		${expandedCSourceFileName} \
 		pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}-headless/${platform}/source"
-		sh "scp -o StrictHostKeyChecking=no \
+		sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 		${expandedCSourceFileName} \
 		pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}-headless/${platform}/source/latest${mainBranchVersion()}.zip"
 
 		// Upload Sources TAR.GZ
-		sh "scp -o StrictHostKeyChecking=no \
+		sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 		${expandedCSourceTarName} \
 		pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}-headless/${platform}/source"
-		sh "scp -o StrictHostKeyChecking=no \
+		sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 		${expandedCSourceTarName} \
 		pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}-headless/${platform}/source/latest${mainBranchVersion()}.tar.gz"
 		
 		if(isStableRelease){
-			sh "scp -o StrictHostKeyChecking=no \
+			sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 			${expandedBinaryFileName} \
 			pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}-headless/${platform}/stable${mainBranchVersion()}.zip"
 		}
@@ -359,15 +359,15 @@ def uploadStockReplacement(platform, configuration, archiveName, isStableRelease
 	def expandedBinaryFileName = sh(returnStdout: true, script: "ls build-stockReplacement/build/packages/PharoVM-*-${archiveName}-bin.zip").trim()
 
 	sshagent (credentials: ['files-pharo-org-inria']) {
-		sh "scp -o StrictHostKeyChecking=no \
+		sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 		${expandedBinaryFileName} \
 		pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}/${platform}"
-		sh "scp -o StrictHostKeyChecking=no \
+		sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 		${expandedBinaryFileName} \
 		pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}/${platform}/latestReplacement${mainBranchVersion()}.zip"
 
 		if(isStableRelease){
-			sh "scp -o StrictHostKeyChecking=no \
+			sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 			${expandedBinaryFileName} \
 			pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}/${platform}/stable${mainBranchVersion()}.zip"
 		}
@@ -390,15 +390,15 @@ def uploadStackVM(platform, configuration, archiveName, isStableRelease = false)
 	sh(script: "cp ${oldName} ${expandedBinaryFileName}")
 	
 	sshagent (credentials: ['files-pharo-org-inria']) {
-		sh "scp -o StrictHostKeyChecking=no \
+		sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 		${expandedBinaryFileName} \
 		pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}-headless/${platform}"
-		sh "scp -o StrictHostKeyChecking=no \
+		sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 		${expandedBinaryFileName} \
 		pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}-headless/${platform}/latestStackVM${mainBranchVersion()}.zip"
 
 		if(isStableRelease){
-			sh "scp -o StrictHostKeyChecking=no \
+			sh "scp -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$WORKSPACE/.known_hosts/files.pharo.org \
 			${expandedBinaryFileName} \
 			pharo-ci@files.pharo.org:vm/pharo-spur${wordSize}-headless/${platform}/stableStackVM${mainBranchVersion()}.zip"
 		}
```

Replace `AAAA<pinned-key>...` with the actual upstream host key.
Obtain once with `ssh-keyscan -t ed25519 files.pharo.org` and
verify out-of-band (e.g. via the Pharo infrastructure team) before
committing.

## Test plan

- Run the publish stage: scp succeeds with the pinned key.
- Replace the pinned key with a placeholder: scp fails with
  `Host key verification failed`. Build aborts.
- DNS-redirect files.pharo.org to a different host for a test run:
  scp fails with `Host key verification failed`. The artifact does
  not leak.

## Risk notes

- The pinned key must be updated whenever files.pharo.org rotates
  its host key. That should be a documented operational task.
- Multiple scp invocations are involved; this PR proposes a
  shared `known_hosts` file rather than duplicating the key inline.
