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
@@ -634,6 +634,7 @@
     'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php',
     'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php',
     'DiffusionGitLFSAuthenticateWorkflow' => 'applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php',
+    'DiffusionGitLFSResponse' => 'applications/diffusion/response/DiffusionGitLFSResponse.php',
     'DiffusionGitLFSTemporaryTokenType' => 'applications/diffusion/gitlfs/DiffusionGitLFSTemporaryTokenType.php',
     'DiffusionGitRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php',
     'DiffusionGitReceivePackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php',
@@ -4763,6 +4764,7 @@
     'DiffusionGitBranchTestCase' => 'PhabricatorTestCase',
     'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery',
     'DiffusionGitLFSAuthenticateWorkflow' => 'DiffusionGitSSHWorkflow',
+    'DiffusionGitLFSResponse' => 'AphrontResponse',
     'DiffusionGitLFSTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
     'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery',
     'DiffusionGitReceivePackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php
--- a/src/applications/diffusion/controller/DiffusionServeController.php
+++ b/src/applications/diffusion/controller/DiffusionServeController.php
@@ -5,6 +5,9 @@
   private $serviceViewer;
   private $serviceRepository;
 
+  private $isGitLFSRequest;
+  private $gitLFSToken;
+
   public function setServiceViewer(PhabricatorUser $viewer) {
     $this->serviceViewer = $viewer;
     return $this;
@@ -23,6 +26,14 @@
     return $this->serviceRepository;
   }
 
