diff --git a/examples/k8s/thirdpartyresources.yaml b/examples/k8s/thirdpartyresources.yaml
index 65e0225649f805850ab812a05364c969e922219d..40f03027259236120fa1b854c4b5d9b24d7fbe31 100644
--- a/examples/k8s/thirdpartyresources.yaml
+++ b/examples/k8s/thirdpartyresources.yaml
@@ -46,3 +46,12 @@ kind: ThirdPartyResource
 description: "Refresh tokens for clients to continuously act on behalf of an end user."
 versions:
 - name: v1
+---
+
+metadata:
+  name: password.passwords.oidc.coreos.com
+apiVersion: extensions/v1beta1
+kind: ThirdPartyResource
+description: "Passwords managed by the OIDC server."
+versions:
+- name: v1
diff --git a/storage/kubernetes/client.go b/storage/kubernetes/client.go
index f6f97f17d2ae6c5137c0371d3f9338951e8b2139..ec703214b3d22180251796f5fd989e1c66bff067 100644
--- a/storage/kubernetes/client.go
+++ b/storage/kubernetes/client.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"crypto/tls"
 	"crypto/x509"
+	"encoding/base64"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -165,6 +166,26 @@ func (c *client) delete(resource, name string) error {
 	return checkHTTPErr(resp, http.StatusOK)
 }
 
