Files
coder/coderd/workspaceresourceauth.go
Kyle Carberry a502a5fa14 feat: Add AWS instance identity authentication (#570)
* feat: Add AWS instance identity authentication

This allows zero-trust authentication for all AWS instances.

Prior to this, AWS instances could be used by passing `CODER_TOKEN`
as an environment variable to the startup script. AWS explicitly
states that secrets should not be passed in startup scripts because
it's user-readable.

* Fix sha256 verbosity

* Fix HTTP client being exposed on auth
2022-03-28 19:31:03 +00:00

142 lines
4.7 KiB
Go

package coderd
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/go-chi/render"
"github.com/coder/coder/coderd/awsidentity"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/codersdk"
"github.com/mitchellh/mapstructure"
)
// AWS supports instance identity verification:
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
// Using this, we can exchange a signed instance payload for an agent token.
func (api *api) postWorkspaceAuthAWSInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
var req codersdk.AWSInstanceIdentityToken
if !httpapi.Read(rw, r, &req) {
return
}
identity, err := awsidentity.Validate(req.Signature, req.Document, api.AWSCertificates)
if err != nil {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: fmt.Sprintf("validate: %s", err),
})
return
}
api.handleAuthInstanceID(rw, r, identity.InstanceID)
}
// Google Compute Engine supports instance identity verification:
// https://cloud.google.com/compute/docs/instances/verifying-instance-identity
// Using this, we can exchange a signed instance payload for an agent token.
func (api *api) postWorkspaceAuthGoogleInstanceIdentity(rw http.ResponseWriter, r *http.Request) {
var req codersdk.GoogleInstanceIdentityToken
if !httpapi.Read(rw, r, &req) {
return
}
// We leave the audience blank. It's not important we validate who made the token.
payload, err := api.GoogleTokenValidator.Validate(r.Context(), req.JSONWebToken, "")
if err != nil {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: fmt.Sprintf("validate: %s", err),
})
return
}
claims := struct {
Google struct {
ComputeEngine struct {
InstanceID string `mapstructure:"instance_id"`
} `mapstructure:"compute_engine"`
} `mapstructure:"google"`
}{}
err = mapstructure.Decode(payload.Claims, &claims)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("decode jwt claims: %s", err),
})
return
}
api.handleAuthInstanceID(rw, r, claims.Google.ComputeEngine.InstanceID)
}
func (api *api) handleAuthInstanceID(rw http.ResponseWriter, r *http.Request, instanceID string) {
agent, err := api.Database.GetWorkspaceAgentByInstanceID(r.Context(), instanceID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
Message: fmt.Sprintf("instance with id %q not found", instanceID),
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job agent: %s", err),
})
return
}
resource, err := api.Database.GetWorkspaceResourceByID(r.Context(), agent.ResourceID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job resource: %s", err),
})
return
}
job, err := api.Database.GetProvisionerJobByID(r.Context(), resource.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
if job.Type != database.ProvisionerJobTypeWorkspaceBuild {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("%q jobs cannot be authenticated", job.Type),
})
return
}
var jobData workspaceProvisionJob
err = json.Unmarshal(job.Input, &jobData)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("extract job data: %s", err),
})
return
}
resourceHistory, err := api.Database.GetWorkspaceBuildByID(r.Context(), jobData.WorkspaceBuildID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace build: %s", err),
})
return
}
// This token should only be exchanged if the instance ID is valid
// for the latest history. If an instance ID is recycled by a cloud,
// we'd hate to leak access to a user's workspace.
latestHistory, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), resourceHistory.WorkspaceID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get latest workspace build: %s", err),
})
return
}
if latestHistory.ID.String() != resourceHistory.ID.String() {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("resource found for id %q, but isn't registered on the latest history", instanceID),
})
return
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, codersdk.WorkspaceAgentAuthenticateResponse{
SessionToken: agent.AuthToken.String(),
})
}