+  public function getIsGitLFSRequest() {
+    return $this->isGitLFSRequest;
+  }
+
+  public function getGitLFSToken() {
+    return $this->gitLFSToken;
+  }
+
   public function isVCSRequest(AphrontRequest $request) {
     $identifier = $this->getRepositoryIdentifierFromRequest($request);
     if ($identifier === null) {
@@ -32,6 +43,9 @@
     $content_type = $request->getHTTPHeader('Content-Type');
     $user_agent = idx($_SERVER, 'HTTP_USER_AGENT');
 
+    // This may have a "charset" suffix, so only match the prefix.
+    $lfs_pattern = '(^application/vnd\\.git-lfs\\+json(;|\z))';
+
     $vcs = null;
     if ($request->getExists('service')) {
       $service = $request->getStr('service');
@@ -46,6 +60,10 @@
     } else if ($content_type == 'application/x-git-receive-pack-request') {
       // We get this for `git-receive-pack`.
       $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
+    } else if (preg_match($lfs_pattern, $content_type)) {
+      // This is a Git LFS HTTP API request.
+      $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
+      $this->isGitLFSRequest = true;
     } else if ($request->getExists('cmd')) {
       // Mercurial also sends an Accept header like
       // "application/mercurial-0.1", and a User-Agent like
@@ -142,7 +160,17 @@
       $username = $_SERVER['PHP_AUTH_USER'];
       $password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']);
 
-      $viewer = $this->authenticateHTTPRepositoryUser($username, $password);
+      // Try Git LFS auth first since we can usually reject it without doing
+      // any queries, since the username won't match the one we expect or the
+      // request won't be LFS.
+      $viewer = $this->authenticateGitLFSUser($username, $password);
+
+      // If that failed, try normal auth. Note that we can use normal auth on
+      // LFS requests, so this isn't strictly an alternative to LFS auth.
+      if (!$viewer) {
+        $viewer = $this->authenticateHTTPRepositoryUser($username, $password);
+      }
+
       if (!$viewer) {
         return new PhabricatorVCSResponse(
           403,
@@ -212,46 +240,57 @@
 
     $is_push = !$this->isReadOnlyRequest($repository);
 
-    switch ($repository->getServeOverHTTP()) {
-      case PhabricatorRepository::SERVE_READONLY:
-        if ($is_push) {
+    if ($this->getIsGitLFSRequest() && $this->getGitLFSToken()) {
+      // We allow git LFS requests over HTTP even if the repository does not
+      // otherwise support HTTP reads or writes, as long as the user is using a
+      // token from SSH. If they're using HTTP username + password auth, they
+      // have to obey the normal HTTP rules.
+    } else {
+      switch ($repository->getServeOverHTTP()) {
+        case PhabricatorRepository::SERVE_READONLY:
+          if ($is_push) {
+            return new PhabricatorVCSResponse(
+              403,
+              pht('This repository is read-only over HTTP.'));
+          }
+          break;
+        case PhabricatorRepository::SERVE_READWRITE:
+          // We'll check for push capability below.
+          break;
+        case PhabricatorRepository::SERVE_OFF:
+        default:
           return new PhabricatorVCSResponse(
             403,
-            pht('This repository is read-only over HTTP.'));
-        }
-        break;
-      case PhabricatorRepository::SERVE_READWRITE:
-        if ($is_push) {
-          $can_push = PhabricatorPolicyFilter::hasCapability(
-            $viewer,
-            $repository,
-            DiffusionPushCapability::CAPABILITY);
-          if (!$can_push) {
-            if ($viewer->isLoggedIn()) {
-              return new PhabricatorVCSResponse(
-                403,
-                pht('You do not have permission to push to this repository.'));
-            } else {
-              if ($allow_auth) {
-                return new PhabricatorVCSResponse(
-                  401,
-                  pht('You must log in to push to this repository.'));
-              } else {
-                return new PhabricatorVCSResponse(
-                  403,
-                  pht(
-                    'Pushing to this repository requires authentication, '.
-                    'which is forbidden over HTTP.'));
-              }
-            }
+            pht('This repository is not available over HTTP.'));
+      }
+    }
+
+    if ($is_push) {
+      $can_push = PhabricatorPolicyFilter::hasCapability(
+        $viewer,
+        $repository,
+        DiffusionPushCapability::CAPABILITY);
+      if (!$can_push) {
+        if ($viewer->isLoggedIn()) {
+          return new PhabricatorVCSResponse(
+            403,
+            pht(
+              'You do not have permission to push to this '.
+              'repository.'));
+        } else {
+          if ($allow_auth) {
+            return new PhabricatorVCSResponse(
+              401,
+              pht('You must log in to push to this repository.'));
+          } else {
+            return new PhabricatorVCSResponse(
+              403,
+              pht(
+                'Pushing to this repository requires authentication, '.
+                'which is forbidden over HTTP.'));
           }
         }
-        break;
-      case PhabricatorRepository::SERVE_OFF:
-      default:
-        return new PhabricatorVCSResponse(
-          403,
-          pht('This repository is not available over HTTP.'));
+      }
     }
 
     $vcs_type = $repository->getVersionControlSystem();
@@ -324,6 +363,14 @@
     PhabricatorRepository $repository,
     PhabricatorUser $viewer) {
 
+    // We can serve Git LFS requests first, since we don't need to proxy them.
+    // It's also important that LFS requests never fall through to standard
+    // service pathways, because that would let you use LFS tokens to read
+    // normal repository data.
+    if ($this->getIsGitLFSRequest()) {
+      return $this->serveGitLFSRequest($repository, $viewer);
+    }
+
     // If this repository is hosted on a service, we need to proxy the request
     // to a host which can serve it.
     $is_cluster_request = $this->getRequest()->isProxiedClusterRequest();
@@ -363,6 +410,8 @@
 
     // TODO: This implementation is safe by default, but very incomplete.
 
+    // TODO: This doesn't get the right result for Git LFS yet.
+
     switch ($repository->getVersionControlSystem()) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         $service = $request->getStr('service');
@@ -514,6 +563,52 @@
     return $base_path;
   }
 
+  private function authenticateGitLFSUser(
+    $username,
+    PhutilOpaqueEnvelope $password) {
+
+    // Never accept these credentials for requests which aren't LFS requests.
+    if (!$this->getIsGitLFSRequest()) {
+      return null;
+    }
+
+    // If we have the wrong username, don't bother checking if the token
+    // is right.
+    if ($username !== DiffusionGitLFSTemporaryTokenType::HTTP_USERNAME) {
+      return null;
+    }
+
+    $lfs_pass = $password->openEnvelope();
+    $lfs_hash = PhabricatorHash::digest($lfs_pass);
+
+    $token = id(new PhabricatorAuthTemporaryTokenQuery())
+      ->setViewer(PhabricatorUser::getOmnipotentUser())
+      ->withTokenTypes(array(DiffusionGitLFSTemporaryTokenType::TOKENTYPE))
+      ->withTokenCodes(array($lfs_hash))
+      ->withExpired(false)
+      ->executeOne();
+    if (!$token) {
+      return null;
+    }
+
+    $user = id(new PhabricatorPeopleQuery())
+      ->setViewer(PhabricatorUser::getOmnipotentUser())
+      ->withPHIDs(array($token->getUserPHID()))
+      ->executeOne();
+
+    if (!$user) {
+      return null;
+    }
+
+    if (!$user->isUserActivated()) {
+      return null;
+    }
+
+    $this->gitLFSToken = $token;
+
+    return $user;
+  }
+
   private function authenticateHTTPRepositoryUser(
     $username,
     PhutilOpaqueEnvelope $password) {
@@ -739,4 +834,32 @@
     );
   }
 
+  private function serveGitLFSRequest(
+    PhabricatorRepository $repository,
+    PhabricatorUser $viewer) {
+
+    if (!$this->getIsGitLFSRequest()) {
+      throw new Exception(pht('This is not a Git LFS request!'));
+    }
+
+    $path = $this->getGitLFSRequestPath($repository);
+
+    return DiffusionGitLFSResponse::newErrorResponse(
+      404,
+      pht(
+        'Git LFS operation "%s" is not supported by this server.',
+        $path));
+  }
+
+  private function getGitLFSRequestPath(PhabricatorRepository $repository) {
+    $request_path = $this->getRequestDirectoryPath($repository);
+
+    $matches = null;
+    if (preg_match('(^/info/lfs(?:\z|/)(.*))', $request_path, $matches)) {
+      return $matches[1];
+    }
+
+    return null;
+  }
+
 }
diff --git a/src/applications/diffusion/response/DiffusionGitLFSResponse.php b/src/applications/diffusion/response/DiffusionGitLFSResponse.php
new file mode 100644
--- /dev/null
+++ b/src/applications/diffusion/response/DiffusionGitLFSResponse.php
@@ -0,0 +1,37 @@
+<?php
+
+final class DiffusionGitLFSResponse extends AphrontResponse {
+
+  private $content;
+
+  public static function newErrorResponse($code, $message) {
+
+    // We can optionally include "request_id" and "documentation_url" in
+    // this response.
+
+    return id(new self())
+      ->setHTTPResponseCode($code)
+      ->setContent(
+        array(
+          'message' => $message,
+        ));
+  }
+
+  public function setContent(array $content) {
+    $this->content = phutil_json_encode($content);
+    return $this;
+  }
+
+  public function buildResponseString() {
+    return $this->content;
+  }
+
+  public function getHeaders() {
+    $headers = array(
+      array('Content-Type', 'application/vnd.git-lfs+json'),
+    );
+
+    return array_merge(parent::getHeaders(), $headers);
+  }
+
+}