Usage of state machine to model lifetime of an IP address
Better late than never.

Yes, a state machine makes sense here.  You've chosen good names and your coding style is good.

A few comments:

The "puts" lines appear to be for debugging.  If that is so, and they are going to remain in the code, I would consider writing to an IO object rather than $stdout.  This will make tests much nicer to run, since when passing, you won't be bothered by all the debugging information.  For example:

attr_accessor :debug_io
...
after_transition any =&gt; any do |ip, transition|
  @debug_io.puts "after_transition -&gt; state was: #{transition.from}, state is: #{transition.to}"
end


And because you want it to still work even if the caller has not set debug_io, give it a default.  "No output" is a good default:

class NullIO
  def puts ; end
end

def initialize
  super
  @debug_io = NullIO.new
end


The return value of is_protected? depends only upon the state, being true if in the bound or protected state.  Remove:

before_transition any =&gt; :protected, :do =&gt; :activate_firewall_rules
before_transition any =&gt; :unprotected, :do =&gt; :deactivate_firewall_rules


and the methods they call, and change is_protected? to:

def is_protected?
  [:protected, :bound].include?(state)
end


Similarly, remove:

before_transition any =&gt; :bound, :do =&gt; :bind_to_server
before_transition any =&gt; :unbound, :do =&gt; :unbind_from_server


and the methods they call, and change is_bound? to:

def is_bound?
  [:bound, :unprotected].include?(state)
end


Ruby predicates usually leave off the "is_" prefix.  Consider changing is_bound? to bound? and is_protected? to protected?.  The is_ prefix comes from languages which do not allow question marks in identifiers.

Mark as "private" methods which are for the use of the class only, and should not be called by the class's user.  This will help prevent accidental misuse of the class, but more importantly, document the class's public signature, which helps in understanding and is a big help when refactoring.  Methods such as bind_to_server and unbind_from_server are private (or were, before they were removed).

It's not clear what this line is for:

throw :halt unless ip.send("is_#{transition.to}?")


If it's not for debugging, it seems like an odd bit of hidden coupling between the class and the rest of the program; I would try to find another way to do this that makes it clearer why the :halt is being thrown.