vxcontrol/pentagi · critical

Token.CreationDisabled

Token.CreationDisabled

Error message

token creation is disabled with default salt

What it means

TokenService.CreateToken refuses to mint API tokens when the server's global salt is empty or still the default value 'salt', because HMAC/token signing with a default salt would let anyone forge tokens. This is a deliberate security kill-switch: token creation stays disabled until a real salt is configured, and the endpoint returns Token.CreationDisabled (403).

Source

Thrown at backend/pkg/server/services/api_tokens.go:61

		ss:         ss,
	}
}

// CreateToken creates a new API token
// @Summary Create new API token for automation
// @Tags Tokens
// @Accept json
// @Produce json
// @Param json body models.CreateAPITokenRequest true "Token creation request"
// @Success 201 {object} response.successResp{data=models.APITokenWithSecret} "token created successful"
// @Failure 400 {object} response.errorResp "invalid token request or default salt"
// @Failure 403 {object} response.errorResp "creating token not permitted"
// @Failure 500 {object} response.errorResp "internal error on creating token"
// @Router /tokens [post]
func (s *TokenService) CreateToken(c *gin.Context) {
	if s.globalSalt == "" || s.globalSalt == "salt" {
		logger.FromContext(c).Errorf("token creation attempted with default salt")
		response.Error(c, response.ErrTokenCreationDisabled, errors.New("token creation is disabled with default salt"))
		return
	}

	uid := c.GetUint64("uid")
	rid := c.GetUint64("rid")
	uhash := c.GetString("uhash")

	var req models.CreateAPITokenRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		logger.FromContext(c).WithError(err).Errorf("error binding JSON")
		response.Error(c, response.ErrTokenInvalidRequest, err)
		return
	}
	if err := req.Valid(); err != nil {
		logger.FromContext(c).WithError(err).Errorf("error validating JSON")
		response.Error(c, response.ErrTokenInvalidRequest, err)
		return
	}

View on GitHub (pinned to ea665308ba)

Solutions

  1. Set a strong, unique global salt in the environment (e.g. SALT=<random 32+ byte secret>) and restart the backend.
  2. Replace the default 'salt' value in .env / docker-compose.yml with a generated secret (openssl rand -hex 32).
  3. Confirm the salt is propagated to all backend replicas — a mismatched salt invalidates tokens across instances.
  4. If token creation is intentionally disabled, use an identity provider (OAuth2) instead of API tokens.

Example fix

// before (.env)
SALT=salt
// after
SALT=9f2c7a1e4b8d...generated-64-hex-chars
Defensive patterns

Strategy: validation

Validate before calling

// ops check before deploying / calling POST /tokens
if [ -z "$SALT" ] || [ "$SALT" = "salt" ]; then echo "FATAL: set a strong SALT"; exit 1; fi

Type guard

func tokenCreationEnabled(salt string) bool { return salt != "" && salt != "salt" }

Try / catch

try { await api.post('/tokens', body); }
catch (e) {
  if (e.response?.status === 403 && e.response?.data?.code === 'Token.CreationDisabled') {
    notifyAdmin('set a non-default global salt to enable token creation');
  } else throw e;
}

Prevention

When it happens

Trigger: POST /tokens while the backend was started without a configured global salt (empty env value) or with the literal placeholder 'salt' (the shipped docker-compose default).

Common situations: Fresh deployment where .env was copied but the salt variable never changed; docker-compose defaults left in place; CI/staging environments reusing example configs; operator tried to create a personal API token right after install.

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/b5f72b8fe5f4674f. Report an issue: GitHub.