diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1883,6 +1883,7 @@
     'PhabricatorAuthSSHKeyListController' => 'applications/auth/controller/PhabricatorAuthSSHKeyListController.php',
     'PhabricatorAuthSSHKeyPHIDType' => 'applications/auth/phid/PhabricatorAuthSSHKeyPHIDType.php',
     'PhabricatorAuthSSHKeyQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyQuery.php',
+    'PhabricatorAuthSSHKeyReplyHandler' => 'applications/auth/mail/PhabricatorAuthSSHKeyReplyHandler.php',
     'PhabricatorAuthSSHKeySearchEngine' => 'applications/auth/query/PhabricatorAuthSSHKeySearchEngine.php',
     'PhabricatorAuthSSHKeyTableView' => 'applications/auth/view/PhabricatorAuthSSHKeyTableView.php',
     'PhabricatorAuthSSHKeyTransaction' => 'applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php',
@@ -6318,6 +6319,7 @@
     'PhabricatorAuthSSHKeyListController' => 'PhabricatorAuthSSHKeyController',
     'PhabricatorAuthSSHKeyPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorAuthSSHKeyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+    'PhabricatorAuthSSHKeyReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PhabricatorAuthSSHKeySearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorAuthSSHKeyTableView' => 'AphrontView',
     'PhabricatorAuthSSHKeyTransaction' => 'PhabricatorApplicationTransaction',
diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php
--- a/src/applications/almanac/storage/AlmanacDevice.php
+++ b/src/applications/almanac/storage/AlmanacDevice.php
@@ -227,6 +227,14 @@
     return $this->getName();
   }
 
+  public function getSSHKeyNotifyPHIDs() {
+    // Devices don't currently have anyone useful to notify about SSH key
+    // edits, and they're usually a difficult vector to attack since you need
+    // access to a cluster host. However, it would be nice to make them
+    // subscribable at some point.
+    return array();
+  }
+
 
 /* -(  PhabricatorDestructibleInterface  )----------------------------------- */
 
diff --git a/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php b/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php
--- a/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php
+++ b/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php
@@ -36,13 +36,31 @@
 
       $type = $public_key->getType();
       $body = $public_key->getBody();
+      $comment = pht('Generated');
 
-      $key
-        ->setName($default_name)
-        ->setKeyType($type)
-        ->setKeyBody($body)
-        ->setKeyComment(pht('Generated'))
-        ->save();
+      $entire_key = "{$type} {$body} {$comment}";
+
+      $type_create = PhabricatorTransactions::TYPE_CREATE;
+      $type_name = PhabricatorAuthSSHKeyTransaction::TYPE_NAME;
+      $type_key = PhabricatorAuthSSHKeyTransaction::TYPE_KEY;
+
+      $xactions = array();
+
+      $xactions[] = id(new PhabricatorAuthSSHKeyTransaction())
+        ->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
+
+      $xactions[] = id(new PhabricatorAuthSSHKeyTransaction())
+        ->setTransactionType($type_name)
+        ->setNewValue($default_name);
+
+      $xactions[] = id(new PhabricatorAuthSSHKeyTransaction())
+        ->setTransactionType($type_key)
+        ->setNewValue($entire_key);
+
+      $editor = id(new PhabricatorAuthSSHKeyEditor())
+        ->setActor($viewer)
+        ->setContentSourceFromRequest($request)
+        ->applyTransactions($key, $xactions);
 
       // NOTE: We're disabling workflow on submit so the download works. We're
       // disabling workflow on cancel so the page reloads, showing the new
diff --git a/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php b/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php
--- a/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php
+++ b/src/applications/auth/editor/PhabricatorAuthSSHKeyEditor.php
@@ -177,4 +177,68 @@
   }
 
 
