Skip to content

SRVOCF-1078: Reduce deploy ServiceAccount token lifetime to a configurable 30 days - #174

Open
dsimansk wants to merge 1 commit into
openshift:masterfrom
dsimansk:SRVOCF-1078-reduce-lifetime-of-deploy-serviceaccount
Open

SRVOCF-1078: Reduce deploy ServiceAccount token lifetime to a configurable 30 days#174
dsimansk wants to merge 1 commit into
openshift:masterfrom
dsimansk:SRVOCF-1078-reduce-lifetime-of-deploy-serviceaccount

Conversation

@dsimansk

@dsimansk dsimansk commented Sep 1, 2026

Copy link
Copy Markdown

Summary

  • Reduce the deploy ServiceAccount token default lifetime from 365 days
    (1 year) to 30 days, shrinking the exposure window for the credential
    stored in the repo's GitHub Actions KUBECONFIG secret.
  • Make the lifetime configurable via a new SA_TOKEN_EXPIRY env var,
    accepting common duration notation (30d, 10h, 7d12h). Empty falls back
    to the 30 day default. Exposed as the plugin.saTokenExpiry Helm value
    and wired into the deployment container env.
  • Thread the configured expiry through cluster.New, Handlers, and
    RequestToken; add unit tests for the parser and for the configured
    expiry reaching the TokenRequest.

Fixes SRVOCF-1078

Checklist

  • Updated docs/ARCHITECTURE.md (if there are relevant changes to our layered architecture)

Additional Info

The token is still a one-shot credential with no in-product refresh, so a
function's CI stops deploying once the token expires. A user-triggered
re-issue path and keyless rotation (GitHub OIDC federation or a GitOps
pull model) are tracked as follow-ups on the Jira issue; they are out of
scope here.

The deploy ServiceAccount token was minted with a 365 day (1 year)
lifetime and stored in a GitHub Actions secret. A credential that
long-lived is unnecessary exposure if the secret leaks, since it far
outlasts the interval between deployments.

Reduce the default lifetime to 30 days and make it configurable via
the SA_TOKEN_EXPIRY env var, accepting common duration notation such
as 30d, 10h, or 7d12h. The value is wired through a Helm value
(plugin.saTokenExpiry) into the deployment's container env.

The token is still a one-shot credential with no in-product refresh,
so a function's CI stops deploying once the token expires. A
user-triggered re-issue path and keyless rotation are tracked as
follow-ups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@openshift-merge-bot

Copy link
Copy Markdown

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: LGTM mode

@openshift-ci-robot openshift-ci-robot added the jira/valid-reference Indicates that this PR references a valid Jira ticket of any type. label Sep 1, 2026
@openshift-ci-robot

openshift-ci-robot commented Sep 1, 2026

Copy link
Copy Markdown

@dsimansk: This pull request references SRVOCF-1078 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the sub-task to target the "5.1.0" version, but no target version was set.

Details

In response to this:

Summary

  • Reduce the deploy ServiceAccount token default lifetime from 365 days
    (1 year) to 30 days, shrinking the exposure window for the credential
    stored in the repo's GitHub Actions KUBECONFIG secret.
  • Make the lifetime configurable via a new SA_TOKEN_EXPIRY env var,
    accepting common duration notation (30d, 10h, 7d12h). Empty falls back
    to the 30 day default. Exposed as the plugin.saTokenExpiry Helm value
    and wired into the deployment container env.
  • Thread the configured expiry through cluster.New, Handlers, and
    RequestToken; add unit tests for the parser and for the configured
    expiry reaching the TokenRequest.

Fixes SRVOCF-1078

Checklist

  • Updated docs/ARCHITECTURE.md (if there are relevant changes to our layered architecture)

Additional Info

The token is still a one-shot credential with no in-product refresh, so a
function's CI stops deploying once the token expires. A user-triggered
re-issue path and keyless rotation (GitHub OIDC federation or a GitOps
pull model) are tracked as follow-ups on the Jira issue; they are out of
scope here.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci
openshift-ci Bot requested review from Cragsmann and pmeida September 1, 2026 11:32
@openshift-ci

openshift-ci Bot commented Sep 1, 2026

Copy link
Copy Markdown

@dsimansk: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@twoGiants twoGiants self-assigned this Sep 3, 2026
@twoGiants
twoGiants self-requested a review September 3, 2026 09:02

@twoGiants twoGiants left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, looks good!

