We can also use physics itself to do the clamping, at the end of each
// Project where our velocity will take us by the end of the frame.
Vector3 positionAtEndOfStep = rb.position + rb.velocity * Time.deltaTime;
// Limit that projected position to within our allowed bounds.
positionAtEndOfStep.x = Mathf.Clamp(positionAtEndOfStep.x, min, max);
// Compute a velocity that will take us to this clamped position instead.
Vector3 neededVelocity = (positionAtEndOfStep - rb.position)/Time.deltaTime;
// You can also calculate this as the needed velocity change/acceleration,
// and add it as a force instead if you prefer.
rb.velocity = neededVelocity;
In a step where we're moving toward the edge and about to cross it, this will slow but not quite stop the x velocity, without shifting the other components, so we arrive at the edge without crossing.
In a step where we're skating along the edge, this will zero-out the x velocity so we don't go past it. Velocity inward, away from the boundary, is preserved.
Note that if an object collides with this one midway through the step, that could add more velocity and nudge us past the barrier, but we'll correct in the next step.
For even tighter control, you can use a Joint component to limit the object's slide over one axis while leaving the others free. The physics engine will try to iteratively solve these constraints together with collisions that happen in the frame.