Skip to content

Commit

Permalink
AGENT-950: Implement Separate JWT Tokens for Different User Personas
Browse files Browse the repository at this point in the history
- Create 3 seperate JWT tokens- AGENT_AUTH_TOKEN, USER_AUTH_TOKEN, WATCHER_AUTH_TOKEN
- Update the claim to set 'sub' to identify the user persona
- Pass different headers in the curl requests depending on the user persona, for example, Watcher-Authorization header for watcher user persona
- User generic name to specify auth token expiry
  • Loading branch information
pawanpinjarkar committed Sep 19, 2024
1 parent 10951c5 commit 41f0f0c
Show file tree
Hide file tree
Showing 16 changed files with 90 additions and 54 deletions.
9 changes: 5 additions & 4 deletions data/data/agent/files/usr/local/bin/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@
curl_assisted_service() {
local endpoint=$1
local method=${2:-GET}
local additional_options=("${@:3}") # Capture all arguments starting from the third one
local authz=${3:-}
local additional_options=("${@:4}") # Capture all arguments starting from the fourth one
local baseURL="${SERVICE_BASE_URL}api/assisted-install/v2"

case "${method}" in
"POST")
curl -s -S -X POST "${additional_options[@]}" "${baseURL}${endpoint}" \
-H "Authorization: ${AGENT_AUTH_TOKEN}" \
-H "Authorization: ${authz}" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
;;
"GET")
curl -s -S -X GET "${additional_options[@]}" "${baseURL}${endpoint}" \
-H "Authorization: ${AGENT_AUTH_TOKEN}" \
-H "Authorization: ${authz}" \
-H "Accept: application/json"
;;
esac
}
}
2 changes: 1 addition & 1 deletion data/data/agent/files/usr/local/bin/start-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ INFRA_ENV_ID=""
until [[ $INFRA_ENV_ID != "" && $INFRA_ENV_ID != "null" ]]; do
sleep 5
>&2 echo "Querying assisted-service for infra-env-id..."
INFRA_ENV_ID=$(curl_assisted_service "/infra-envs" GET | jq -r .[0].id)
INFRA_ENV_ID=$(curl_assisted_service "/infra-envs" GET $USER_AUTH_TOKEN | jq -r '.[0].id')
done
echo "Fetched infra-env-id and found: $INFRA_ENV_ID"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ cluster_id=""
while [[ "${cluster_id}" = "" ]]
do
# Get cluster id
cluster_id=$(curl_assisted_service "/clusters" GET | jq -r .[].id)
cluster_id=$(curl_assisted_service "/clusters" GET $USER_AUTH_TOKEN | jq -r .[].id)
if [[ "${cluster_id}" = "" ]]; then
sleep 2
fi
Expand All @@ -28,7 +28,7 @@ status_issue="90_start-install"
num_known_hosts() {
local known_hosts=0
local insufficient_hosts=0
host_status=$(curl_assisted_service "/infra-envs/${INFRA_ENV_ID}/hosts" GET | jq -r .[].status)
host_status=$(curl_assisted_service "/infra-envs/${INFRA_ENV_ID}/hosts" GET $USER_AUTH_TOKEN | jq -r .[].status)
if [[ -n ${host_status} ]]; then
for status in ${host_status}; do
if [[ "${status}" == "known" ]]; then
Expand Down Expand Up @@ -58,15 +58,15 @@ clear_issue "${status_issue}"
while [[ "${cluster_status}" != "installed" ]]
do
sleep 5
cluster_info="$(curl_assisted_service "/clusters" GET)"
cluster_info="$(curl_assisted_service "/clusters" GET $USER_AUTH_TOKEN)"
cluster_status=$(printf '%s' "${cluster_info}" | jq -r .[].status)
echo "Cluster status: ${cluster_status}" 1>&2
# Start the cluster install, if it transitions back to Ready due to a failure,
# then it will be restarted
case "${cluster_status}" in
"ready")
echo "Starting cluster installation..." 1>&2
res=$(curl_assisted_service "/clusters/${cluster_id}/actions/install" POST -w "%{http_code}" -o /dev/null)
res=$(curl_assisted_service "/clusters/${cluster_id}/actions/install" POST $USER_AUTH_TOKEN -w "%{http_code}" -o /dev/null)
if [[ $res = "202" ]]; then
printf '\nCluster installation started\n' 1>&2
fi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ source "common.sh"

echo "Waiting for assisted-service to be ready"