Adding the SA token expiration as a configuration via env vars is a good call.

I'd remove the tokenExpiration from the client struct -> it doesn't belong there, it's an option for RequestToken not a config parameter of the k8 client.

On Tuesday I talked with Finn about it -> we will have an Admin dashboard at some point from where such things like token expiration will be configured.

Comment thread backend/cluster/client.go
// override is configured. Kept short to limit exposure of the token stored in
// an SCM Actions secret if leaked. Override via the SA_TOKEN_EXPIRY env var
// (a duration such as 30d, 10h, or 7d12h); see ParseTokenExpiry.
const DefaultTokenExpiry int64 = 30 * 24 * 60 * 60 // 30 days

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not be exported. Pls set to private.

Can you rename it to DefaultSATokenExpiry and add the SA letters to all the variables and parameters for the token? Thx!

Comment thread backend/cluster/client.go
// seconds. The value is a duration in common notation, e.g. 30d, 10h, or
// 7d12h. The 'd' (days) unit extends Go's standard duration units (h, m, s).
// An empty value yields DefaultTokenExpiry.
func ParseTokenExpiry(s string) (int64, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add SA to the name.

Suggested change
func ParseTokenExpiry(s string) (int64, error) {
func ParseSATokenExpiry(s string) (int64, error) {

Comment thread backend/cluster/client.go
type k8sClient struct {
clientset kubernetes.Interface
clientset kubernetes.Interface
tokenExpiry int64 // requested SA token lifetime in seconds

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the name is pretty clear, even more so with SA -> saTokenExpiry, so no comment is needed.

Comment thread backend/cluster/client.go
// When host is non-empty (dev/test) it is used as the API server URL directly.
// When host is empty the standard in-cluster config is used (pod env vars + SA files).
func New(host, token string, caCert []byte) (Client, error) {
func New(host, token string, caCert []byte, tokenExpiry int64) (Client, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One could think here that tokenExpiry is for the token which is also passed but it's not.

Can you rename:

  • token -> restToken
  • tokenExpiry -> saTokenExpiry (as proposed above)

Comment thread backend/cluster/client.go

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Token expiry is a parameter of RequestToken, one of the cluster client's operations. It doesn't belong on the client struct itself.

Move saTokenExpiry out of k8sClient and into the method signatures:

// cluster/client.go
RequestToken(ctx context.Context, namespace string, saTokenExpiry int64) (string, error)

// cluster/kubeconfig.go
func GenerateKubeconfig(ctx context.Context, client Client, namespace, externalAPIServerURL string, caCert []byte, saTokenExpiry int64) (string, error)

cluster.New stays as it was (no saTokenExpiry parameter), k8sClient doesn't store it:

func New(host, token string, caCert []byte) (Client, error)

The env var should be DEFAULT_SA_TOKEN_EXPIRY, parsed at startup and stored on the Handlers struct:

// main.go
saTokenExpiry, err := cluster.ParseSATokenExpiry(os.Getenv("DEFAULT_SA_TOKEN_EXPIRY"))

// handler/handler.go
type Handlers struct {
	// ...existing fields...
	defaultSATokenExpiry int64 // fallback SA token lifetime; only used when the create request omits saTokenExpiry
}

The handler resolves the effective expiry and passes it through:

func (h *Handlers) resolveSATokenExpiry(requested string) (int64, error) {
	if requested == "" {
		return h.defaultSATokenExpiry, nil
	}
	return cluster.ParseSATokenExpiry(requested)
}

// in createFunction:
cl, err := newClusterClient(h.kubeHost, ocpToken, h.caCert)
// ...
kubeconfig, err := cluster.GenerateKubeconfig(ctx, cl, req.Namespace, h.externalAPIServerURL, h.caCert, h.defaultSATokenExpiry)

Helm value rename accordingly (plugin.defaultSaTokenExpiry) with a comment that it's the fallback when the create request doesn't provide one.

At some point when we will probably have an admin dashboard and we will read the saTokenExpiry in the backend from some config which we created from the admin dashboard.

})
cl := &k8sClient{clientset: cs, tokenExpiry: 7 * 24 * 60 * 60}

_, err := cl.RequestToken(context.Background(), "default")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test will need an update.

@openshift-ci

openshift-ci Bot commented Sep 3, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please ask for approval from twogiants. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

jira/valid-reference Indicates that this PR references a valid Jira ticket of any type.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants