summaryrefslogtreecommitdiff
path: root/src/monkeysphere-host
blob: 830646a2d0c2615b98ad86b431818f5b4adc0249 (plain)
  1. #!/usr/bin/env bash
  2. # monkeysphere-host: Monkeysphere host admin tool
  3. #
  4. # The monkeysphere scripts are written by:
  5. # Jameson Rollins <jrollins@fifthhorseman.net>
  6. # Jamie McClelland <jm@mayfirst.org>
  7. # Daniel Kahn Gillmor <dkg@fifthhorseman.net>
  8. #
  9. # They are Copyright 2008, and are all released under the GPL, version 3
  10. # or later.
  11. ########################################################################
  12. PGRM=$(basename $0)
  13. SYSSHAREDIR=${MONKEYSPHERE_SYSSHAREDIR:-"/usr/share/monkeysphere"}
  14. export SYSSHAREDIR
  15. . "${SYSSHAREDIR}/common" || exit 1
  16. SYSDATADIR=${MONKEYSPHERE_SYSDATADIR:-"/var/lib/monkeysphere/host"}
  17. export SYSDATADIR
  18. # monkeysphere temp directory, in sysdatadir to enable atomic moves of
  19. # authorized_keys files
  20. MSTMPDIR="${SYSDATADIR}/tmp"
  21. export MSTMPDIR
  22. # UTC date in ISO 8601 format if needed
  23. DATE=$(date -u '+%FT%T')
  24. # unset some environment variables that could screw things up
  25. unset GREP_OPTIONS
  26. # default return code
  27. RETURN=0
  28. ########################################################################
  29. # FUNCTIONS
  30. ########################################################################
  31. usage() {
  32. cat <<EOF >&2
  33. usage: $PGRM <subcommand> [options] [args]
  34. Monkeysphere host admin tool.
  35. subcommands:
  36. show-key (s) output all host key information
  37. extend-key (e) EXPIRE extend host key expiration
  38. add-hostname (n+) NAME[:PORT] add hostname user ID to host key
  39. revoke-hostname (n-) NAME[:PORT] revoke hostname user ID
  40. add-revoker (o) FINGERPRINT add a revoker to the host key
  41. revoke-key (r) revoke host key
  42. publish-key (p) publish server host key to keyserver
  43. expert
  44. import-key (i) import existing ssh key to gpg
  45. --hostname (-h) NAME[:PORT] hostname for key user ID
  46. --keyfile (-f) FILE key file to import
  47. --expire (-e) EXPIRE date to expire
  48. gen-key (g) generate gpg key for the host
  49. --hostname (-h) NAME[:PORT] hostname for key user ID
  50. --length (-l) BITS key length in bits (2048)
  51. --expire (-e) EXPIRE date to expire
  52. --revoker (-r) FINGERPRINT add a revoker
  53. diagnostics (d) monkeysphere host status
  54. version (v) show version number
  55. help (h,?) this help
  56. EOF
  57. }
  58. # function to run command as monkeysphere user
  59. su_monkeysphere_user() {
  60. # if the current user is the monkeysphere user, then just eval
  61. # command
  62. if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
  63. eval "$@"
  64. # otherwise su command as monkeysphere user
  65. else
  66. su "$MONKEYSPHERE_USER" -c "$@"
  67. fi
  68. }
  69. # function to interact with the host gnupg keyring
  70. gpg_host() {
  71. local returnCode
  72. GNUPGHOME="$GNUPGHOME_HOST"
  73. export GNUPGHOME
  74. # NOTE: we supress this warning because we need the monkeysphere
  75. # user to be able to read the host pubring. we realize this might
  76. # be problematic, but it's the simplest solution, without too much
  77. # loss of security.
  78. gpg --no-permission-warning "$@"
  79. returnCode="$?"
  80. # always reset the permissions on the host pubring so that the
  81. # monkeysphere user can read the trust signatures
  82. chgrp "$MONKEYSPHERE_USER" "${GNUPGHOME_HOST}/pubring.gpg"
  83. chmod g+r "${GNUPGHOME_HOST}/pubring.gpg"
  84. return "$returnCode"
  85. }
  86. # check if user is root
  87. is_root() {
  88. [ $(id -u 2>/dev/null) = '0' ]
  89. }
  90. # check that user is root, for functions that require root access
  91. check_user() {
  92. is_root || failure "You must be root to run this command."
  93. }
  94. # output just key fingerprint
  95. fingerprint_server_key() {
  96. # set the pipefail option so functions fails if can't read sec key
  97. set -o pipefail
  98. gpg_host --list-secret-keys --fingerprint \
  99. --with-colons --fixed-list-mode 2> /dev/null | \
  100. grep '^fpr:' | head -1 | cut -d: -f10 2>/dev/null
  101. }
  102. # function to check for host secret key
  103. check_host_keyring() {
  104. fingerprint_server_key >/dev/null \
  105. || failure "You don't appear to have a Monkeysphere host key on this server. Please run 'monkeysphere-server gen-key' first."
  106. }
  107. # output key information
  108. show_server_key() {
  109. local fingerprintPGP
  110. local fingerprintSSH
  111. local ret=0
  112. # FIXME: you shouldn't have to be root to see the host key fingerprint
  113. if is_root ; then
  114. check_host_keyring
  115. fingerprintPGP=$(fingerprint_server_key)
  116. gpg_authentication "--fingerprint --list-key --list-options show-unusable-uids $fingerprintPGP" 2>/dev/null
  117. echo "OpenPGP fingerprint: $fingerprintPGP"
  118. else
  119. log info "You must be root to see host OpenPGP fingerprint."
  120. ret='1'
  121. fi
  122. if [ -f "${SYSDATADIR}/ssh_host_rsa_key.pub" ] ; then
  123. fingerprintSSH=$(ssh-keygen -l -f "${SYSDATADIR}/ssh_host_rsa_key.pub" | \
  124. awk '{ print $1, $2, $4 }')
  125. echo "ssh fingerprint: $fingerprintSSH"
  126. else
  127. log info "SSH host key not found."
  128. ret='1'
  129. fi
  130. return $ret
  131. }
  132. # extend the lifetime of a host key:
  133. extend_key() {
  134. local fpr=$(fingerprint_server_key)
  135. local extendTo="$1"
  136. # get the new expiration date
  137. extendTo=$(get_gpg_expiration "$extendTo")
  138. gpg_host --quiet --command-fd 0 --edit-key "$fpr" <<EOF
  139. expire
  140. $extendTo
  141. save
  142. EOF
  143. echo
  144. echo "NOTE: Host key expiration date adjusted, but not yet published."
  145. echo "Run '$PGRM publish-key' to publish the new expiration date."
  146. }
  147. # add hostname user ID to server key
  148. add_hostname() {
  149. local userID
  150. local fingerprint
  151. local tmpuidMatch
  152. local line
  153. local adduidCommand
  154. if [ -z "$1" ] ; then
  155. failure "You must specify a hostname to add."
  156. fi
  157. userID="ssh://${1}"
  158. fingerprint=$(fingerprint_server_key)
  159. # match to only ultimately trusted user IDs
  160. tmpuidMatch="u:$(echo $userID | gpg_escape)"
  161. # find the index of the requsted user ID
  162. # NOTE: this is based on circumstantial evidence that the order of
  163. # this output is the appropriate index
  164. if line=$(gpg_host --list-keys --with-colons --fixed-list-mode "0x${fingerprint}!" \
  165. | egrep '^(uid|uat):' | cut -f2,10 -d: | grep -n -x -F "$tmpuidMatch") ; then
  166. failure "Host userID '$userID' already exists."
  167. fi
  168. echo "The following user ID will be added to the host key:"
  169. echo " $userID"
  170. read -p "Are you sure you would like to add this user ID? (y/N) " OK; OK=${OK:=N}
  171. if [ ${OK/y/Y} != 'Y' ] ; then
  172. failure "User ID not added."
  173. fi
  174. # edit-key script command to add user ID
  175. adduidCommand=$(cat <<EOF
  176. adduid
  177. $userID
  178. save
  179. EOF
  180. )
  181. # execute edit-key script
  182. if echo "$adduidCommand" | \
  183. gpg_host --quiet --command-fd 0 --edit-key "0x${fingerprint}!" ; then
  184. # update the trustdb for the authentication keyring
  185. gpg_authentication "--check-trustdb"
  186. show_server_key
  187. echo
  188. echo "NOTE: User ID added to key, but key not published."
  189. echo "Run '$PGRM publish-key' to publish the new user ID."
  190. else
  191. failure "Problem adding user ID."
  192. fi
  193. }
  194. # revoke hostname user ID to server key
  195. revoke_hostname() {
  196. local userID
  197. local fingerprint
  198. local tmpuidMatch
  199. local line
  200. local uidIndex
  201. local message
  202. local revuidCommand
  203. if [ -z "$1" ] ; then
  204. failure "You must specify a hostname to revoke."
  205. fi
  206. echo "WARNING: There is a known bug in this function."
  207. echo "This function has been known to occasionally revoke the wrong user ID."
  208. echo "Please see the following bug report for more information:"
  209. echo "http://web.monkeysphere.info/bugs/revoke-hostname-revoking-wrong-userid/"
  210. read -p "Are you sure you would like to proceed? (y/N) " OK; OK=${OK:=N}
  211. if [ ${OK/y/Y} != 'Y' ] ; then
  212. failure "aborting."
  213. fi
  214. userID="ssh://${1}"
  215. fingerprint=$(fingerprint_server_key)
  216. # match to only ultimately trusted user IDs
  217. tmpuidMatch="u:$(echo $userID | gpg_escape)"
  218. # find the index of the requsted user ID
  219. # NOTE: this is based on circumstantial evidence that the order of
  220. # this output is the appropriate index
  221. if line=$(gpg_host --list-keys --with-colons --fixed-list-mode "0x${fingerprint}!" \
  222. | egrep '^(uid|uat):' | cut -f2,10 -d: | grep -n -x -F "$tmpuidMatch") ; then
  223. uidIndex=${line%%:*}
  224. else
  225. failure "No non-revoked user ID '$userID' is found."
  226. fi
  227. echo "The following host key user ID will be revoked:"
  228. echo " $userID"
  229. read -p "Are you sure you would like to revoke this user ID? (y/N) " OK; OK=${OK:=N}
  230. if [ ${OK/y/Y} != 'Y' ] ; then
  231. failure "User ID not revoked."
  232. fi
  233. message="Hostname removed by monkeysphere-server $DATE"
  234. # edit-key script command to revoke user ID
  235. revuidCommand=$(cat <<EOF
  236. $uidIndex
  237. revuid
  238. y
  239. 4
  240. $message
  241. y
  242. save
  243. EOF
  244. )
  245. # execute edit-key script
  246. if echo "$revuidCommand" | \
  247. gpg_host --quiet --command-fd 0 --edit-key "0x${fingerprint}!" ; then
  248. # update the trustdb for the authentication keyring
  249. gpg_authentication "--check-trustdb"
  250. show_server_key
  251. echo
  252. echo "NOTE: User ID revoked, but revocation not published."
  253. echo "Run '$PGRM publish-key' to publish the revocation."
  254. else
  255. failure "Problem revoking user ID."
  256. fi
  257. }
  258. # add a revoker to the host key
  259. add_revoker() {
  260. # FIXME: implement!
  261. failure "not implemented yet!"
  262. }
  263. # revoke the host key
  264. revoke_key() {
  265. # FIXME: implement!
  266. failure "not implemented yet!"
  267. }
  268. # publish server key to keyserver
  269. publish_server_key() {
  270. read -p "Really publish host key to $KEYSERVER? (y/N) " OK; OK=${OK:=N}
  271. if [ ${OK/y/Y} != 'Y' ] ; then
  272. failure "key not published."
  273. fi
  274. # find the key fingerprint
  275. fingerprint=$(fingerprint_server_key)
  276. # publish host key
  277. gpg_authentication "--keyserver $KEYSERVER --send-keys '0x${fingerprint}!'"
  278. }
  279. diagnostics() {
  280. # * check on the status and validity of the key and public certificates
  281. local seckey
  282. local keysfound
  283. local curdate
  284. local warnwindow
  285. local warndate
  286. local create
  287. local expire
  288. local uid
  289. local fingerprint
  290. local badhostkeys
  291. local sshd_config
  292. local problemsfound=0
  293. # FIXME: what's the correct, cross-platform answer?
  294. sshd_config=/etc/ssh/sshd_config
  295. seckey=$(gpg_host --list-secret-keys --fingerprint --with-colons --fixed-list-mode)
  296. keysfound=$(echo "$seckey" | grep -c ^sec:)
  297. curdate=$(date +%s)
  298. # warn when anything is 2 months away from expiration
  299. warnwindow='2 months'
  300. warndate=$(advance_date $warnwindow +%s)
  301. if ! id monkeysphere >/dev/null ; then
  302. echo "! No monkeysphere user found! Please create a monkeysphere system user with bash as its shell."
  303. problemsfound=$(($problemsfound+1))
  304. fi
  305. if ! [ -d "$SYSDATADIR" ] ; then
  306. echo "! no $SYSDATADIR directory found. Please create it."
  307. problemsfound=$(($problemsfound+1))
  308. fi
  309. echo "Checking host GPG key..."
  310. if (( "$keysfound" < 1 )); then
  311. echo "! No host key found."
  312. echo " - Recommendation: run 'monkeysphere-server gen-key'"
  313. problemsfound=$(($problemsfound+1))
  314. elif (( "$keysfound" > 1 )); then
  315. echo "! More than one host key found?"
  316. # FIXME: recommend a way to resolve this
  317. problemsfound=$(($problemsfound+1))
  318. else
  319. create=$(echo "$seckey" | grep ^sec: | cut -f6 -d:)
  320. expire=$(echo "$seckey" | grep ^sec: | cut -f7 -d:)
  321. fingerprint=$(echo "$seckey" | grep ^fpr: | head -n1 | cut -f10 -d:)
  322. # check for key expiration:
  323. if [ "$expire" ]; then
  324. if (( "$expire" < "$curdate" )); then
  325. echo "! Host key is expired."
  326. echo " - Recommendation: extend lifetime of key with 'monkeysphere-server extend-key'"
  327. problemsfound=$(($problemsfound+1))
  328. elif (( "$expire" < "$warndate" )); then
  329. echo "! Host key expires in less than $warnwindow:" $(advance_date $(( $expire - $curdate )) seconds +%F)
  330. echo " - Recommendation: extend lifetime of key with 'monkeysphere-server extend-key'"
  331. problemsfound=$(($problemsfound+1))
  332. fi
  333. fi
  334. # and weirdnesses:
  335. if [ "$create" ] && (( "$create" > "$curdate" )); then
  336. echo "! Host key was created in the future(?!). Is your clock correct?"
  337. echo " - Recommendation: Check clock ($(date +%F_%T)); use NTP?"
  338. problemsfound=$(($problemsfound+1))
  339. fi
  340. # check for UserID expiration:
  341. echo "$seckey" | grep ^uid: | cut -d: -f6,7,10 | \
  342. while IFS=: read create expire uid ; do
  343. # FIXME: should we be doing any checking on the form
  344. # of the User ID? Should we be unmangling it somehow?
  345. if [ "$create" ] && (( "$create" > "$curdate" )); then
  346. echo "! User ID '$uid' was created in the future(?!). Is your clock correct?"
  347. echo " - Recommendation: Check clock ($(date +%F_%T)); use NTP?"
  348. problemsfound=$(($problemsfound+1))
  349. fi
  350. if [ "$expire" ] ; then
  351. if (( "$expire" < "$curdate" )); then
  352. echo "! User ID '$uid' is expired."
  353. # FIXME: recommend a way to resolve this
  354. problemsfound=$(($problemsfound+1))
  355. elif (( "$expire" < "$warndate" )); then
  356. echo "! User ID '$uid' expires in less than $warnwindow:" $(advance_date $(( $expire - $curdate )) seconds +%F)
  357. # FIXME: recommend a way to resolve this
  358. problemsfound=$(($problemsfound+1))
  359. fi
  360. fi
  361. done
  362. # FIXME: verify that the host key is properly published to the
  363. # keyservers (do this with the non-privileged user)
  364. # FIXME: check that there are valid, non-expired certifying signatures
  365. # attached to the host key after fetching from the public keyserver
  366. # (do this with the non-privileged user as well)
  367. # FIXME: propose adding a revoker to the host key if none exist (do we
  368. # have a way to do that after key generation?)
  369. # Ensure that the ssh_host_rsa_key file is present and non-empty:
  370. echo
  371. echo "Checking host SSH key..."
  372. if [ ! -s "${SYSDATADIR}/ssh_host_rsa_key" ] ; then
  373. echo "! The host key as prepared for SSH (${SYSDATADIR}/ssh_host_rsa_key) is missing or empty."
  374. problemsfound=$(($problemsfound+1))
  375. else
  376. if [ $(ls -l "${SYSDATADIR}/ssh_host_rsa_key" | cut -f1 -d\ ) != '-rw-------' ] ; then
  377. echo "! Permissions seem wrong for ${SYSDATADIR}/ssh_host_rsa_key -- should be 0600."
  378. problemsfound=$(($problemsfound+1))
  379. fi
  380. # propose changes needed for sshd_config (if any)
  381. if ! grep -q "^HostKey[[:space:]]\+${SYSDATADIR}/ssh_host_rsa_key$" "$sshd_config"; then
  382. echo "! $sshd_config does not point to the monkeysphere host key (${SYSDATADIR}/ssh_host_rsa_key)."
  383. echo " - Recommendation: add a line to $sshd_config: 'HostKey ${SYSDATADIR}/ssh_host_rsa_key'"
  384. problemsfound=$(($problemsfound+1))
  385. fi
  386. if badhostkeys=$(grep -i '^HostKey' "$sshd_config" | grep -v "^HostKey[[:space:]]\+${SYSDATADIR}/ssh_host_rsa_key$") ; then
  387. echo "! $sshd_config refers to some non-monkeysphere host keys:"
  388. echo "$badhostkeys"
  389. echo " - Recommendation: remove the above HostKey lines from $sshd_config"
  390. problemsfound=$(($problemsfound+1))
  391. fi
  392. # FIXME: test (with ssh-keyscan?) that the running ssh
  393. # daemon is actually offering the monkeysphere host key.
  394. fi
  395. fi
  396. # FIXME: look at the ownership/privileges of the various keyrings,
  397. # directories housing them, etc (what should those values be? can
  398. # we make them as minimal as possible?)
  399. # FIXME: look to see that the ownertrust rules are set properly on the
  400. # authentication keyring
  401. # FIXME: make sure that at least one identity certifier exists
  402. # FIXME: look at the timestamps on the monkeysphere-generated
  403. # authorized_keys files -- warn if they seem out-of-date.
  404. # FIXME: check for a cronjob that updates monkeysphere-generated
  405. # authorized_keys?
  406. echo
  407. echo "Checking for MonkeySphere-enabled public-key authentication for users ..."
  408. # Ensure that User ID authentication is enabled:
  409. if ! grep -q "^AuthorizedKeysFile[[:space:]]\+${SYSDATADIR}/authorized_keys/%u$" "$sshd_config"; then
  410. echo "! $sshd_config does not point to monkeysphere authorized keys."
  411. echo " - Recommendation: add a line to $sshd_config: 'AuthorizedKeysFile ${SYSDATADIR}/authorized_keys/%u'"
  412. problemsfound=$(($problemsfound+1))
  413. fi
  414. if badauthorizedkeys=$(grep -i '^AuthorizedKeysFile' "$sshd_config" | grep -v "^AuthorizedKeysFile[[:space:]]\+${SYSDATADIR}/authorized_keys/%u$") ; then
  415. echo "! $sshd_config refers to non-monkeysphere authorized_keys files:"
  416. echo "$badauthorizedkeys"
  417. echo " - Recommendation: remove the above AuthorizedKeysFile lines from $sshd_config"
  418. problemsfound=$(($problemsfound+1))
  419. fi
  420. if [ "$problemsfound" -gt 0 ]; then
  421. echo "When the above $problemsfound issue"$(if [ "$problemsfound" -eq 1 ] ; then echo " is" ; else echo "s are" ; fi)" resolved, please re-run:"
  422. echo " monkeysphere-server diagnostics"
  423. else
  424. echo "Everything seems to be in order!"
  425. fi
  426. }
  427. ########################################################################
  428. # MAIN
  429. ########################################################################
  430. # unset variables that should be defined only in config file
  431. unset KEYSERVER
  432. unset AUTHORIZED_USER_IDS
  433. unset RAW_AUTHORIZED_KEYS
  434. unset MONKEYSPHERE_USER
  435. # load configuration file
  436. [ -e ${MONKEYSPHERE_SERVER_CONFIG:="${SYSCONFIGDIR}/monkeysphere-server.conf"} ] && . "$MONKEYSPHERE_SERVER_CONFIG"
  437. # set empty config variable with ones from the environment, or with
  438. # defaults
  439. LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
  440. KEYSERVER=${MONKEYSPHERE_KEYSERVER:=${KEYSERVER:="pool.sks-keyservers.net"}}
  441. AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:=${AUTHORIZED_USER_IDS:="%h/.monkeysphere/authorized_user_ids"}}
  442. RAW_AUTHORIZED_KEYS=${MONKEYSPHERE_RAW_AUTHORIZED_KEYS:=${RAW_AUTHORIZED_KEYS:="%h/.ssh/authorized_keys"}}
  443. MONKEYSPHERE_USER=${MONKEYSPHERE_MONKEYSPHERE_USER:=${MONKEYSPHERE_USER:="monkeysphere"}}
  444. # other variables
  445. CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:="true"}
  446. REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
  447. GNUPGHOME_HOST=${MONKEYSPHERE_GNUPGHOME_HOST:="${SYSDATADIR}/gnupg-host"}
  448. GNUPGHOME_AUTHENTICATION=${MONKEYSPHERE_GNUPGHOME_AUTHENTICATION:="${SYSDATADIR}/gnupg-authentication"}
  449. # export variables needed in su invocation
  450. export DATE
  451. export MODE
  452. export MONKEYSPHERE_USER
  453. export LOG_LEVEL
  454. export KEYSERVER
  455. export CHECK_KEYSERVER
  456. export REQUIRED_USER_KEY_CAPABILITY
  457. export GNUPGHOME_HOST
  458. export GNUPGHOME_AUTHENTICATION
  459. export GNUPGHOME
  460. # get subcommand
  461. COMMAND="$1"
  462. [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
  463. shift
  464. case $COMMAND in
  465. 'show-key'|'show'|'s')
  466. show_server_key
  467. ;;
  468. 'extend-key'|'e')
  469. check_user
  470. check_host_keyring
  471. extend_key "$@"
  472. ;;
  473. 'add-hostname'|'add-name'|'n+')
  474. check_user
  475. check_host_keyring
  476. add_hostname "$@"
  477. ;;
  478. 'revoke-hostname'|'revoke-name'|'n-')
  479. check_user
  480. check_host_keyring
  481. revoke_hostname "$@"
  482. ;;
  483. 'add-revoker'|'o')
  484. check_user
  485. check_host_keyring
  486. add_revoker "$@"
  487. ;;
  488. 'revoke-key'|'r')
  489. check_user
  490. check_host_keyring
  491. revoke_key "$@"
  492. ;;
  493. 'publish-key'|'publish'|'p')
  494. check_user
  495. check_host_keyring
  496. publish_server_key
  497. ;;
  498. 'expert'|'e')
  499. check_user
  500. SUBCOMMAND="$1"
  501. shift
  502. case "$SUBCOMMAND" in
  503. 'import-key'|'i')
  504. import_key "$@"
  505. ;;
  506. 'gen-key'|'g')
  507. gen_key "$@"
  508. ;;
  509. 'diagnostics'|'d')
  510. diagnostics
  511. ;;
  512. *)
  513. failure "Unknown expert subcommand: '$COMMAND'
  514. Type '$PGRM help' for usage."
  515. ;;
  516. esac
  517. ;;
  518. 'version'|'v')
  519. echo "$VERSION"
  520. ;;
  521. '--help'|'help'|'-h'|'h'|'?')
  522. usage
  523. ;;
  524. *)
  525. failure "Unknown command: '$COMMAND'
  526. Type '$PGRM help' for usage."
  527. ;;
  528. esac
  529. exit "$RETURN"