Well I figured it out; maybe this is helpful for 1.14.4 and beyond too.
Declare a Vec3d representing the offset in the world. This is how much the world will be translated around the camera.
The following statements transform that world offset into screen coordinates, so that no matter where the player is looking the camera moves by the same offset in the world.
Vec3d alignedToYaw = rotateAroundVector(offsetVec, new Vec3d(0, 1, 0), player.rotationYaw * Math.PI / 180);
Vec3d alignedToPitch = rotateAroundVector(alignedToYaw, new Vec3d(1, 0, 0), player.rotationPitch * Math.PI / 180);
//Put rotation first, so it comes out, then turns at the point it came out to
GL11.glRotatef(prevRotationRoll + (rotationRoll - prevRotationRoll) * f, 0.0F, 0.0F, 1.0F);
GL11.glTranslated(alignedToPitch.x, alignedToPitch.y, -alignedToPitch.z);
If you swap the rotate and translate, this is effectively rotating your offset vector and you won't see a camera tilt.
rotateAroundVector is a function that takes one vector and returns the rotated vector using another vector as the axis.
You can implement your own or see an example implementation here.