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
@@ -80,6 +80,7 @@
     'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php',
     'PhutilAWSException' => 'future/aws/PhutilAWSException.php',
     'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php',
+    'PhutilAWSS3Client' => 'future/aws/PhutilAWSS3Client.php',
     'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php',
     'PhutilAggregateException' => 'error/PhutilAggregateException.php',
     'PhutilAmazonAuthAdapter' => 'auth/PhutilAmazonAuthAdapter.php',
diff --git a/src/future/aws/PhutilAWSFuture.php b/src/future/aws/PhutilAWSFuture.php
--- a/src/future/aws/PhutilAWSFuture.php
+++ b/src/future/aws/PhutilAWSFuture.php
@@ -5,9 +5,13 @@
   private $future;
   private $awsAccessKey;
   private $awsPrivateKey;
+  private $awsSessionToken;
+  private $awsSessionExpiry;
   private $awsRegion;
   private $builtRequest;
   private $params;
+  private $useIAM = false;
+  private $useSSL = false;
 
   abstract public function getServiceName();
 
@@ -15,41 +19,86 @@
     parent::__construct(null);
   }
 
-  public function setAWSKeys($access, $private) {
-    $this->awsAccessKey = $access;
-    $this->awsPrivateKey = $private;
-    return $this;
-  }
+  public final function getAWSAccessKey() {
+    if ($this->useIAM) {
+      $this->refreshInstanceProfileCredentials();
+    }
 
-  public function getAWSAccessKey() {
     return $this->awsAccessKey;
   }
 
-  public function getAWSPrivateKey() {
+  public final function getAWSPrivateKey() {
+    if ($this->useIAM) {
+      $this->refreshInstanceProfileCredentials();
+    }
+
     return $this->awsPrivateKey;
   }
 
-  public function getAWSRegion() {
+  public final function getAWSSessionToken() {
+    return $this->awsSessionToken;
+  }
+
+  public final function getAWSSessionExpiry() {
+    return $this->awsSessionExpiry;
+  }
+
+  public final function setAWSKeys($access, $private) {
+    $this->awsAccessKey     = $access;
+    $this->awsPrivateKey    = $private;
+    $this->awsSessionToken  = null;
+    $this->awsSessionExpiry = null;
+    return $this;
+  }
+
+  public final function getAWSRegion() {
     return $this->awsRegion;
   }
 
-  public function setAWSRegion($region) {
+  public final function setAWSRegion($region) {
     $this->awsRegion = $region;
     return $this;
   }
 
   public function getHost() {
-    $host = $this->getServiceName().'.'.$this->awsRegion.'.amazonaws.com';
-    return $host;
+    return $this->getServiceName().'.'.$this->awsRegion.'.amazonaws.com';
   }
 
-  public function setRawAWSQuery($action, array $params = array()) {
+  public function getPath() {
+    return '';
+  }
+
+  public final function setRawAWSQuery($action, array $params = array()) {
     $this->params = $params;
     $this->params['Action'] = $action;
     return $this;
   }
 
-  protected function getProxiedFuture() {
+  /**
+   * Set whether to use instance profile credentials for authentication. See
+   * http://docs.aws.amazon.com/IAM/latest/UserGuide/instance-profiles.html for
+   * more information
+   *
+   * @param  bool  True to use instance profile credentials, otherwise false.
+   * @return this
+   */
+  public final function setUseIAM($use_iam) {
+    $this->useIAM = $use_iam;
+
+    if ($this->useIAM) {
+      $this->awsAccessKey  = null;
+      $this->awsPrivateKey = null;
+    }
+
+    return $this;
+  }
+
+  public final function setUseSSL($use_ssl) {
+    $this->useSSL = $use_ssl;
+    return $this;
+  }
+
+  protected final function getProxiedFuture() {
     if (!$this->future) {
       $params = $this->params;
 
@@ -57,26 +106,36 @@
         throw new Exception('You must setRawAWSQuery()!');
       }
 
-      if (!$this->getAWSAccessKey()) {
-        throw new Exception('You must setAWSKeys()!');
+      if (!$this->getAWSAccessKey() && !$this->useIAM) {
+        throw new Exception('You must setAWSKeys() or setUseIAM(true)!');
       }
 
       $params['AWSAccessKeyId'] = $this->getAWSAccessKey();
       $params['Version']        = '2013-10-15';
       $params['Timestamp']      = date('c');
 
+      if ($this->useIAM) {
+        $this->refreshInstanceProfileCredentials();
+        $params['SecurityToken'] = $this->awsSessionToken;
+      }
+
       $params = $this->sign($params);
 
-      $uri = new PhutilURI('http://'.$this->getHost().'/');
-      $uri->setQueryParams($params);
+      $uri = id(new PhutilURI())
+        ->setDomain($this->getHost())
+        ->setQueryParams($params);
 
-      $this->future = new HTTPFuture($uri);
+      if ($this->useSSL) {
+        $this->future = new HTTPSFuture($uri->setProtocol('https'));
+      } else {
+        $this->future = new HTTPFuture($uri->setProtocol('http'));
+      }
     }
 
     return $this->future;
   }
 
-  protected function didReceiveResult($result) {
+  protected final function didReceiveResult($result) {
     list($status, $body, $headers) = $result;
 
     try {
@@ -110,7 +169,6 @@
    * http://bit.ly/wU0JFh
    */
   private function sign(array $params) {
-
     $params['SignatureMethod'] = 'HmacSHA256';
     $params['SignatureVersion'] = '2';
 
@@ -122,20 +180,74 @@
     }
     $pstr = implode('&', $pstr);
 
-    $sign = "GET"."\n".
-            strtolower($this->getHost())."\n".
-            "/"."\n".
-            $pstr;
-
-    $hash = hash_hmac(
-      'sha256',
-      $sign,
-      $this->getAWSPrivateKey(),
-      $raw_ouput = true);
+    $sign = implode("\n", array(
+      'GET',
+      strtolower($this->getHost()),
+      '/',
+      $pstr,
+    ));
 
+    $hash = hash_hmac('sha256', $sign, $this->getAWSPrivateKey(), true);
     $params['Signature'] = base64_encode($hash);
 
     return $params;
   }
 
+  /**
+   * Returns a URI which can be used to retrieve instance metadata from an AWS
+   * EC2 instance. See
+   * http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
+   * for more information.
+   *
+   * @return PhutilURI
+   */
+  protected final function getInstanceMetadataURL() {
+    return new PhutilURI('http://169.254.169.254/latest/meta-data/');
+  }
+
+  /**
+   * Refresh credentials from the instance profile.
+   *
+   * @return this
+   */
+  private function refreshInstanceProfileCredentials() {
+    if (time() > $this->awsSessionExpiry) {
+      // Instance profile credentials have expired, retrieve a new token.
+      $this->setInstanceProfileCredentials();
+    }
+
+    return $this;
+  }
+
+  /**
+   * Set credentials from the instance profile.
+   *
+   * http://bit.ly/1mLDoQ3
+   */
+  private function setInstanceProfileCredentials() {
+    $url = $this->getInstanceMetadataURL()
+      ->appendPath('/iam/security-credentials/');
+
+    // Get role.
+    $future = new HTTPFuture($url);
+    list($response, $headers) = $future->resolvex();
+    $credentials = trim($response);
+
+    // Get credentials.
+    $future = new HTTPFuture($url->appendPath($credentials));
+    list($response, $headers) = $future->resolvex();
+    $response = phutil_json_decode($response);
+
+    if ($response['Code'] !== 'Success') {
+      throw new RuntimeException(
+        pht('Unexpected response code: "%s"', $response['Code']));
+    }
+
+    $this->awsAccessKey     = $response['AccessKeyId'];
+    $this->awsPrivateKey    = $response['SecretAccessKey'];
+    $this->awsSessionToken  = $response['Token'];
+    $this->awsSessionExpiry = strtotime($response['Expiration']);
+    return $this;
+  }
+
 }
diff --git a/src/future/aws/PhutilAWSS3Client.php b/src/future/aws/PhutilAWSS3Client.php
new file mode 100644
--- /dev/null
+++ b/src/future/aws/PhutilAWSS3Client.php
@@ -0,0 +1,68 @@
+<?php
+
+final class PhutilAWSS3Client {
+
+  private $future;
+
+  public function __construct() {
+    $this->future = new PhutilAWSS3Future();
+  }
+
+  public function setAWSKeys($access, $private) {
+    $this->future->setAWSKeys($access, $private);
+  }
+
+  public function setUseIAM($use_iam) {
+    $this->future->setUseIAM($use_iam);
+  }
+
+  private function getFuture() {
+    $future = clone $this->future;
+    return $future;
+  }
+
+  public function deleteBucket($bucket) {
+    return $this->getFuture()
+      ->setBucket($bucket)
+      ->setMethod('DELETE')
+      ->resolvex();
+  }
+
+  public function deleteObject($bucket, $key) {
+    return $this->getFuture()
+      ->setBucket($bucket)
+      ->setKey($key)
+      ->setMethod('DELETE')
+      ->resolvex();
+  }
+
+  public function getObject($bucket, $key) {
+    return $this->getFuture()
+      ->setBucket($bucket)
+      ->setKey($key)
+      ->setMethod('GET')
+      ->resolvex();
+  }
+
+  public function listBucket($bucket) {
+    return $this->getFuture()
+      ->setBucket($bucket)
+      ->setMethod('GET')
+      ->resolvex();
+  }
+
+  public function putObject($data, $bucket, $key) {
+    $params = array(
+      'data' => $data,
+      'size' => strlen($data),
+      'md5sum' => base64_encode(md5($data, true)),
+    );
+
+    return $this->getFuture()
+      ->setBucket($bucket)
+      ->setKey($key)
+      ->setMethod('PUT')
+      ->resolvex();
+  }
+
+}
diff --git a/src/future/aws/PhutilAWSS3Future.php b/src/future/aws/PhutilAWSS3Future.php
--- a/src/future/aws/PhutilAWSS3Future.php
+++ b/src/future/aws/PhutilAWSS3Future.php
@@ -2,8 +2,37 @@
 
 final class PhutilAWSS3Future extends PhutilAWSFuture {
 
+  private $bucket;
+  private $key;
+
   public function getServiceName() {
     return 's3';
   }
 
+  public function setBucket($bucket) {
+    $this->bucket = $bucket;
+    return $this;
+  }
+
+  public function getBucket() {
+    return $this->bucket;
+  }
+
+  public function setKey($key) {
+    $this->key = $key;
+    return $this;
+  }
+
+  public function getKey() {
+    return $this->key;
+  }
+
+  public final function getHost() {
+    return $this->getBucket().'.'.parent::getHost();
+  }
+
+  public function getPath() {
+    return $this->key;
+  }
+
 }