+  protected function shouldSendMail(
+    PhabricatorLiskDAO $object,
+    array $xactions) {
+    return true;
+  }
+
+  protected function getMailSubjectPrefix() {
+    return pht('[SSH Key]');
+  }
+
+  protected function getMailThreadID(PhabricatorLiskDAO $object) {
+    return 'ssh-key-'.$object->getPHID();
+  }
+
+  protected function getMailTo(PhabricatorLiskDAO $object) {
+    return $object->getObject()->getSSHKeyNotifyPHIDs();
+  }
+
+  protected function getMailCC(PhabricatorLiskDAO $object) {
+    return array();
+  }
+
+  protected function buildReplyHandler(PhabricatorLiskDAO $object) {
+    return id(new PhabricatorAuthSSHKeyReplyHandler())
+      ->setMailReceiver($object);
+  }
+
+  protected function buildMailTemplate(PhabricatorLiskDAO $object) {
+    $id = $object->getID();
+    $name = $object->getName();
+    $phid = $object->getPHID();
+
+    $mail = id(new PhabricatorMetaMTAMail())
+      ->setSubject(pht('SSH Key %d: %s', $id, $name))
+      ->addHeader('Thread-Topic', $phid);
+
+    // The primary value of this mail is alerting users to account compromises,
+    // so force delivery. In particular, this mail should still be delievered
+    // even if "self mail" is disabled.
+    $mail->setForceDelivery(true);
+
+    return $mail;
+  }
+
+  protected function buildMailBody(
+    PhabricatorLiskDAO $object,
+    array $xactions) {
+
+    $body = parent::buildMailBody($object, $xactions);
+
+    $body->addLinkSection(
+      pht('SECURITY WARNING'),
+      pht(
+        'If you do not recognize this change, it may indicate your account '.
+        'has been compromised.'));
+
+    $detail_uri = $object->getURI();
+    $detail_uri = PhabricatorEnv::getProductionURI($detail_uri);
+
+    $body->addLinkSection(pht('SSH KEY DETAIL'), $detail_uri);
+
+    return $body;
+  }
+
 }
diff --git a/src/applications/auth/mail/PhabricatorAuthSSHKeyReplyHandler.php b/src/applications/auth/mail/PhabricatorAuthSSHKeyReplyHandler.php
new file mode 100644
--- /dev/null
+++ b/src/applications/auth/mail/PhabricatorAuthSSHKeyReplyHandler.php
@@ -0,0 +1,17 @@
+<?php
+
+final class PhabricatorAuthSSHKeyReplyHandler
+  extends PhabricatorApplicationTransactionReplyHandler {
+
+  public function validateMailReceiver($mail_receiver) {
+    if (!($mail_receiver instanceof PhabricatorAuthSSHKey)) {
+      throw new Exception(
+        pht('Mail receiver is not a %s!', 'PhabricatorAuthSSHKey'));
+    }
+  }
+
+  public function getObjectPrefix() {
+    return 'SSHKEY';
+  }
+
+}
diff --git a/src/applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php b/src/applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php
--- a/src/applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php
+++ b/src/applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php
@@ -17,4 +17,6 @@
    */
   public function getSSHKeyDefaultName();
 
+  public function getSSHKeyNotifyPHIDs();
+
 }
diff --git a/src/applications/auth/storage/PhabricatorAuthSSHKey.php b/src/applications/auth/storage/PhabricatorAuthSSHKey.php
--- a/src/applications/auth/storage/PhabricatorAuthSSHKey.php
+++ b/src/applications/auth/storage/PhabricatorAuthSSHKey.php
@@ -70,6 +70,12 @@
     return parent::save();
   }
 
+  public function getMailKey() {
+    // NOTE: We don't actually receive mail for these objects. It's OK for
+    // the mail key to be predictable until we do.
+    return PhabricatorHash::digestForIndex($this->getPHID());
+  }
+
   public function toPublicKey() {
     return PhabricatorAuthSSHPublicKey::newFromStoredKey($this);
   }
@@ -164,7 +170,7 @@
   }
 
   public function getApplicationTransactionTemplate() {
-    return new PhabricatorAuthProviderConfigTransaction();
+    return new PhabricatorAuthSSHKeyTransaction();
   }
 
   public function willRenderTimeline(
diff --git a/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php b/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php
--- a/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php
+++ b/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php
@@ -26,6 +26,10 @@
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
+      case PhabricatorTransactions::TYPE_CREATE:
+        return pht(
+          '%s created this key.',
+          $this->renderHandleLink($author_phid));
       case self::TYPE_NAME:
         return pht(
           '%s renamed this key from "%s" to "%s".',
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -1342,6 +1342,12 @@
     return 'id_rsa_phabricator';
   }
 
+  public function getSSHKeyNotifyPHIDs() {
+    return array(
+      $this->getPHID(),
+    );
+  }
+
 
 /* -(  PhabricatorApplicationTransactionInterface  )------------------------- */