until curl_assisted_service "/infra-envs" GET -o /dev/null --silent --fail; do
until curl_assisted_service "/infra-envs" GET $USER_AUTH_TOKEN -o /dev/null --silent --fail; do
printf '.'
sleep 5
done
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ OPENSHIFT_INSTALL_RELEASE_IMAGE_MIRROR={{.ReleaseImageMirror}}
STORAGE=filesystem
INFRA_ENV_ID={{.InfraEnvID}}
EC_PUBLIC_KEY_PEM={{.PublicKeyPEM}}
AGENT_AUTH_TOKEN={{.Token}}
AGENT_AUTH_TOKEN={{.AgentAuthToken}}
USER_AUTH_TOKEN={{.UserAuthToken}}
WATCHER_AUTH_TOKEN={{.WatcherAuthToken}}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ EnvironmentFile=/usr/local/share/assisted-service/assisted-service.env
EnvironmentFile=/etc/assisted/add-nodes.env
ExecStartPre=/bin/rm -f %t/%n.ctr-id
ExecStartPre=/usr/local/bin/wait-for-assisted-service.sh
ExecStart=podman run --net host --cidfile=%t/%n.ctr-id --cgroups=no-conmon --log-driver=journald --rm --pod-id-file=%t/assisted-service-pod.pod-id --replace --name=agent-import-cluster -v /etc/assisted/clusterconfig:/clusterconfig -v /etc/assisted/manifests:/manifests -v /etc/assisted/extra-manifests:/extra-manifests {{ if .HaveMirrorConfig }}-v /etc/containers:/etc/containers{{ end }} {{.CaBundleMount}} --env SERVICE_BASE_URL --env OPENSHIFT_INSTALL_RELEASE_IMAGE_MIRROR --env CLUSTER_ID --env CLUSTER_NAME --env CLUSTER_API_VIP_DNS_NAME --env AGENT_AUTH_TOKEN $SERVICE_IMAGE /usr/local/bin/agent-installer-client importCluster
ExecStart=podman run --net host --cidfile=%t/%n.ctr-id --cgroups=no-conmon --log-driver=journald --rm --pod-id-file=%t/assisted-service-pod.pod-id --replace --name=agent-import-cluster -v /etc/assisted/clusterconfig:/clusterconfig -v /etc/assisted/manifests:/manifests -v /etc/assisted/extra-manifests:/extra-manifests {{ if .HaveMirrorConfig }}-v /etc/containers:/etc/containers{{ end }} {{.CaBundleMount}} --env SERVICE_BASE_URL --env OPENSHIFT_INSTALL_RELEASE_IMAGE_MIRROR --env CLUSTER_ID --env CLUSTER_NAME --env CLUSTER_API_VIP_DNS_NAME --env USER_AUTH_TOKEN $SERVICE_IMAGE /usr/local/bin/agent-installer-client importCluster
ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ EnvironmentFile=/usr/local/share/assisted-service/agent-images.env
EnvironmentFile=/usr/local/share/assisted-service/assisted-service.env
ExecStartPre=/bin/rm -f %t/%n.ctr-id
ExecStartPre=/usr/local/bin/wait-for-assisted-service.sh
ExecStart=podman run --net host --cidfile=%t/%n.ctr-id --cgroups=no-conmon --log-driver=journald --rm --pod-id-file=%t/assisted-service-pod.pod-id --replace --name=agent-register-cluster -v /etc/assisted/manifests:/manifests -v /etc/assisted/extra-manifests:/extra-manifests {{ if .HaveMirrorConfig }}-v /etc/containers:/etc/containers{{ end }} {{.CaBundleMount}} --env SERVICE_BASE_URL --env OPENSHIFT_INSTALL_RELEASE_IMAGE_MIRROR --env AGENT_AUTH_TOKEN $SERVICE_IMAGE /usr/local/bin/agent-installer-client registerCluster
ExecStart=podman run --net host --cidfile=%t/%n.ctr-id --cgroups=no-conmon --log-driver=journald --rm --pod-id-file=%t/assisted-service-pod.pod-id --replace --name=agent-register-cluster -v /etc/assisted/manifests:/manifests -v /etc/assisted/extra-manifests:/extra-manifests {{ if .HaveMirrorConfig }}-v /etc/containers:/etc/containers{{ end }} {{.CaBundleMount}} --env SERVICE_BASE_URL --env OPENSHIFT_INSTALL_RELEASE_IMAGE_MIRROR --env USER_AUTH_TOKEN $SERVICE_IMAGE /usr/local/bin/agent-installer-client registerCluster
ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ EnvironmentFile=/etc/assisted/rendezvous-host.env
EnvironmentFile=/usr/local/share/assisted-service/agent-images.env
EnvironmentFile=/usr/local/share/assisted-service/assisted-service.env
ExecStartPre=/bin/rm -f %t/%n.ctr-id
ExecStart=podman run --net host --cidfile=%t/%n.ctr-id --cgroups=no-conmon --log-driver=journald --rm --pod-id-file=%t/assisted-service-pod.pod-id --replace --name=agent-register-infraenv -v /etc/assisted/manifests:/manifests --env SERVICE_BASE_URL --env IMAGE_TYPE_ISO --env AGENT_AUTH_TOKEN $SERVICE_IMAGE /usr/local/bin/agent-installer-client registerInfraEnv
ExecStart=podman run --net host --cidfile=%t/%n.ctr-id --cgroups=no-conmon --log-driver=journald --rm --pod-id-file=%t/assisted-service-pod.pod-id --replace --name=agent-register-infraenv -v /etc/assisted/manifests:/manifests --env SERVICE_BASE_URL --env IMAGE_TYPE_ISO --env USER_AUTH_TOKEN $SERVICE_IMAGE /usr/local/bin/agent-installer-client registerInfraEnv
ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id