+func (c *client) deleteAll(resource string) error {
+	var list struct {
+		k8sapi.TypeMeta `json:",inline"`
+		k8sapi.ListMeta `json:"metadata,omitempty"`
+		Items           []struct {
+			k8sapi.TypeMeta   `json:",inline"`
+			k8sapi.ObjectMeta `json:"metadata,omitempty"`
+		} `json:"items"`
+	}
+	if err := c.list(resource, &list); err != nil {
+		return err
+	}
+	for _, item := range list.Items {
+		if err := c.delete(resource, item.Name); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func (c *client) put(resource, name string, v interface{}) error {
 	body, err := json.Marshal(v)
 	if err != nil {
@@ -190,9 +211,9 @@ func (c *client) put(resource, name string, v interface{}) error {
 
 func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string) (*client, error) {
 	tlsConfig := cryptopasta.DefaultTLSConfig()
-	data := func(b []byte, file string) ([]byte, error) {
-		if b != nil {
-			return b, nil
+	data := func(b string, file string) ([]byte, error) {
+		if b != "" {
+			return base64.StdEncoding.DecodeString(b)
 		}
 		if file == "" {
 			return nil, nil
diff --git a/storage/kubernetes/k8sapi/client.go b/storage/kubernetes/k8sapi/client.go
index c8df73416ecdff113c0aca0cb8f1728c66137ed5..d84fa5ccee3bab76a9e5c7bf2bd152a8dcec70e4 100644
--- a/storage/kubernetes/k8sapi/client.go
+++ b/storage/kubernetes/k8sapi/client.go
@@ -62,7 +62,9 @@ type Cluster struct {
 	// CertificateAuthority is the path to a cert file for the certificate authority.
 	CertificateAuthority string `yaml:"certificate-authority,omitempty"`
 	// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
-	CertificateAuthorityData []byte `yaml:"certificate-authority-data,omitempty"`
+	//
+	// NOTE(ericchiang): Our yaml parser doesn't assume []byte is a base64 encoded string.
+	CertificateAuthorityData string `yaml:"certificate-authority-data,omitempty"`
 	// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
 	Extensions []NamedExtension `yaml:"extensions,omitempty"`
 }
@@ -72,11 +74,15 @@ type AuthInfo struct {
 	// ClientCertificate is the path to a client cert file for TLS.
 	ClientCertificate string `yaml:"client-certificate,omitempty"`
 	// ClientCertificateData contains PEM-encoded data from a client cert file for TLS. Overrides ClientCertificate
-	ClientCertificateData []byte `yaml:"client-certificate-data,omitempty"`
+	//
+	// NOTE(ericchiang): Our yaml parser doesn't assume []byte is a base64 encoded string.
+	ClientCertificateData string `yaml:"client-certificate-data,omitempty"`
 	// ClientKey is the path to a client key file for TLS.
 	ClientKey string `yaml:"client-key,omitempty"`
 	// ClientKeyData contains PEM-encoded data from a client key file for TLS. Overrides ClientKey
-	ClientKeyData []byte `yaml:"client-key-data,omitempty"`
+	//
+	// NOTE(ericchiang): Our yaml parser doesn't assume []byte is a base64 encoded string.
+	ClientKeyData string `yaml:"client-key-data,omitempty"`
 	// Token is the bearer token for authentication to the kubernetes cluster.
 	Token string `yaml:"token,omitempty"`
 	// Impersonate is the username to imperonate.  The name matches the flag.
diff --git a/storage/kubernetes/storage.go b/storage/kubernetes/storage.go
index bfb488556635c0ffd43afeb25134eb1e23ca84d0..44920f6b261b03f66f9e66e8c531e4cf98da7a6a 100644
--- a/storage/kubernetes/storage.go
+++ b/storage/kubernetes/storage.go
@@ -20,6 +20,7 @@ const (
 	kindClient       = "OAuth2Client"
 	kindRefreshToken = "RefreshToken"
 	kindKeys         = "SigningKey"
+	kindPassword     = "Password"
 )
 
 const (
@@ -28,6 +29,7 @@ const (
 	resourceClient       = "oauth2clients"
 	resourceRefreshToken = "refreshtokens"
 	resourceKeys         = "signingkeies" // Kubernetes attempts to pluralize.
+	resourcePassword     = "passwords"
 )
 
 // Config values for the Kubernetes storage type.
@@ -109,6 +111,10 @@ func (cli *client) CreateAuthCode(c storage.AuthCode) error {
 	return cli.post(resourceAuthCode, cli.fromStorageAuthCode(c))
 }
 
+func (cli *client) CreatePassword(p storage.Password) error {
+	return cli.post(resourcePassword, cli.fromStoragePassword(p))
+}
+
 func (cli *client) CreateRefresh(r storage.RefreshToken) error {
 	refresh := RefreshToken{
 		TypeMeta: k8sapi.TypeMeta{
@@ -152,6 +158,14 @@ func (cli *client) GetClient(id string) (storage.Client, error) {
 	return toStorageClient(c), nil
 }
 
+func (cli *client) GetPassword(email string) (storage.Password, error) {
+	var p Password
+	if err := cli.get(resourcePassword, emailToID(email), &p); err != nil {
+		return storage.Password{}, err
+	}
+	return toStoragePassword(p), nil
+}
+
 func (cli *client) GetKeys() (storage.Keys, error) {
 	var keys Keys
 	if err := cli.get(resourceKeys, keysName, &keys); err != nil {
@@ -199,6 +213,10 @@ func (cli *client) DeleteRefresh(id string) error {
 	return cli.delete(resourceRefreshToken, id)
 }
 
+func (cli *client) DeletePassword(email string) error {
+	return cli.delete(resourcePassword, emailToID(email))
+}
+
 func (cli *client) UpdateClient(id string, updater func(old storage.Client) (storage.Client, error)) error {
 	var c Client
 	if err := cli.get(resourceClient, id, &c); err != nil {
@@ -214,6 +232,23 @@ func (cli *client) UpdateClient(id string, updater func(old storage.Client) (sto
 	return cli.put(resourceClient, id, newClient)
 }
 
+func (cli *client) UpdatePassword(email string, updater func(old storage.Password) (storage.Password, error)) error {
+	id := emailToID(email)
+	var p Password
+	if err := cli.get(resourcePassword, id, &p); err != nil {
+		return err
+	}
+
+	updated, err := updater(toStoragePassword(p))
+	if err != nil {
+		return err
+	}
+
+	newPassword := cli.fromStoragePassword(updated)
+	newPassword.ObjectMeta = p.ObjectMeta
+	return cli.put(resourcePassword, id, newPassword)
+}
+
 func (cli *client) UpdateKeys(updater func(old storage.Keys) (storage.Keys, error)) error {
 	firstUpdate := false
 	var keys Keys
diff --git a/storage/kubernetes/storage_test.go b/storage/kubernetes/storage_test.go
index c0011f39cc4958756a402ad53715ccb68bf0b04d..f41b01b1d0b90c409047f38d598f921f2fcb62bc 100644
--- a/storage/kubernetes/storage_test.go
+++ b/storage/kubernetes/storage_test.go
@@ -75,7 +75,18 @@ func TestURLFor(t *testing.T) {
 func TestStorage(t *testing.T) {
 	client := loadClient(t)
 	conformance.RunTestSuite(t, func() storage.Storage {
-		// TODO(erichiang): Tear down namespaces between each iteration.
+		for _, resource := range []string{
+			resourceAuthCode,
+			resourceAuthRequest,
+			resourceClient,
+			resourceRefreshToken,
+			resourceKeys,
+			resourcePassword,
+		} {
+			if err := client.deleteAll(resource); err != nil {
+				t.Fatalf("delete all %q failed: %v", resource, err)
+			}
+		}
 		return client
 	})
 }
diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go
index 8bc934f2ebdf090869e2b2f804cbb8bf62ea49f6..3c914e844a4ce1c6c8ade9c0aa6be743f0c6b8e1 100644
--- a/storage/kubernetes/types.go
+++ b/storage/kubernetes/types.go
@@ -1,6 +1,8 @@
 package kubernetes
 
 import (
+	"encoding/base32"
+	"strings"
 	"time"
 
 	jose "gopkg.in/square/go-jose.v2"
@@ -182,6 +184,60 @@ func (cli *client) fromStorageAuthRequest(a storage.AuthRequest) AuthRequest {
 	return req
 }
 
+// Password is a mirrored struct from the stroage with JSON struct tags and
+// Kubernetes type metadata.
+type Password struct {
+	k8sapi.TypeMeta   `json:",inline"`
+	k8sapi.ObjectMeta `json:"metadata,omitempty"`
+
+	// The Kubernetes name is actually an encoded version of this value.
+	//
+	// This field is IMMUTABLE. Do not change.
+	Email string `json:"email,omitempty"`
+
+	Hash     []byte `json:"hash,omitempty"`
+	Username string `json:"username,omitempty"`
+	UserID   string `json:"userID,omitempty"`
+}
+
+// Kubernetes only allows lower case letters for names.
+//
+// NOTE(ericchiang): This is currently copied from the storage package's NewID()
+// method. Once we refactor those into the storage, just use that instead.
+var encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567")
+
+// Map an arbitrary email to a valid Kuberntes name.
+func emailToID(email string) string {
+	return strings.TrimRight(encoding.EncodeToString([]byte(strings.ToLower(email))), "=")
+}
+
+func (cli *client) fromStoragePassword(p storage.Password) Password {
+	email := strings.ToLower(p.Email)
+	return Password{
+		TypeMeta: k8sapi.TypeMeta{
+			Kind:       kindPassword,
+			APIVersion: cli.apiVersionForResource(resourcePassword),
+		},
+		ObjectMeta: k8sapi.ObjectMeta{
+			Name:      emailToID(email),
+			Namespace: cli.namespace,
+		},
+		Email:    email,
+		Hash:     p.Hash,
+		Username: p.Username,
+		UserID:   p.UserID,
+	}
+}
+
+func toStoragePassword(p Password) storage.Password {
+	return storage.Password{
+		Email:    p.Email,
+		Hash:     p.Hash,
+		Username: p.Username,
+		UserID:   p.UserID,
+	}
+}
+
 // AuthCode is a mirrored struct from storage with JSON struct tags and
 // Kubernetes type metadata.
 type AuthCode struct {