Expand Down
2 changes: 1 addition & 1 deletion data/data/agent/systemd/units/apply-host-config.service
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ EnvironmentFile=/usr/local/share/assisted-service/assisted-service.env
ExecStartPre=/bin/rm -f %t/%n.ctr-id
ExecStartPre=/bin/mkdir -p %t/agent-installer /etc/assisted/hostconfig
ExecStartPre=/usr/local/bin/wait-for-assisted-service.sh
ExecStart=podman run --net host --cidfile=%t/%n.ctr-id --cgroups=no-conmon --log-driver=journald --restart=on-failure:10 --pod-id-file=%t/assisted-service-pod.pod-id --replace --name=apply-host-config -v /etc/assisted/hostconfig:/etc/assisted/hostconfig -v %t/agent-installer:/var/run/agent-installer:z --env SERVICE_BASE_URL --env INFRA_ENV_ID --env WORKFLOW_TYPE --env AGENT_AUTH_TOKEN $SERVICE_IMAGE /usr/local/bin/agent-installer-client configure
ExecStart=podman run --net host --cidfile=%t/%n.ctr-id --cgroups=no-conmon --log-driver=journald --restart=on-failure:10 --pod-id-file=%t/assisted-service-pod.pod-id --replace --name=apply-host-config -v /etc/assisted/hostconfig:/etc/assisted/hostconfig -v %t/agent-installer:/var/run/agent-installer:z --env SERVICE_BASE_URL --env INFRA_ENV_ID --env WORKFLOW_TYPE --env USER_AUTH_TOKEN $SERVICE_IMAGE /usr/local/bin/agent-installer-client configure
ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id

Expand Down
8 changes: 4 additions & 4 deletions pkg/agent/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,25 @@ func NewCluster(ctx context.Context, assetDir, rendezvousIP, kubeconfigPath, ssh
czero := &Cluster{}
capi := &clientSet{}

var authToken string
var watcherAuthToken string
var err error

switch workflowType {
case workflow.AgentWorkflowTypeInstall:
authToken, err = FindAuthTokenFromAssetStore(assetDir)
watcherAuthToken, err = FindAuthTokenFromAssetStore(assetDir)
if err != nil {
return nil, err
}
case workflow.AgentWorkflowTypeAddNodes:
authToken, err = gencrypto.GetAuthTokenFromCluster(ctx, kubeconfigPath)
watcherAuthToken, err = gencrypto.GetAuthTokenFromCluster(ctx, kubeconfigPath)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("AgentWorkflowType value not supported: %s", workflowType)
}

restclient := NewNodeZeroRestClient(ctx, rendezvousIP, sshKey, authToken)
restclient := NewNodeZeroRestClient(ctx, rendezvousIP, sshKey, watcherAuthToken)

kubeclient, err := NewClusterKubeAPIClient(ctx, kubeconfigPath)
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions pkg/agent/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type NodeZeroRestClient struct {
}

// NewNodeZeroRestClient Initialize a new rest client to interact with the Agent Rest API on node zero.
func NewNodeZeroRestClient(ctx context.Context, rendezvousIP, sshKey, token string) *NodeZeroRestClient {
func NewNodeZeroRestClient(ctx context.Context, rendezvousIP, sshKey, watcherAuthToken string) *NodeZeroRestClient {
restClient := &NodeZeroRestClient{}

// Get SSH Keys which can be used to determine if Rest API failures are due to network connectivity issues
Expand All @@ -48,7 +48,7 @@ func NewNodeZeroRestClient(ctx context.Context, rendezvousIP, sshKey, token stri
Path: client.DefaultBasePath,
}

config.AuthInfo = gencrypto.UserAuthHeaderWriter(token)
config.AuthInfo = gencrypto.WatcherAuthHeaderWriter(watcherAuthToken) // this is used only by wait-for so set watcher auth alone

client := client.New(config)

Expand Down Expand Up @@ -137,7 +137,7 @@ func FindAuthTokenFromAssetStore(assetDir string) (string, error) {

var authToken string
if authConfig != nil {
authToken = authConfig.(*gencrypto.AuthConfig).AgentAuthToken
authToken = authConfig.(*gencrypto.AuthConfig).WatcherAuthToken
}

return authToken, nil
Expand Down
6 changes: 3 additions & 3 deletions pkg/asset/agent/gencrypto/auth_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
"github.com/pkg/errors"
)

// UserAuthHeaderWriter sets the JWT authorization token.
func UserAuthHeaderWriter(token string) runtime.ClientAuthInfoWriter {
// WatcherAuthHeaderWriter sets the JWT authorization token.
func WatcherAuthHeaderWriter(token string) runtime.ClientAuthInfoWriter {
return runtime.ClientAuthInfoWriterFunc(func(r runtime.ClientRequest, _ strfmt.Registry) error {
return r.SetHeaderParam("Authorization", token)
return r.SetHeaderParam("Watcher-Authorization", token)
})
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/asset/agent/gencrypto/auth_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ func TestParseExpirationFromToken(t *testing.T) {
assert.NotEmpty(t, privateKey)
assert.NoError(t, err)

tokenNoExp, err := generateToken(privateKey, nil)
tokenNoExp, err := generateToken("userAuth", privateKey, nil)
assert.NotEmpty(t, tokenNoExp)
assert.NoError(t, err)

expiry := time.Now().UTC().Add(30 * time.Second)
tokenWithExp, err := generateToken(privateKey, &expiry)
tokenWithExp, err := generateToken("userAuth", privateKey, &expiry)
assert.NotEmpty(t, tokenWithExp)
assert.NoError(t, err)

Expand Down
59 changes: 43 additions & 16 deletions pkg/asset/agent/gencrypto/authconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const AuthType = "agent-installer-local"

// AuthConfig is an asset that generates ECDSA public/private keys, JWT token.
type AuthConfig struct {
PublicKey, AgentAuthToken, AgentAuthTokenExpiry, AuthType string
PublicKey, AgentAuthToken, UserAuthToken, WatcherAuthToken, AuthTokenExpiry, AuthType string
}

var _ asset.Asset = (*AuthConfig)(nil)
Expand Down Expand Up @@ -69,23 +69,49 @@ func (a *AuthConfig) Generate(_ context.Context, dependencies asset.Parents) err
switch agentWorkflow.Workflow {
case workflow.AgentWorkflowTypeInstall:
// Auth tokens do not expire
token, err := generateToken(privateKey, nil)
agentAuthToken, err := generateToken("agentAuth", privateKey, nil)
if err != nil {
return err
}
a.AgentAuthToken = token
a.AgentAuthToken = agentAuthToken

userAuthToken, err := generateToken("userAuth", privateKey, nil)
if err != nil {
return err
}
a.UserAuthToken = userAuthToken

watcherAuthToken, err := generateToken("watcherAuth", privateKey, nil)
if err != nil {
return err
}
a.WatcherAuthToken = watcherAuthToken

case workflow.AgentWorkflowTypeAddNodes:
addNodesConfig := &joiner.AddNodesConfig{}
dependencies.Get(addNodesConfig)

// Auth tokens expires after 48 hours
expiry := time.Now().UTC().Add(48 * time.Hour)
a.AgentAuthTokenExpiry = expiry.Format(time.RFC3339)
token, err := generateToken(privateKey, &expiry)
a.AuthTokenExpiry = expiry.Format(time.RFC3339)

agentAuthToken, err := generateToken("agentAuth", privateKey, &expiry)
if err != nil {
return err
}
a.AgentAuthToken = token
a.AgentAuthToken = agentAuthToken

userAuthToken, err := generateToken("userAuth", privateKey, &expiry)
if err != nil {
return err
}
a.UserAuthToken = userAuthToken

watcherAuthToken, err := generateToken("watcherAuth", privateKey, &expiry)
if err != nil {
return err
}
a.WatcherAuthToken = watcherAuthToken

err = a.createOrUpdateAuthTokenSecret(addNodesConfig.Params.Kubeconfig)
if err != nil {
Expand Down Expand Up @@ -147,10 +173,11 @@ func keyPairPEM() (string, string, error) {
}

// generateToken returns a JWT token based on the private key.
func generateToken(privateKkeyPem string, expiry *time.Time) (string, error) {
func generateToken(userPersona string, privateKeyPem string, expiry *time.Time) (string, error) {
// Create the JWT claims
claims := jwt.MapClaims{}

claims := jwt.MapClaims{
"sub": userPersona,
}
// Set the expiry time if provided
if expiry != nil {
claims["exp"] = expiry.Unix()
Expand All @@ -159,7 +186,7 @@ func generateToken(privateKkeyPem string, expiry *time.Time) (string, error) {
// Create the token using the ES256 signing method and the claims
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)

priv, err := jwt.ParseECPrivateKeyFromPEM([]byte(privateKkeyPem))
priv, err := jwt.ParseECPrivateKeyFromPEM([]byte(privateKeyPem))
if err != nil {
return "", err
}
Expand Down Expand Up @@ -229,15 +256,15 @@ func (a *AuthConfig) createOrUpdateAuthTokenSecret(kubeconfigPath string) error
// Update the token in asset store with the retrieved token from the cluster
a.AgentAuthToken = retrievedToken
// get the token expiry time of the retrieved token from the cluster
a.AgentAuthTokenExpiry = expiryTime.UTC().Format(time.RFC3339)
a.AuthTokenExpiry = expiryTime.UTC().Format(time.RFC3339)

retrievedPublicKey, err := extractPublicKeyFromSecret(retrievedSecret)
if err != nil {
return err
}
// Update the asset store with the retrieved public key associated with the valid token from the cluster
a.PublicKey = retrievedPublicKey
logrus.Infof("Reusing existing auth token (valid up to %s)", a.AgentAuthTokenExpiry)
logrus.Infof("Reusing existing auth token (valid up to %s)", a.AuthTokenExpiry)
}
return err
}
Expand All @@ -250,7 +277,7 @@ func (a *AuthConfig) createSecret(k8sclientset kubernetes.Interface) error {
// only for informational purposes
Annotations: map[string]string{
"updatedAt": "", // Initially set to empty
"expiresAt": a.AgentAuthTokenExpiry,
"expiresAt": a.AuthTokenExpiry,
},
},
Type: corev1.SecretTypeOpaque,
Expand All @@ -263,7 +290,7 @@ func (a *AuthConfig) createSecret(k8sclientset kubernetes.Interface) error {
if err != nil {
return fmt.Errorf("failed to create secret: %w", err)
}
logrus.Infof("Generated auth token (valid up to %s)", a.AgentAuthTokenExpiry)
logrus.Infof("Generated auth token (valid up to %s)", a.AuthTokenExpiry)
logrus.Infof("Created secret %s/%s", authTokenSecretNamespace, authTokenSecretName)

return nil
Expand All @@ -274,13 +301,13 @@ func (a *AuthConfig) refreshAuthTokenSecret(k8sclientset kubernetes.Interface, r
retrievedSecret.Data[authTokenPublicDataKey] = []byte(a.PublicKey)
// only for informational purposes
retrievedSecret.Annotations["updatedAt"] = time.Now().UTC().Format(time.RFC3339)
retrievedSecret.Annotations["expiresAt"] = a.AgentAuthTokenExpiry
retrievedSecret.Annotations["expiresAt"] = a.AuthTokenExpiry

_, err := k8sclientset.CoreV1().Secrets(authTokenSecretNamespace).Update(context.TODO(), retrievedSecret, metav1.UpdateOptions{})
if err != nil {
return err
}
logrus.Infof("Auth token regenerated (valid up to %s)", a.AgentAuthTokenExpiry)
logrus.Infof("Auth token regenerated (valid up to %s)", a.AuthTokenExpiry)
logrus.Infof("Updated secret %s/%s", authTokenSecretNamespace, authTokenSecretName)
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/asset/agent/image/agentimage.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (a *AgentImage) Generate(ctx context.Context, dependencies asset.Parents) e

a.platform = clusterInfo.PlatformType
a.isoFilename = agentAddNodesISOFilename
a.imageExpiresAt = authConfig.AgentAuthTokenExpiry
a.imageExpiresAt = authConfig.AuthTokenExpiry

default:
return fmt.Errorf("AgentWorkflowType value not supported: %s", agentWorkflow.Workflow)
Expand Down
Loading

0 comments on commit 41f0f0c

Please sign in to comment.